From b91ef27bc2b558ce9a9160527e1dbdc040b3a7a3 Mon Sep 17 00:00:00 2001 From: DB Date: Thu, 5 Apr 2018 12:30:26 +0200 Subject: [PATCH] Run on Python2 and 3; add debugging to file; fix double-json decoding issue --- README.md | 4 ++- runwith.py | 72 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f517b12..2dc159a 100644 --- a/README.md +++ b/README.md @@ -183,10 +183,12 @@ The important parts are the **`name`**, which uniquely identifies the NM host, t This NM manifest has to be copied or symlinked [to the correct location for your operating system](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_manifests), under Linux a suitable location is `~/.mozilla/native-messaging-hosts/.json`, in our case thus `~/.mozilla/native-messaging-hosts/runwith.json`. -A Python NM program that works the way the addon expects (**`runwith.py`**) [is included in the repo](https://github.com/waldner/Firefox-RunWith/blob/master/runwith.py), so you should copy it somewhere and update its path in the NM manifest. +A Python NM program that works the way the addon expects (**`runwith.py`**) [is included in the repo](https://github.com/waldner/Firefox-RunWith/blob/master/runwith.py), so you should copy it somewhere and update its path in the NM manifest. It should work with Python 2 and Python 3. **`runwith.py`** speaks the NM protocol, it expects to receive on stdin a JSON array with the command to run and its arguments, runs the command according to the user's shell/wait preferences, then writes back (to the extension) a brief summary of the execution (in case you're interested, it can be seen in the browser console, which can be opened with CTRL+SHIFT+J, along with other debugging messages output by the [**`background.js`**](https://github.com/waldner/Firefox-RunWith/blob/master/addon/background.js) script). +Inside `runwith.py`, you can set the **`enable_debug`** variable to `True` to dump the various incoming and outgoing messages it processes to a file (by default, `/tmp/runwith_debug.log`, see the `debug_file` variable). + #### What do "`shell`" and "`wait`" do exactly? Let's assume you have a command like the following for an action in your configuration: diff --git a/runwith.py b/runwith.py index 05719fc..32fd621 100755 --- a/runwith.py +++ b/runwith.py @@ -1,4 +1,5 @@ -#!/usr/bin/python2 +#!/usr/bin/python + # 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. @@ -6,35 +7,64 @@ import sys, json, struct, os import subprocess +def debug(msg): + if enable_debug: + with open(debug_file, 'a') as outfile: + outfile.write(msg) + outfile.write("\n") + # Read a message from stdin and decode it. def getMessage(): - rawLength = sys.stdin.read(4) + rawLength = our_stdin.read(4) if len(rawLength) == 0: sys.exit(0) messageLength = struct.unpack('@I', rawLength)[0] - message = sys.stdin.read(messageLength) + debug("getMessage: messageLength is %s" % messageLength) + message = our_stdin.read(messageLength).decode() + debug("getMessage: message is %s" % message) return json.loads(message) # Encode a message for transmission, given its content. def encodeMessage(messageContent): + debug("encodeMessage: messageContent is %s" % messageContent) encodedContent = json.dumps(messageContent) + debug("encodeMessage: encodedContent is %s\n" % encodedContent) encodedLength = struct.pack('@I', len(encodedContent)) - return {'length': encodedLength, 'content': encodedContent} + debug("encodeMessage: encodedLength is %s" % encodedLength) + return {'length': encodedLength, 'content': encodedContent.encode()} # Send an encoded message to stdout. def sendMessage(encodedMessage): - sys.stdout.write(encodedMessage['length']) - sys.stdout.write(encodedMessage['content']) - sys.stdout.flush() + debug("sendMessage: encodedMessage is %s" % encodedMessage) + debug("sendMessage: encodedMessage['length'] is %s" % encodedMessage['length']) + debug("sendMessage: encodedMessage['content'] is %s" % encodedMessage['content']) + + our_stdout.write(encodedMessage['length']) + our_stdout.write(encodedMessage['content']) + our_stdout.flush() + +# determine Python version +python_version = sys.version_info[0] # should be 2 or 3 + +if python_version == 2: + our_stdin = sys.stdin + our_stdout = sys.stdout +else: + our_stdin = sys.stdin.buffer + our_stdout = sys.stdout.buffer -receivedMessage = getMessage() -msg = json.loads(receivedMessage) +# set this to true to dump things to the debug_file +enable_debug = False +debug_file = "/tmp/runwith_debug.log" -cmd = msg['cmd'] -shell = msg['shell'] -wait = msg['wait'] +receivedMessage = getMessage() +debug("main: receivedMessage is %s" % receivedMessage) + +cmd = receivedMessage['cmd'] +shell = receivedMessage['shell'] +wait = receivedMessage['wait'] use_shell = False command = cmd @@ -43,21 +73,25 @@ def sendMessage(encodedMessage): command = ' '.join(cmd) devnull = open(os.devnull, 'w') -stdout = devnull -stderr = devnull +child_stdout = devnull +child_stderr = devnull close_fds = True if wait: - stdout = subprocess.PIPE - stderr = subprocess.PIPE + child_stdout = subprocess.PIPE + child_stderr = subprocess.PIPE close_fds = False -proc = subprocess.Popen(command, stdout = stdout, stderr = stderr, shell = use_shell, close_fds = close_fds) +proc = subprocess.Popen(command, stdout = child_stdout, stderr = child_stderr, shell = use_shell, close_fds = close_fds) + +msg = { 'pid': proc.pid, 'shell': use_shell, 'wait': wait } if wait: stdout, stderr = proc.communicate() exit_code = proc.returncode - msg = "command ran, exit status: %s, stderr: %s" % (exit_code, stderr) + msg['exit-status'] = exit_code + msg['stderr'] = stderr.decode() else: - msg = "async process launched, PID: %s" % proc.pid + msg['exit-status'] = 'N/A' + msg['stderr'] = 'N/A' sendMessage(encodeMessage(msg))