diff options
Diffstat (limited to 'testing/tools/websocketprocessbridge/websocketprocessbridge.py')
-rw-r--r-- | testing/tools/websocketprocessbridge/websocketprocessbridge.py | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/testing/tools/websocketprocessbridge/websocketprocessbridge.py b/testing/tools/websocketprocessbridge/websocketprocessbridge.py new file mode 100644 index 0000000000..f922194466 --- /dev/null +++ b/testing/tools/websocketprocessbridge/websocketprocessbridge.py @@ -0,0 +1,123 @@ +# vim: set ts=4 et sw=4 tw=80 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from twisted.internet import protocol, reactor +from twisted.internet.task import LoopingCall +from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory + +import psutil + +import argparse +import six +import sys +import os + +# maps a command issued via websocket to running an executable with args +commands = { + "iceserver": [sys.executable, "-u", os.path.join("iceserver", "iceserver.py")] +} + + +class ProcessSide(protocol.ProcessProtocol): + """Handles the spawned process (I/O, process termination)""" + + def __init__(self, socketSide): + self.socketSide = socketSide + + def outReceived(self, data): + data = six.ensure_str(data) + if self.socketSide: + lines = data.splitlines() + for line in lines: + self.socketSide.sendMessage(line.encode("utf8"), False) + + def errReceived(self, data): + self.outReceived(data) + + def processEnded(self, reason): + if self.socketSide: + self.outReceived(reason.getTraceback()) + self.socketSide.processGone() + + def socketGone(self): + self.socketSide = None + self.transport.loseConnection() + self.transport.signalProcess("KILL") + + +class SocketSide(WebSocketServerProtocol): + """ + Handles the websocket (I/O, closed connection), and spawning the process + """ + + def __init__(self): + super(SocketSide, self).__init__() + self.processSide = None + + def onConnect(self, request): + return None + + def onOpen(self): + return None + + def onMessage(self, payload, isBinary): + # We only expect a single message, which tells us what kind of process + # we're supposed to launch. ProcessSide pipes output to us for sending + # back to the websocket client. + if not self.processSide: + self.processSide = ProcessSide(self) + # We deliberately crash if |data| isn't on the "menu", + # or there is some problem spawning. + data = six.ensure_str(payload) + try: + reactor.spawnProcess( + self.processSide, commands[data][0], commands[data], env=os.environ + ) + except BaseException as e: + print(e.str()) + self.sendMessage(e.str()) + self.processGone() + + def onClose(self, wasClean, code, reason): + if self.processSide: + self.processSide.socketGone() + + def processGone(self): + self.processSide = None + self.transport.loseConnection() + + +# Parent process could have already exited, so this is slightly racy. Only +# alternative is to set up a pipe between parent and child, but that requires +# special cooperation from the parent. +parent_process = psutil.Process(os.getpid()).parent() + + +def check_parent(): + """Checks if parent process is still alive, and exits if not""" + if not parent_process.is_running(): + print("websocket/process bridge exiting because parent process is gone") + reactor.stop() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Starts websocket/process bridge.") + parser.add_argument( + "--port", + type=str, + dest="port", + default="8191", + help="Port for websocket/process bridge. Default 8191.", + ) + args = parser.parse_args() + + parent_checker = LoopingCall(check_parent) + parent_checker.start(1) + + bridgeFactory = WebSocketServerFactory() + bridgeFactory.protocol = SocketSide + reactor.listenTCP(int(args.port), bridgeFactory) + print("websocket/process bridge listening on port %s" % args.port) + reactor.run() |