#!/usr/bin/python3 # # Central daemon for all the services on gstein's home server. # This is a Quart-based app to present web pages, and to run # numerous background tasks. # import os.path import logging import asyncio import signal import functools import ezt import quart import yaml import asfquart # implies .utils import util LOGGER = logging.getLogger(__name__) CONFIG_FNAME = 'config.yaml' # Note: no decorators. Route will be added once an APP is constructed. async def startup(): ### anything to do? pass # Note: no decorators. Route will be added once an APP is constructed. async def shutdown(): ### wontonly kill everything? Or let each task their own ### individual destruction? print('BACKGROUND[APP]:', APP.background_tasks) print('BACKGROUND[APP_EXT]:', APP_EXT.background_tasks) # Note: no decorators. Route will be added once an APP is constructed. async def homepage(): links = [ _item(**link) for link in APP.cfg['links'] ] data = { 'message': 'message goes here', 'user': 'gstein', 'title': 'Home Page', 'links': links, } return data # Note: no decorators. Route will be added once an APP is constructed. async def favicon(): ### set ETag and cache values? return await quart.send_from_directory(util.STATIC_DIR, 'favicon.ico') # Note: no decorators. Route will be added once an APP is constructed. async def static_content(subdir, fname): dname = os.path.join(util.STATIC_DIR, subdir) ### set ETag and cache values. Especially for the bootstrap files ### to deal with upgrades. meh? versioned in the name. return await quart.send_from_directory(dname, fname) class _item: def __init__(self, **kwargs): vars(self).update(kwargs) def main(): ### DEBUG for now logging.basicConfig(level=logging.DEBUG) # Order these properly, as last-constructed becomes asfquart.APP global APP, APP_EXT APP_EXT = asfquart.construct('central:external', oauth=False) APP = asfquart.construct('central', oauth=False) print('APP_DIR:', APP.app_dir) # Add some various routes. ### note: if this group gets larger, then move to separate module that ### is imported after APP has been constructed. APP.route('/')(APP.use_template('templates/homepage.ezt')(homepage)) APP.route('/favicon.ico')(favicon) APP.route('//')(static_content) # Add some additional hooks APP.before_serving(startup) APP.after_serving(shutdown) #APP_EXT.before_serving(??) #APP_EXT.after_serving(??) cfg = yaml.safe_load(open(os.path.join(util.THIS_DIR, CONFIG_FNAME))) APP.cfg = cfg APP_EXT.cfg = cfg # Now that asfquart.APP now exists. Load each of our submodules, to # provide all our functionality. # This module monitors the DVD directories on the server, providing # change notifications to other feature modules (eg. media). import disk APP.dvdroot = disk.DVDRoot(APP.cfg['dvdroot']) APP.dvdroot.load() # Make the Alexa listener conditional, so that production and # dev/test do not fight for the UPnP mapping. if cfg['alexa']: LOGGER.info('Starting Alexa listener.') # Note: this actually places routes on APP_EXT import alexa else: LOGGER.info('SKIPPING Alexa listener.') import media # media management (DVDs, Plex, etc) # import udev # actions when a disc is inserted # import ripserver ### part of media? # import plexdirs ### part of media? # import bridge # move mosquitto content into InfluxDB host = cfg['hostname'] ### extra files we should watch to trigger an app reload extra_files = set() # NOTE: much of the code below is direct from quart/app.py:Quart.run() # This local "copy" is to deal with simultaneous operation of two # applications, with different routes/etc. loop = asyncio.new_event_loop() loop.set_debug(True) # Set the loop manually, so that any Futures are created in this loop. asyncio.set_event_loop(loop) extra_files = { os.path.join(util.THIS_DIR, CONFIG_FNAME), } ### for testing .runx() if False: # Test asfquart's .runx() APP.runx(host=host, port=cfg['port'], loop=loop, extra_files=extra_files) return # Get a constructor for an "awaitable" that triggers reload/shutdown. trigger = asfquart.base.QuartApp.factory_trigger(loop, extra_files) # Trigger work to shutdown/close APP_EXT. ext_shutdown_event = asyncio.Event() async def modified_trigger(): try: await trigger() except Exception as e: LOGGER.debug(f'APP TRIGGERED: {repr(e)}') # Signal APP_EXT to gracefully exit, and wait for its completion. ext_shutdown_event.set() # Now wait for APP_EXT to complete its exit. await asyncio.wait((task1, task2), return_when=asyncio.FIRST_COMPLETED) # Propagate the error to the LOOP, and have APP exit. raise t1 = APP.run_task(host=host, port=cfg['port'], debug=True, shutdown_trigger=modified_trigger, ) t2 = APP_EXT.run_task(host=host, port=cfg['port_ext'], debug=True, shutdown_trigger=ext_shutdown_event.wait, ) task1 = loop.create_task(t1, name='APP') task2 = loop.create_task(t2, name='APP_EXT') async def run_apps(): # Two applications need to be run simultaneously. gather() will run # and wait until ALL tasks have completed. # # task1 (APP) will only complete by throwing an exception to stop, # or to restart the server (to pick up code/config changes). # # task2 (APP_EXT) will only complete when it is told to, using the # EXT_SHUTDOWN_EVENT signal. # # When the loop processing in APP catches the exception, then # task1 will exit gracefully, or it will exec() a new process. # Because the exec() does not exit, the gather() cannot ensure # task2 has completed. Thus, modified_trigger() explicitly waits # for task2 to finish. return await asyncio.gather(task1, task2) print(f' * Serving Quart apps "{APP.name}" and "{APP_EXT.name}"') print(f' * Debug mode: {APP.debug}') print(f' * Using reloader: ALTERNATE') print(f' * Running on http://{host}:{cfg["port"]}') print(f' * ... and on http://{host}:{cfg["port_ext"]}') print(f' * ... CTRL + C to quit') asfquart.base.QuartApp.run_forever(loop, run_apps()) def CUSTOM_PROTOCOL_MAYBE(): def protocol_factory(): ### streams.py:start_server protocol = None return protocol ### app:run_task() ### asyncio.__init__:serve() ### asyncio.run:worker_serve() ### asyncio.streams:start_server() ### asyncio.streams:StreamReaderProtocol.connection_made() ### asyncio.tcp_server:TCPServer.__init__() #t2 = loop.create_server(protocol_factory, hostname, port, ...) if __name__ == '__main__': main()