How to set up Google Chrome Extension Native Messaging and sending parameters to native host properly?

68 Views Asked by At

I am developing a chrome extension and I need to implement a feature that allows to launch an .exe on button click AND pass user entered parameters to that .exe.

Feature details: User selects file and clicks on a button -> button triggers prompt menu, getting input from user -> if user provides input extension launches .exe file, passing user entered input to .exe's stdio stream -> .exe reads stdin and saves user entered input to txt file -> (optional) .exe sends success message to extension.

The problem is it does not work as expected and i have no idea what did i do wrong :(

Here is what i did and my results:

Button onClick logic:

content.js

    button.addEventListener('click', function() {
        allSelected = document.querySelectorAll('[aria-selected="true"]')
        console.log(allSelected.length)
        let allFIles = []
        allSelected.forEach(element => {
            // Get the parent element
            const parentElement = element.parentNode;
            
            // Check if the parent element has the data-id attribute
            if (parentElement && parentElement.hasAttribute('data-id')) {
                // Retrieve the value of the data-id attribute
                const dataIdValue = parentElement.getAttribute('data-id');
                console.log('Data ID value:', dataIdValue);
                allFIles.push(dataIdValue)
            } else {
                console.log('Parent element does not have a data-id attribute');
            }
        });
        const userInput = window.prompt(`You selected ${allFIles.length} files. Please, provide info on these files below.`, '');

        // Check if the user entered something
        if (userInput !== null) {
            // User clicked OK and entered some text
            console.log('User input:', userInput);
            allInfo = {"fileIds": allFIles, "newValues": userInput}
            chrome.runtime.sendMessage({ action: 'connectNative', data: allInfo});
        } else {
            // User clicked Cancel or closed the prompt dialog
            console.log('User canceled input.');
        }
    });

In my background.js file I tried using both chrome.runtime.connectNative+port.postMessage or chrome.runtime.sendNativeMessage. Trough a lot of trial and error I came up with this block of code that seems to work (to some extent):

background.js

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  console.log('Message received:', request);

  if (request.action === 'connectNative') {

    console.log("try");
    let port = chrome.runtime.connectNative('com.google_drive.edit_description'); 

    port.postMessage({filesToEdit: request["data"]})
    port.onMessage.addListener(function(message) {
      console.log("Message from native messaging host:", message);

    });
  }
});

I created com.my_extension.new_feature folder in NativeMessagingHost and provided full path to native-apps/editInfo.json

editInfo.json

{
    "name": "com.my_extension.new_feature",
    "description": "My Application",
    "path": "new_feature.exe",
    "type": "stdio",
    "allowed_origins": ["chrome-extension://<passed my_extension's id>/"]
  }

I added nativeMessaging permissions to manifest.json. So far so good. Then i created my .exe file from Python script (this is my primary programming language)

new_feature.py

import sys
import json
import logging

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                    datefmt='%d-%b-%y %H:%M:%S',
                    filename='new_feature.log',
                    filemode='a')


def main():
    logging.info("Started")

    input_data = sys.stdin.read()

    logging.info("Read stdin")
    logging.info(f"Input data: {input_data}")

    with open('output.txt', 'w') as f:
        f.write(input_data)

    logging.info("Wrote to file")
    send_message({"message": "Success"})



if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        logging.error(e)

It looks like everything should be fine now, but this is what happens: On button click my new_feature.exe is triggered. It writes to .log file 15-Feb-24 18:51:34 - root - INFO - Started and then it does nothing! I tried setting setTimeout before port.postMessage in background.js (just in case there are some time-related problems)- nothing changed, same results. But while experimenting I noticed that when I reload extension from in chrome://extensions/ it seems to "release" this part of code input_data = sys.stdin.read(), and successfuly runs the rest of the code, writing to .log

