remote_access.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """
  2. * Implementation of remote access
  3. Use https://github.com/wang0618/localshare service by running a ssh subprocess in PyWebIO application.
  4. The stdout of ssh process is the connection info.
  5. """
  6. import json
  7. import logging
  8. import os
  9. import threading
  10. import time
  11. import shlex
  12. from subprocess import Popen, PIPE
  13. import shutil
  14. logger = logging.getLogger(__name__)
  15. success_msg = """
  16. ================================================================================
  17. PyWebIO Application Remote Access
  18. Remote access address: {address}
  19. ================================================================================
  20. """
  21. _ssh_process = None # type: Popen
  22. def am_i_the_only_thread():
  23. """Whether the current thread is the only non-Daemon threads in the process"""
  24. alive_none_daemonic_thread_cnt = sum(
  25. 1 for t in threading.enumerate()
  26. if t.is_alive() and not t.isDaemon()
  27. )
  28. return alive_none_daemonic_thread_cnt == 1
  29. def remote_access_service(local_port=8080, server='app.pywebio.online', server_port=1022, setup_timeout=60):
  30. """
  31. Wait at most one minute to get the ssh output, if it gets a normal out, the connection is successfully established.
  32. Otherwise report error and kill ssh process.
  33. :param local_port: ssh local listen port
  34. :param server: ssh server domain
  35. :param server_port: ssh server port
  36. :param setup_timeout: If the service can't setup successfully in `setup_timeout` seconds, then exit.
  37. """
  38. global _ssh_process
  39. cmd = "ssh -oStrictHostKeyChecking=no -R 80:localhost:%s -p %s %s -- --output json" % (
  40. local_port, server_port, server)
  41. args = shlex.split(cmd)
  42. logger.debug('remote access service command: %s', cmd)
  43. _ssh_process = Popen(args, stdout=PIPE, stderr=PIPE)
  44. logger.debug('remote access process pid: %s', _ssh_process.pid)
  45. success = False
  46. def timeout_killer(wait_sec):
  47. time.sleep(wait_sec)
  48. if not success and _ssh_process.poll() is None:
  49. _ssh_process.kill()
  50. threading.Thread(target=timeout_killer, kwargs=dict(wait_sec=setup_timeout), daemon=True).start()
  51. stdout = _ssh_process.stdout.readline().decode('utf8')
  52. logger.debug('ssh server stdout: %s', stdout)
  53. connection_info = {}
  54. try:
  55. connection_info = json.loads(stdout)
  56. success = True
  57. except json.decoder.JSONDecodeError:
  58. if not success and _ssh_process.poll() is None:
  59. _ssh_process.kill()
  60. if success:
  61. if connection_info.get('status', 'fail') != 'success':
  62. print("Failed to establish remote access, this is the error message from service provider:",
  63. connection_info.get('message', ''))
  64. else:
  65. print(success_msg.format(address=connection_info['address']))
  66. # wait ssh or main thread exit
  67. while not am_i_the_only_thread() and _ssh_process.poll() is None:
  68. time.sleep(1)
  69. if _ssh_process.poll() is None: # main thread exit, kill ssh process
  70. logger.debug('App process exit, killing ssh process')
  71. _ssh_process.kill()
  72. else: # ssh process exit by itself or by timeout killer
  73. stderr = _ssh_process.stderr.read().decode('utf8')
  74. if stderr:
  75. logger.error('PyWebIO application remote access service error: %s', stderr)
  76. else:
  77. logger.info('PyWebIO application remote access service exit.')
  78. def start_remote_access_service_(**kwargs):
  79. try:
  80. remote_access_service(**kwargs)
  81. except KeyboardInterrupt: # ignore KeyboardInterrupt
  82. pass
  83. finally:
  84. if _ssh_process:
  85. logger.debug('Exception occurred, killing ssh process')
  86. _ssh_process.kill()
  87. raise SystemExit
  88. def start_remote_access_service(**kwargs):
  89. if not shutil.which("ssh"):
  90. return logging.error("No ssh client found, remote access service can't start.")
  91. server = os.environ.get('PYWEBIO_REMOTE_ACCESS', 'app.pywebio.online:1022')
  92. if ':' not in server:
  93. server_port = 22
  94. else:
  95. server, server_port = server.split(':', 1)
  96. kwargs.setdefault('server', server)
  97. kwargs.setdefault('server_port', server_port)
  98. thread = threading.Thread(target=start_remote_access_service_, kwargs=kwargs)
  99. thread.start()
  100. return thread
  101. if __name__ == '__main__':
  102. import argparse
  103. logging.basicConfig(level=logging.DEBUG)
  104. parser = argparse.ArgumentParser(description="localhost.run Remote Access service")
  105. parser.add_argument("--local-port", help="the local port to connect the tunnel to", type=int, default=8080)
  106. parser.add_argument("--server", help="the local port to connect the tunnel to", type=str,
  107. default='app.pywebio.online')
  108. parser.add_argument("--server-port", help="the local port to connect the tunnel to", type=int, default=1022)
  109. args = parser.parse_args()
  110. t = start_remote_access_service(local_port=args.local_port, server=args.server, server_port=args.server_port)
  111. t.join()