|
1 | 1 | """Entrypoint.""" |
2 | 2 |
|
3 | 3 | import asyncio |
4 | | -import functools |
5 | 4 | import logging |
6 | 5 | import signal |
7 | 6 | import sys |
8 | | -from typing import Coroutine, Sequence |
| 7 | +from typing import Sequence |
9 | 8 |
|
10 | 9 | from configargparse import ArgumentParser |
11 | 10 |
|
| 11 | +from ..config.error import ArgsParseError |
| 12 | + |
12 | 13 | try: |
13 | 14 | import uvloop |
14 | 15 | except ImportError: |
|
24 | 25 |
|
25 | 26 |
|
26 | 27 | async def start_app(conductor: Conductor): |
27 | | - """Start up.""" |
| 28 | + """Start up the application.""" |
28 | 29 | await conductor.setup() |
29 | 30 | await conductor.start() |
30 | 31 |
|
31 | 32 |
|
32 | 33 | async def shutdown_app(conductor: Conductor): |
33 | | - """Shut down.""" |
| 34 | + """Shut down the application.""" |
34 | 35 | LOGGER.info("Shutting down") |
35 | 36 | await conductor.stop() |
36 | 37 |
|
| 38 | + # Cancel remaining tasks |
| 39 | + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] |
| 40 | + for task in tasks: |
| 41 | + task.cancel() |
| 42 | + await asyncio.gather(*tasks, return_exceptions=True) |
| 43 | + |
37 | 44 |
|
38 | 45 | def init_argument_parser(parser: ArgumentParser): |
39 | 46 | """Initialize an argument parser with the module's arguments.""" |
40 | 47 | return arg.load_argument_groups(parser, *arg.group.get_registered(arg.CAT_START)) |
41 | 48 |
|
42 | 49 |
|
43 | | -def execute(argv: Sequence[str] = None): |
44 | | - """Entrypoint.""" |
| 50 | +async def run_app(argv: Sequence[str] = None): |
| 51 | + """Main async runner for the app.""" |
45 | 52 | parser = arg.create_argument_parser(prog=PROG) |
46 | 53 | parser.prog += " start" |
47 | 54 | get_settings = init_argument_parser(parser) |
48 | 55 | args = parser.parse_args(argv) |
49 | 56 | settings = get_settings(args) |
50 | 57 | common_config(settings) |
51 | 58 |
|
52 | | - # set ledger to read only if explicitly specified |
| 59 | + # Set ledger to read-only if explicitly specified |
53 | 60 | settings["ledger.read_only"] = settings.get("read_only_ledger", False) |
54 | 61 |
|
55 | | - # Create the Conductor instance |
56 | | - context_builder = DefaultContextBuilder(settings) |
57 | | - conductor = Conductor(context_builder) |
58 | | - |
59 | | - # Run the application |
60 | 62 | if uvloop: |
61 | 63 | uvloop.install() |
62 | 64 | LOGGER.info("uvloop installed") |
63 | | - run_loop(start_app(conductor), shutdown_app(conductor)) |
64 | | - |
65 | | - |
66 | | -def run_loop(startup: Coroutine, shutdown: Coroutine): |
67 | | - """Execute the application, handling signals and ctrl-c.""" |
68 | | - |
69 | | - async def init(cleanup): |
70 | | - """Perform startup, terminating if an exception occurs.""" |
71 | | - try: |
72 | | - await startup |
73 | | - except Exception: |
74 | | - LOGGER.exception("Exception during startup:") |
75 | | - cleanup() |
76 | | - |
77 | | - async def done(): |
78 | | - """Run shutdown and clean up any outstanding tasks.""" |
79 | | - await shutdown |
80 | | - |
81 | | - if sys.version_info.major == 3 and sys.version_info.minor > 6: |
82 | | - all_tasks = asyncio.all_tasks() |
83 | | - current_task = asyncio.current_task() |
84 | | - else: |
85 | | - all_tasks = asyncio.Task.all_tasks() |
86 | | - current_task = asyncio.Task.current_task() |
87 | | - |
88 | | - tasks = [task for task in all_tasks if task is not current_task] |
89 | | - for task in tasks: |
90 | | - task.cancel() |
91 | | - if tasks: |
92 | | - await asyncio.gather(*tasks, return_exceptions=True) |
93 | | - asyncio.get_event_loop().stop() |
94 | | - |
95 | | - loop = asyncio.get_event_loop() |
96 | | - cleanup = functools.partial(asyncio.ensure_future, done(), loop=loop) |
97 | | - loop.add_signal_handler(signal.SIGTERM, cleanup) |
98 | | - asyncio.ensure_future(init(cleanup), loop=loop) |
99 | 65 |
|
| 66 | + context_builder = DefaultContextBuilder(settings) |
| 67 | + conductor = Conductor(context_builder) |
| 68 | + |
| 69 | + loop = asyncio.get_running_loop() |
| 70 | + shutdown_event = asyncio.Event() |
| 71 | + |
| 72 | + def handle_signal(): |
| 73 | + LOGGER.info("Received stop signal") |
| 74 | + shutdown_event.set() |
| 75 | + |
| 76 | + loop.add_signal_handler(signal.SIGTERM, handle_signal) |
| 77 | + loop.add_signal_handler(signal.SIGINT, handle_signal) |
| 78 | + |
| 79 | + try: |
| 80 | + await start_app(conductor) |
| 81 | + await shutdown_event.wait() |
| 82 | + finally: |
| 83 | + await shutdown_app(conductor) |
| 84 | + |
| 85 | + |
| 86 | +def execute(argv: Sequence[str] = None): |
| 87 | + """Entrypoint.""" |
100 | 88 | try: |
101 | | - loop.run_forever() |
| 89 | + asyncio.run(run_app(argv)) |
| 90 | + except ArgsParseError as e: |
| 91 | + LOGGER.error("Argument parsing error: %s", e) |
| 92 | + raise e |
102 | 93 | except KeyboardInterrupt: |
103 | | - loop.run_until_complete(done()) |
| 94 | + LOGGER.info("Interrupted by user") |
| 95 | + except Exception: |
| 96 | + LOGGER.exception("Unexpected exception during execution") |
| 97 | + sys.exit(1) |
104 | 98 |
|
105 | 99 |
|
106 | 100 | def main(): |
107 | 101 | """Execute the main line.""" |
108 | | - if __name__ == "__main__": |
109 | | - execute() |
| 102 | + execute() |
110 | 103 |
|
111 | 104 |
|
112 | | -main() |
| 105 | +if __name__ == "__main__": |
| 106 | + main() |
0 commit comments