15-Feb-24 18:58:10 - root - INFO - Read stdin
15-Feb-24 18:58:10 - root - INFO - Input data: 6  {"filesToEdit":{"fileIds":[<fileIds>],"newValues":"<newValues>"}}
15-Feb-24 18:58:10 - root - INFO - Wrote to file

I don`t understand why this is happening and this does not solve my problem - I am not going to reload extension every time I want to use this feature. I read a lot of different answers here as well as everywhere else, including asking chatGPT. Nothing seemed to work.

Then I accidentally didn't comment the rest of the code when trying to send message from .exe to extension (why not...) AND IT WORKED. I have no idea why, here is the full code that reads stdin and writes to .log file with no problems:

new_feature.py

import sys
import json
import logging

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                    datefmt='%d-%b-%y %H:%M:%S',
                    filename='edit_gd_description.log',
                    filemode='a')

def send_message(message):
    sys.stdout.write(json.dumps(message))
    sys.stdout.flush()


def main():
    logging.info("Started")

    send_message({"message": "Waiting for input"})

    input_data = sys.stdin.read()

    logging.info("Read stdin")
    logging.info(f"Input data: {input_data}")

    with open('output.txt', 'w') as f:
        f.write(input_data)

    logging.info("Wrote to file")
    send_message({"message": "Success"})



if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        logging.error(e)

This logs Unchecked runtime.lastError: Error when communicating with the native messaging host. to background.js console, but the .exe works well, getting right input from stdin that extesion sends, and writing this input to .log and .txt.

I would like to know why is that happening, what did I do wrong? How can i make .exe send messages to extension?

Thank you for any help or ideas!

1

There are 1 best solutions below

0
guest271314 On

Here's your working code based on https://github.com/guest271314/NativeMessagingHosts/blob/main/nm_python.py. Native Messaging host uses a loop to keep the host active and not exit.

Nowhere in your code do you parse the message length, which happens in the below code in getMessage and encodeMessage.

#!/usr/bin/env -S python3 -u
# https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging
# https://github.com/mdn/webextensions-examples/pull/157
# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.

import sys
import json
import struct
import traceback
import logging

import logging

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                    datefmt='%d-%b-%y %H:%M:%S',
                    filename='new_feature.log',
                    filemode='a')

try:
    # Python 3.x version
    # Read a message from stdin and decode it.
    def getMessage():
        rawLength = sys.stdin.buffer.read(4)
        if len(rawLength) == 0:
            sys.exit(0)
        messageLength = struct.unpack('@I', rawLength)[0]
        message = sys.stdin.buffer.read(messageLength).decode('utf-8')
        return json.loads(message)

    # Encode a message for transmission,
    # given its content.
    def encodeMessage(messageContent):
        # https://stackoverflow.com/a/56563264
        # https://docs.python.org/3/library/json.html#basic-usage
        # To get the most compact JSON representation, you should specify 
        # (',', ':') to eliminate whitespace.
        encodedContent = json.dumps(messageContent, separators=(',', ':')).encode('utf-8')
        encodedLength = struct.pack('@I', len(encodedContent))
        return {'length': encodedLength, 'content': encodedContent}

    # Send an encoded message to stdout
    def sendMessage(encodedMessage):
        sys.stdout.buffer.write(encodedMessage['length'])
        sys.stdout.buffer.write(encodedMessage['content'])
        sys.stdout.buffer.flush()
        
    while True:
        receivedMessage = getMessage()
        logging.info("Started")
        logging.info("Read stdin")
        logging.info(f"Input data: {receivedMessage}")
        with open('output.txt', 'w') as f:
            f.write(str(receivedMessage))
        logging.info("Wrote to file")
        sendMessage(encodeMessage({"message": "Success"}))

except Exception as e:
    sys.stdout.buffer.flush()
    sys.stdin.buffer.flush()
    # https://discuss.python.org/t/how-to-read-1mb-of-input-from-stdin/22534/14
    with open('nm_python.log', 'w', encoding='utf-8') as f:
        traceback.print_exc(file=f)
    sys.exit(0)