""" * Implementation of remote access Use https://github.com/wang0618/localshare service by running a ssh subprocess in PyWebIO application. The stdout of ssh process is the connection info. """ import json import logging import os import threading import time import shlex from subprocess import Popen, PIPE import shutil logger = logging.getLogger(__name__) success_msg = """ ================================================================================ PyWebIO Application Remote Access Remote access address: {address} ================================================================================ """ _ssh_process = None # type: Popen def am_i_the_only_thread(): """Whether the current thread is the only non-Daemon threads in the process""" alive_none_daemonic_thread_cnt = sum( 1 for t in threading.enumerate() if t.is_alive() and not t.isDaemon() ) return alive_none_daemonic_thread_cnt == 1 def remote_access_service(local_port=8080, server='app.pywebio.online', server_port=1022, setup_timeout=60): """ Wait at most one minute to get the ssh output, if it gets a normal out, the connection is successfully established. Otherwise report error and kill ssh process. :param local_port: ssh local listen port :param server: ssh server domain :param server_port: ssh server port :param setup_timeout: If the service can't setup successfully in `setup_timeout` seconds, then exit. """ global _ssh_process cmd = "ssh -oStrictHostKeyChecking=no -R 80:localhost:%s -p %s %s -- --output json" % ( local_port, server_port, server) args = shlex.split(cmd) logger.debug('remote access service command: %s', cmd) _ssh_process = Popen(args, stdout=PIPE, stderr=PIPE) logger.debug('remote access process pid: %s', _ssh_process.pid) success = False def timeout_killer(wait_sec): time.sleep(wait_sec) if not success and _ssh_process.poll() is None: _ssh_process.kill() threading.Thread(target=timeout_killer, kwargs=dict(wait_sec=setup_timeout), daemon=True).start() stdout = _ssh_process.stdout.readline().decode('utf8') logger.debug('ssh server stdout: %s', stdout) connection_info = {} try: connection_info = json.loads(stdout) success = True except json.decoder.JSONDecodeError: if not success and _ssh_process.poll() is None: _ssh_process.kill() if success: if connection_info.get('status', 'fail') != 'success': print("Failed to establish remote access, this is the error message from service provider:", connection_info.get('message', '')) else: print(success_msg.format(address=connection_info['address'])) # wait ssh or main thread exit while not am_i_the_only_thread() and _ssh_process.poll() is None: time.sleep(1) if _ssh_process.poll() is None: # main thread exit, kill ssh process logger.debug('App process exit, killing ssh process') _ssh_process.kill() else: # ssh process exit by itself or by timeout killer stderr = _ssh_process.stderr.read().decode('utf8') if stderr: logger.error('PyWebIO application remote access service error: %s', stderr) else: logger.info('PyWebIO application remote access service exit.') def start_remote_access_service_(**kwargs): try: remote_access_service(**kwargs) except KeyboardInterrupt: # ignore KeyboardInterrupt pass finally: if _ssh_process: logger.debug('Exception occurred, killing ssh process') _ssh_process.kill() raise SystemExit def start_remote_access_service(**kwargs): if not shutil.which("ssh"): return logging.error("No ssh client found, remote access service can't start.") server = os.environ.get('PYWEBIO_REMOTE_ACCESS', 'app.pywebio.online:1022') if ':' not in server: server_port = 22 else: server, server_port = server.split(':', 1) kwargs.setdefault('server', server) kwargs.setdefault('server_port', server_port) thread = threading.Thread(target=start_remote_access_service_, kwargs=kwargs) thread.start() return thread if __name__ == '__main__': import argparse logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser(description="localhost.run Remote Access service") parser.add_argument("--local-port", help="the local port to connect the tunnel to", type=int, default=8080) parser.add_argument("--server", help="the local port to connect the tunnel to", type=str, default='app.pywebio.online') parser.add_argument("--server-port", help="the local port to connect the tunnel to", type=int, default=1022) args = parser.parse_args() t = start_remote_access_service(local_port=args.local_port, server=args.server, server_port=args.server_port) t.join()