#!/usr/bin/env python3""".. retroarcher.pyResponsible for starting RetroArcher."""# future importsfrom__future__importannotations# standard importsimportargparseimportosimportsysimporttimefromtypingimportUnion# local importsimportpyrafrompyraimportconfigfrompyraimportdefinitionsfrompyraimporthelpersfrompyraimportlocalesfrompyraimportloggerfrompyraimportthreadspy_name='pyra'# locales_=locales.get_text()# get loggerlog=logger.get_logger(name=py_name)
[docs]classIntRange(object):""" Custom IntRange class for argparse. Prevents printing out large list of possible choices for integer ranges. Parameters ---------- stop : int Range maximum value. start : int, default = 0 Range minimum value. Methods ------- __call__: Validate that value is within accepted range. Examples -------- >>> IntRange(0, 10) <retroarcher.IntRange object at 0x...> """def__init__(self,stop:int,start:int=0,):""" Initialize the IntRange class object. If stop is less than start, the values will be corrected automatically. """ifstop<start:stop,start=start,stopself.start,self.stop=start,stopdef__call__(self,value:Union[int,str])->int:""" Validate that value is within accepted range. Validate the provided value is within the range of the `IntRange()` object. Parameters ---------- value : Union[int, str] The value to validate. Returns ------- int The original value. Raises ------ argparse.ArgumentTypeError If provided value is outside the accepted range. Examples -------- >>> IntRange(0, 10).__call__(5) 5 >>> IntRange(0, 10).__call__(15) Traceback (most recent call last): ... argparse.ArgumentTypeError: Value outside of range: (0, 10) """value=int(value)ifvalue<self.startorvalue>=self.stop:raiseargparse.ArgumentTypeError(f'Value outside of range: ({self.start}, {self.stop})')returnvalue
[docs]defmain():""" Application entry point. Parses arguments and initializes the application. Examples -------- >>> if __name__ == "__main__": ... main() """# Fixed paths to RetroArcherifdefinitions.Modes.FROZEN:# only when using the pyinstaller buildifdefinitions.Modes.SPLASH:importpyi_splash# module cannot be installed outside of pyinstaller buildspyi_splash.update_text("Attempting to start RetroArcher")# Set up and gather command line arguments# todo... fix translations for '--help' commandparser=argparse.ArgumentParser(description=_('RetroArcher is a Python based game streaming server.\n''Arguments supplied here are meant to be temporary.'))parser.add_argument('--config',help=_('Specify a config file to use'))parser.add_argument('--debug',action='store_true',help=_('Use debug logging level'))parser.add_argument('--dev',action='store_true',help=_('Start RetroArcher in the development environment'))parser.add_argument('--docker_healthcheck',action='store_true',help=_('Health check the container and exit'))parser.add_argument('--nolaunch',action='store_true',help=_('Do not open RetroArcher in browser'))parser.add_argument('-p','--port',default=9696,type=IntRange(21,65535),help=_('Force RetroArcher to run on a specified port, default=9696'))parser.add_argument('-q','--quiet',action='store_true',help=_('Turn off console logging'))parser.add_argument('-v','--version',action='store_true',help=_('Print the version details and exit'))args=parser.parse_args()ifargs.docker_healthcheck:status=helpers.docker_healthcheck()exit_code=int(notstatus)sys.exit(exit_code)ifargs.version:print('version arg is not yet implemented')sys.exit()ifargs.config:config_file=args.configelse:config_file=os.path.join(definitions.Paths.DATA_DIR,definitions.Files.CONFIG)ifargs.debug:pyra.DEBUG=Trueifargs.dev:pyra.DEV=Trueifargs.quiet:pyra.QUIET=True# initialize retroarcher# logging should not occur until after initialize# any submodules that require translations need to be imported after config is initializepyra.initialize(config_file=config_file)ifargs.config:log.info(msg=f"RetroArcher is using custom config file: {config_file}.")ifargs.debug:log.info(msg="RetroArcher will log debug messages.")ifargs.dev:log.info(msg="RetroArcher is running in the dev environment.")ifargs.quiet:log.info(msg="RetroArcher is running in quiet mode. Nothing will be printed to console.")ifargs.port:config.CONFIG['Network']['HTTP_PORT']=args.portconfig.CONFIG.write()ifconfig.CONFIG['General']['SYSTEM_TRAY']:frompyraimporttray_icon# submodule requires translations so importing after initialization# also do not import if not required by config optionstray_icon.tray_run_threaded()# start the webappifdefinitions.Modes.SPLASH:# pyinstaller build only, not darwin platformspyi_splash.update_text("Starting the webapp")time.sleep(3)# show splash screen for a min of 3 secondspyi_splash.close()# close the splash screenfrompyraimportwebapp# import at use due to translationsthreads.run_in_thread(target=webapp.start_webapp,name='Flask',daemon=True).start()# this should be after starting flask appifconfig.CONFIG['General']['LAUNCH_BROWSER']andnotargs.nolaunch:url=f"http://127.0.0.1:{config.CONFIG['Network']['HTTP_PORT']}"helpers.open_url_in_browser(url=url)wait()# wait for signal
[docs]defwait():""" Wait for signal. Endlessly loop while `pyra.SIGNAL = None`. If `pyra.SIGNAL` is changed to `shutdown` or `restart` `pyra.stop()` will be executed. If KeyboardInterrupt signal is detected `pyra.stop()` will be executed. Examples -------- >>> wait() """frompyraimporthardware# submodule requires translations so importing after initializationlog.info("RetroArcher is ready!")whileTrue:# wait endlessly for a signalifnotpyra.SIGNAL:hardware.update()# update dashboard resource valuestry:time.sleep(1)exceptKeyboardInterrupt:pyra.SIGNAL='shutdown'else:log.info(f'Received signal: {pyra.SIGNAL}')ifpyra.SIGNAL=='shutdown':pyra.stop()elifpyra.SIGNAL=='restart':pyra.stop(restart=True)else:log.error('Unknown signal. Shutting down...')pyra.stop()break