def test_missing_global_configuration_file(self, monkeypatch, tmpdir): """Missing astrality.yml should result in default values.""" # Create directory used as astrality config directory config_home = Path(tmpdir) monkeypatch.setattr( os, 'environ', {'ASTRALITY_CONFIG_HOME': str(config_home)}, ) # Sanity check assert len(list(config_home.iterdir())) == 0 # Create modules and context files, but *not* astrality.yml modules = {'A': {'enabled': False}} dump_yaml(path=config_home / 'modules.yml', data=modules) context = {'section': {'key': 'value'}} dump_yaml(path=config_home / 'context.yml', data=context) ( global_config, global_modules, global_context, inferred_path, ) = user_configuration() assert global_config == ASTRALITY_DEFAULT_GLOBAL_SETTINGS assert global_modules == modules assert global_context == context assert inferred_path == config_home
def on_application_config_modified(self): """ Reload the ModuleManager if astrality.yml has been modified. Reloadnig the module manager only occurs if the user has configured `hot_reload_config`. """ if not self.application_config['config/astrality']['hot_reload_config']: # Hot reloading is not enabled, so we return early return # Hot reloading is enabled, get the new configuration dict new_application_config = user_configuration( config_directory=self.config_directory, ) try: # Reinstantiate this object new_module_manager = ModuleManager(new_application_config) # Run all old exit actions, since the new config is valid self.exit() # Swap place with the new configuration self = new_module_manager # Run startup commands from the new configuration self.finish_tasks() except Exception: # New configuration is invalid, just keep the old one # TODO: Test this behaviour logger.error('New configuration detected, but it is invalid!') pass
def on_application_config_modified(self): """ Reload the ModuleManager if astrality.yml has been modified. Reloadnig the module manager only occurs if the user has configured `hot_reload_config`. """ if not self.application_config.get( 'astrality', {}, ).get( 'hot_reload_config', False, ): # Hot reloading is not enabled, so we return early logger.info('"hot_reload" disabled.') return # Hot reloading is enabled, get the new configuration dict logger.info('Reloading $ASTRALITY_CONFIG_HOME...') ( new_application_config, new_modules, new_context, directory, ) = user_configuration( config_directory=self.config_directory, ) try: # Reinstantiate this object new_module_manager = ModuleManager( config=new_application_config, modules=new_modules, context=new_context, directory=directory, ) # Run all old exit actions, since the new config is valid self.exit() # Swap place with the new configuration self = new_module_manager # Run startup commands from the new configuration self.finish_tasks() except Exception: # New configuration is invalid, just keep the old one # TODO: Test this behaviour logger.error('New configuration detected, but it is invalid!') pass
def test_get_user_configuration(self, dir_with_compilable_files): user_conf = user_configuration(dir_with_compilable_files) assert user_conf['key1'] == 'test_value' assert user_conf['key2'] == 'test'
def main( modules: List[str] = [], logging_level: str = 'INFO', dry_run: bool = False, test: bool = False, ): """ Run the main process for Astrality. :param modules: Modules to be enabled. If empty, use astrality.yml. :param logging_level: Loging level. :param dry_run: If file system actions should be printed and skipped. :param test: If True, return after one iteration loop. """ if 'ASTRALITY_LOGGING_LEVEL' in os.environ: # Override logging level if env variable is set logging_level = os.environ['ASTRALITY_LOGGING_LEVEL'] # Set the logging level to the configured setting logging.basicConfig( level=logging.getLevelName(logging_level), # type: ignore ) if not modules and not dry_run and not test: # Quit old astrality instances kill_old_astrality_processes() # How to quit this process def exit_handler(signal=None, frame=None) -> None: """ Cleanup all temporary files and run module exit handlers. The temp directory is left alone, for two reasons: 1: An empty directory uses neglible disk space. 2: If this process is interrupted by another Astrality instance, we might experience race conditions when the exit handler deletes the temporary directory *after* the new Astrality instance creates it. """ logger.critical('Astrality was interrupted') logger.info('Cleaning up temporary files before exiting...') try: # Run all the module exit handlers module_manager.exit() except NameError: # The module_manager instance has not been assigned yet. pass try: sys.exit(0) except SystemExit: os._exit(0) # Some SIGINT signals are not properly interupted by python and converted # into KeyboardInterrupts, so we have to register a signal handler to # safeguard against such cases. This seems to be the case when conky is # launched as a subprocess, making it the process that receives the SIGINT # signal and not python. These signal handlers cause issues for \ # NamedTemporaryFile.close() though, so they are only registrered when # we are not testing. if not test: signal.signal(signal.SIGINT, exit_handler) # Also catch kill-signkal from OS, # e.g. `kill $(pgrep -f "python astrality.py")` signal.signal(signal.SIGTERM, exit_handler) try: ( config, module_configs, global_context, directory, ) = user_configuration() if modules: config['modules']['enabled_modules'] = [{ 'name': module_name } for module_name in modules] # Delay further actions if configuration says so time.sleep(config['astrality']['startup_delay']) module_manager = ModuleManager( config=config, modules=module_configs, context=global_context, directory=directory, dry_run=dry_run, ) module_manager.finish_tasks() while True: if module_manager.has_unfinished_tasks(): # TODO: Log which new event which has been detected logger.info('New event detected.') module_manager.finish_tasks() logger.info(f'Event change routine finished.') if test or dry_run: logger.debug('Main loop interupted due to --dry-run.') return elif not module_manager.keep_running: logger.info( 'No more tasks to be performed. ' 'Executing on_exit blocks.', ) module_manager.exit() return else: logger.info( f'Waiting {module_manager.time_until_next_event()} ' 'until next event change and ensuing update.', ) # Weird bug related to sleeping more than 10e7 seconds # on MacOS, causing OSError: Invalid Argument wait = module_manager.time_until_next_event().total_seconds() if wait >= 10e7: wait = 10e7 time.sleep(wait) except KeyboardInterrupt: # pragma: no cover exit_handler()
def test_get_user_configuration(self, dir_with_compilable_files): """user_configuration should use compile_yaml properly.""" user_conf, *_ = user_configuration(dir_with_compilable_files) assert user_conf['key1'] == 'test_value' assert user_conf['key2'] == 'test'
def modules(): """Return the modules object for the example configuration.""" this_test_file = os.path.abspath(__file__) conf_path = Path(this_test_file).parents[1] / 'config' return user_configuration(conf_path)[1]