Last active
October 3, 2017 11:27
-
-
Save luvies/7bca172196027cb00cfebd5238d44e9e to your computer and use it in GitHub Desktop.
Allows for a daemon to have a separate client command to control it. The DaemonClient simply passes all arguments to the DaemonServer in place (excluding the first executable argument) and while the server is processing the arguments, the client receives all the output & send all the input.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
import multiprocessing.connection # used to communicate between processes | |
# might re-work this communication to use either a multi-language socket | |
# lib or write my own | |
import socket | |
import sys | |
import traceback | |
# communication messages | |
MSG_ARGS = 0x01 | |
MSG_WRITE = 0x10 | |
MSG_FLUSH = 0x11 | |
MSG_WRITE_ERR = 0x18 | |
MSG_FLUSH_ERR = 0x19 | |
MSG_READLN = 0x21 | |
# control messages | |
MSG_DONE = 0xf0 | |
MSG_FAILED = 0xf1 | |
MSG_ERR = 0xf6 | |
# constants | |
_LOCAL_HOST = "127.0.0.1" | |
_LISTEN_TIMEOUT = 0.1 | |
class _DaemonStdout: | |
# replaces stdout while the command is executing | |
def __init__(self, conn): | |
self._conn = conn | |
def write(self, str): | |
self._conn.send([MSG_WRITE, str]) | |
def flush(self): | |
self._conn.send([MSG_FLUSH]) | |
class _DaemonStdin: | |
# replaces stdin while the command is executing | |
def __init__(self, conn): | |
self._conn = conn | |
def readline(self, size=None): | |
self._conn.send([MSG_READLN, size]) | |
return self._conn.recv() | |
class _DaemonStderr(_DaemonStdout): | |
# replaces stderr while the command is executing | |
def write(self, str): | |
self._conn.send([MSG_WRITE_ERR, str]) | |
def flush(self): | |
self._conn.send([MSG_FLUSH_ERR]) | |
class DaemonServer: | |
"""The daemon server | |
This class will listen for clients, and upon connecting will receive and | |
parse the arguments it was given. | |
""" | |
def __init__(self, port=None, arg_parser=None, cleanup_handler=None): | |
self._arg_parser = arg_parser | |
self._cleanup_handler = cleanup_handler | |
self._do_listen = True | |
self._listener = multiprocessing.connection.Listener( | |
(_LOCAL_HOST, 0 if port is None else port)) | |
self.__closed = False | |
# properties | |
@property | |
def port(self): | |
"""The current port the server is using. | |
This is used for if you make the server dynamically assign itself a port. | |
""" | |
return self._listener.address[1] | |
@property | |
def arg_parser(self): | |
"""Parses the arguments given by the daemon client. | |
The handler is in the format func(args). | |
Handler arguments: | |
args -> the arguments exactly as sys.argv had them on the client. | |
Returns: | |
bool -> Whether the command was executed successfully or not. This determines whether the client exits normally (on true) or exits with code 1 (on false). | |
While this function is running, print and input are replaced with functions | |
that redirect the parameters to the same functions on the client side. | |
""" | |
return self._arg_parser | |
@arg_parser.setter | |
def arg_parser(self, value): | |
if not callable(value) and not None: | |
raise TypeError("Argument parser has to be callable") | |
self._arg_parser = value | |
@property | |
def cleanup_handler(self): | |
"""Called after cleanup when closed. | |
Handler function has no arguments. | |
""" | |
return self._cleanup_handler | |
@cleanup_handler.setter | |
def cleanup_handler(self, value): | |
if not callable(value) and not None: | |
raise TypeError("Cleanup handler has to be callable") | |
self._cleanup_handler = value | |
# listener | |
def listen(self): | |
"""Starts listening for connections | |
Whenever a client connects, the arguments it was given are passed to the server, and then to the argument parser function. | |
Warning: This function will not handle KeyboardInterrupts or SystemExits (like the one argparse uses to display the help message), and as such these will be passed up without much change. If you are using a system like argparse, wrap the whole parser function in a try-except which catches SystemExit, and then pass it (since it will produce almost the exact same output). | |
""" | |
self._listener._listener._socket.settimeout(_LISTEN_TIMEOUT) | |
conn = None | |
while True: | |
try: | |
try: | |
conn = self._listener.accept() | |
except socket.timeout as ex: | |
# pass the socket timeout exception up | |
raise ex | |
except: | |
# all other exceptions are due to KeyboardInterrupt (or | |
# something similar) | |
raise KeyboardInterrupt() | |
msg = conn.recv() | |
if msg[0] == MSG_ARGS: | |
# if client send args message, process it | |
# this is used in case I want to add more options to it | |
success = None | |
try: | |
tmp_stdout, tmp_stdin, tmp_stderr = sys.stdout, sys.stdin, sys.stderr | |
if self.arg_parser is not None: | |
# redirect stdout and stdin | |
sys.stdout, sys.stdin, sys.stderr = _DaemonStdout( | |
conn), _DaemonStdin(conn), _DaemonStderr(conn) | |
# parse the arguments | |
# things like argparse raise SystemExit, these have to be handled in | |
# the function itself (to allow proper exits) | |
success = self.arg_parser(msg[1]) | |
# reset print and input | |
except Exception as ex: | |
# if an exception occurred, pass the relevant info to the client | |
# and exit normally | |
type, value, tb = sys.exc_info() | |
conn.send( | |
[MSG_ERR, value, traceback.format_tb(tb)]) | |
raise ex | |
finally: | |
# cleanup std I/O objects | |
sys.stdout, sys.stdin, sys.stderr = tmp_stdout, tmp_stdin, tmp_stderr | |
# tell the client whether the command was successful or | |
# failed | |
if success: | |
conn.send([MSG_DONE]) | |
else: | |
conn.send([MSG_FAILED]) | |
conn.close() | |
if not self._do_listen: | |
break | |
except socket.timeout: | |
if not self._do_listen: | |
# if we are no longer listening, don't try again | |
break | |
# context management | |
def __enter__(self): | |
return self | |
def __exit__(self, type, value, tb): | |
self.close() | |
def close(self): | |
"""Closes the server down | |
""" | |
if not self.__closed: | |
self._do_listen = False | |
self._listener.close() | |
if self.cleanup_handler is not None: | |
self.cleanup_handler() | |
self.__closed = True | |
class DaemonClient: | |
"""The daemon client | |
This class will connect to the server on the given port, pass all command line arguments to it. | |
It will then receive any output (and print it to the terminal), and handle any input, and send it back to the server. | |
""" | |
def __init__(self, port): | |
self._port = port | |
def process(self): | |
"""Processes the client, sending arguments to the server and handling stdout, stdin and stderr. | |
""" | |
with multiprocessing.connection.Client((_LOCAL_HOST, self._port)) as conn: | |
conn.send([MSG_ARGS, sys.argv[1:]]) | |
while True: | |
try: | |
msg = conn.recv() | |
if msg[0] == MSG_WRITE: | |
sys.stdout.write(msg[1]) | |
elif msg[0] == MSG_FLUSH: | |
sys.stdout.flush() | |
elif msg[0] == MSG_WRITE_ERR: | |
sys.stderr.write(msg[1]) | |
elif msg[0] == MSG_FLUSH_ERR: | |
sys.stderr.flush() | |
elif msg[0] == MSG_READLN: | |
ln = None | |
if msg[1] is None: | |
ln = sys.stdin.readline() | |
else: | |
ln = sys.stdin.readline(msg[1]) | |
conn.send(ln) | |
elif msg[0] == MSG_ERR: | |
# an error happened server-side, so output it and exit | |
# with code 1 | |
print( | |
"Daemon encountered an error while processing the command") | |
print(msg[1]) | |
print("".join(msg[2])) | |
exit(1) | |
elif msg[0] == MSG_DONE: | |
break # exit normally | |
elif msg[0] == MSG_FAILED: | |
exit(1) # exit with code 1 | |
else: | |
# the server sent some unknown message, so just leave | |
print("Daemon sent an unknown message") | |
exit(1) | |
except EOFError: | |
# connection closed without finished message, assume work | |
# was not done | |
print("Daemon unexpectedly closed the connection") | |
exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment