# 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()
