async def setup(self): ''' Extend setup to also close the boot logger and, if desired, save the config. ''' await super().setup() try: if self._save_cfg: user_id = self.account._user_id fingerprint = self.account._fingerprint logger.critical('Account created. Record these values in ' + 'case you need to log in to Hypergolix from ' + 'another machine, or in case your config ' + 'file is corrupted or lost:') logger.critical('User ID:\n' + user_id.as_str()) logger.critical('Fingerprint:\n' + fingerprint.as_str()) with Config.load(self._cfg_path) as config: config.user.fingerprint = fingerprint config.user.user_id = user_id finally: logger.critical('Hypergolix boot complete.') self._boot_logger.stop()
def test_full(self): ''' Test a full command chain for everything. ''' with tempfile.TemporaryDirectory() as root: root = pathlib.Path(root) blank = Config(root / 'hypergolix.yml') blank.coerce_defaults() debug = Config(root / 'hypergolix.yml') debug.coerce_defaults() debug.instrumentation.debug = True nodebug = Config(root / 'hypergolix.yml') nodebug.coerce_defaults() nodebug.instrumentation.debug = False loud = Config(root / 'hypergolix.yml') loud.coerce_defaults() loud.instrumentation.verbosity = 'info' loud.instrumentation.debug = False normal = Config(root / 'hypergolix.yml') normal.coerce_defaults() normal.instrumentation.verbosity = 'warning' normal.instrumentation.debug = False host1 = Config(root / 'hypergolix.yml') host1.coerce_defaults() host1.remotes.append(Remote('host1', 123, True)) host1.instrumentation.verbosity = 'warning' host1.instrumentation.debug = False host1hgx = Config(root / 'hypergolix.yml') host1hgx.coerce_defaults() host1hgx.remotes.append(Remote('host1', 123, True)) host1hgx.remotes.append(Remote('hgx.hypergolix.com', 443, True)) host1hgx.instrumentation.verbosity = 'warning' host1hgx.instrumentation.debug = False host1host2f = Config(root / 'hypergolix.yml') host1host2f.coerce_defaults() host1host2f.remotes.append(Remote('host1', 123, True)) host1host2f.remotes.append(Remote('host2', 123, False)) host1host2f.instrumentation.verbosity = 'warning' host1host2f.instrumentation.debug = False host1host2 = Config(root / 'hypergolix.yml') host1host2.coerce_defaults() host1host2.remotes.append(Remote('host1', 123, True)) host1host2.remotes.append(Remote('host2', 123, True)) host1host2.instrumentation.verbosity = 'warning' host1host2.instrumentation.debug = False # Definitely want to control the order of execution for this. valid_commands = [ ('config', blank), ('config --whoami', blank) ] deprecated_commands = [ ('config --debug', debug), ('config --no-debug', nodebug), ('config --verbosity loud', loud), ('config --verbosity normal', normal), ('config -ah host1 123 t', host1), ('config --addhost host1 123 t', host1), ('config -a hgx', host1hgx), ('config --add hgx', host1hgx), ('config -r hgx', host1), ('config --remove hgx', host1), # Note switch of TLS flag ('config -ah host2 123 f', host1host2f), # Note return of TLS flag ('config --addhost host2 123 t', host1host2), ('config -rh host2 123', host1), ('config --removehost host2 123', host1), ('config -o local', normal), ('config --only local', normal), ('config -ah host1 123 t -ah host2 123 t', host1host2), ] failing_commands = [ 'config -zz top', 'config --verbosity XXXTREEEEEEEME', 'config --debug --no-debug', 'config -o local -a hgx', ] for cmd_str, cmd_result in valid_commands: with self.subTest(cmd_str): cfg_path = root / 'hypergolix.yml' # NOTE THAT THESE TESTS ARE CUMULATIVE! We definitely DON'T # want to start with a fresh config each time around, or # the tests will fail! argv = cmd_str.split() argv.append('--root') argv.append(str(cfg_path)) with _NoSTDOUT(): ingest_args(argv) config = Config.load(cfg_path) # THE PROBLEM HERE IS NOT JUST COERCE DEFAULTS! config.py, # in its handle_args section, is passing in default values # that are interfering with everything else. self.assertEqual(config, cmd_result) for cmd_str, cmd_result in deprecated_commands: with self.subTest(cmd_str): cfg_path = root / 'hypergolix.yml' # NOTE THAT THESE TESTS ARE CUMULATIVE! We definitely DON'T # want to start with a fresh config each time around, or # the tests will fail! argv = cmd_str.split() argv.append('--root') argv.append(str(cfg_path)) with _NoSTDOUT(), self.assertWarns(DeprecationWarning): ingest_args(argv) config = Config.load(cfg_path) # THE PROBLEM HERE IS NOT JUST COERCE DEFAULTS! config.py, # in its handle_args section, is passing in default values # that are interfering with everything else. self.assertEqual(config, cmd_result) # Don't need the temp dir for this; un-context to escape permissions for cmd_str in failing_commands: with self.subTest(cmd_str): argv = cmd_str.split() argv.append('--root') argv.append(str(root)) # Note that argparse will always push usage to stderr in a # suuuuuuper annoying way if we don't suppress it. with self.assertRaises(SystemExit), _NoSTDERR(), _NoSTDOUT(): ingest_args(argv)
def start(namespace=None): ''' Starts a Hypergolix daemon. ''' # Command arg support is deprecated. if namespace is not None: # Gigantic error trap if ((namespace.host is not None) | (namespace.port is not None) | (namespace.debug is not None) | (namespace.traceur is not None) | (namespace.pidfile is not None) | (namespace.logdir is not None) | (namespace.cachedir is not None) | (namespace.chdir is not None) | (namespace.verbosity is not None)): raise RuntimeError('Server configuration through CLI is no ' + 'longer supported. Edit hypergolix.yml ' + 'configuration file instead.') with Daemonizer() as (is_setup, daemonizer): # Get our config path in setup, so that we error out before attempting # to daemonize (if anything is wrong). if is_setup: config = Config.find() config_path = config.path chdir = config_path.parent pid_file = config.server.pid_file else: config_path = None pid_file = None chdir = None # Daemonize. is_parent, config_path = daemonizer( str(pid_file), config_path, chdir = str(chdir), explicit_rescript = '-m hypergolix.service' ) ##################### # PARENT EXITS HERE # ##################### config = Config.load(config_path) _ensure_dir_exists(config.server.ghidcache) _ensure_dir_exists(config.server.logdir) debug = _default_to(config.server.debug, False) verbosity = _default_to(config.server.verbosity, 'info') logutils.autoconfig( tofile = True, logdirname = config.server.logdir, logname = 'hgxserver', loglevel = verbosity ) logger.debug('Parsing config...') host = _cast_host(config.server.host) rps = RemotePersistenceServer( config.server.ghidcache, host, config.server.port, reusable_loop = False, threaded = False, debug = debug ) logger.debug('Starting health check...') # Start a health check healthcheck_server, healthcheck_thread = _serve_healthcheck() healthcheck_thread.start() logger.debug('Starting signal handler...') def signal_handler(signum): logger.info('Caught signal. Exiting.') healthcheck_server.shutdown() rps.stop_threadsafe_nowait() # Normally I'd do this within daemonization, but in this case, we need to # wait to have access to the handler. sighandler = SignalHandler1( str(config.server.pid_file), sigint = signal_handler, sigterm = signal_handler, sigabrt = signal_handler ) sighandler.start() logger.info('Starting remote persistence server...') rps.start()
def run_daemon(cfg_path, pid_file, parent_port, account_entity, root_secret): ''' Start the actual Hypergolix daemon. ''' # Start reporting to our parent about how stuff is going. parent_signaller = _StartupReporter(parent_port) startup_logger = parent_signaller.start() try: config = Config.load(cfg_path) # Convert paths to strs and make sure the dirs exist cache_dir = str(config.process.ghidcache) log_dir = str(config.process.logdir) _ensure_dir_exists(config.process.ghidcache) _ensure_dir_exists(config.process.logdir) debug = _default_to(config.instrumentation.debug, False) verbosity = _default_to(config.instrumentation.verbosity, 'info') logutils.autoconfig( tofile = True, logdirname = log_dir, loglevel = verbosity, logname = 'hgxapp' ) ipc_port = config.process.ipc_port remotes = config.remotes # Look to see if we have an existing user_id to determine behavior save_cfg = not bool(config.user.user_id) hgxcore = _DaemonCore( cache_dir = cache_dir, ipc_port = ipc_port, reusable_loop = False, threaded = False, debug = debug, cfg_path = cfg_path, save_cfg = save_cfg, boot_logger = parent_signaller ) for remote in remotes: hgxcore.add_remote( connection_cls = WSBeatingConn, host = remote.host, port = remote.port, tls = remote.tls ) account = Account( user_id = account_entity, root_secret = root_secret, hgxcore = hgxcore ) hgxcore.account = account # We need a signal handler for that. def signal_handler(signum): logger.info('Caught signal. Exiting.') hgxcore.stop_threadsafe_nowait() # Normally I'd do this within daemonization, but in this case, we need # to wait to have access to the handler. sighandler = SignalHandler1( pid_file, sigint = signal_handler, sigterm = signal_handler, sigabrt = signal_handler ) sighandler.start() startup_logger.info('Booting Hypergolix...') hgxcore.start() except Exception: startup_logger.error('Failed to start Hypergolix:\n' + ''.join(traceback.format_exc())) raise finally: # This is idempotent, so no worries if we already called it parent_signaller.stop()