remote_access.py 4.8 KB

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