diff --git a/eris/eris/__main__.py b/eris/eris/__main__.py index 00f1b73..28c3f43 100755 --- a/eris/eris/__main__.py +++ b/eris/eris/__main__.py @@ -645,10 +645,9 @@ class Listing: class Screen: - def __init__(self, summary, log, main_loop): + def __init__(self, summary, log): self._summary = summary self._log = log - self._main_loop = main_loop self._is_summary_focused = True self.workers = None self._is_listing_portrait = True @@ -660,7 +659,6 @@ class Screen: def __getstate__(self): state = self.__dict__.copy() - state["_main_loop"] = None state["workers"] = None return state @@ -798,7 +796,7 @@ class Screen: self._summary.sort_entries() def quit_(self): - os.kill(os.getpid(), signal.SIGINT) + fill3.SHUTDOWN_EVENT.set() def refresh(self): selection = self._summary.get_selection() @@ -977,7 +975,7 @@ def setup_inotify(root_path, loop, on_filesystem_event, exclude_filter): return pyinotify.AsyncioNotifier(watch_manager, loop, callback=lambda notifier: None) -def load_state(pickle_path, jobs_added_event, root_path, loop): +def load_state(pickle_path, jobs_added_event, root_path): is_first_run = True try: with gzip.open(pickle_path, "rb") as file_: @@ -985,10 +983,9 @@ def load_state(pickle_path, jobs_added_event, root_path, loop): except (FileNotFoundError, AttributeError): summary = Summary(root_path, jobs_added_event) log = Log() - screen = Screen(summary, log, loop) + screen = Screen(summary, log) else: is_first_run = False - screen._main_loop = loop summary = screen._summary summary._jobs_added_event = jobs_added_event summary._root_path = root_path @@ -1017,8 +1014,9 @@ def on_filesystem_event(event, summary, root_path): fill3.APPEARANCE_CHANGED_EVENT.set() -def main(root_path, loop, worker_count=None, editor_command=None, theme=None, - compression=None, is_being_tested=False): +async def main(root_path, worker_count=None, editor_command=None, theme=None, compression=None, + is_being_tested=False): + loop = asyncio.get_running_loop() if worker_count is None: worker_count = max(multiprocessing.cpu_count() - 1, 1) if theme is None: @@ -1028,8 +1026,9 @@ def main(root_path, loop, worker_count=None, editor_command=None, theme=None, os.environ["PYGMENT_STYLE"] = theme pickle_path = os.path.join(tools.CACHE_PATH, "summary.pickle") jobs_added_event = asyncio.Event() - summary, screen, log, is_first_run = load_state(pickle_path, jobs_added_event, root_path, loop) + summary, screen, log, is_first_run = load_state(pickle_path, jobs_added_event, root_path) screen.editor_command = editor_command + fill3.APPEARANCE_CHANGED_EVENT = asyncio.Event() log.log_message("Program started.") jobs_added_event.set() @@ -1039,25 +1038,20 @@ def main(root_path, loop, worker_count=None, editor_command=None, theme=None, try: log.log_message(f"Starting workers ({worker_count}) …") screen.make_workers(worker_count, is_being_tested, compression) - - def exit_loop(): - log.log_command("Exiting…") - time.sleep(0.05) - screen.stop_workers() - loop.stop() loop.create_task(summary.sync_with_filesystem(log)) for worker_ in screen.workers: loop.create_task(worker_.future) - if sys.stdout.isatty(): - with fill3.context(loop, screen, exit_loop=exit_loop): - loop.run_forever() - log.log_message("Program stopped.") - else: - try: - loop.run_forever() - except KeyboardInterrupt: - screen.stop_workers() - loop.stop() + try: + if sys.stdout.isatty() or is_being_tested: + await fill3.tui(screen) + log.log_message("Program stopped.") + else: + shutdown_event = asyncio.Event() + with (fill3.signal_handler(loop, signal.SIGINT, shutdown_event.set), + fill3.signal_handler(loop, signal.SIGTERM, shutdown_event.set)): + await shutdown_event.wait() + finally: + screen.stop_workers() finally: notifier.stop() if summary.is_loaded: @@ -1163,9 +1157,8 @@ def entry_point(): manage_cache(root_path) with terminal.terminal_title("eris: " + os.path.basename(root_path)): with chdir(root_path): # FIX: Don't change directory if possible. - loop = asyncio.get_event_loop() try: - main(root_path, loop, worker_count, editor_command, theme, compression) + asyncio.run(main(root_path, worker_count, editor_command, theme, compression)) except pyinotify.WatchManagerError: inotify_watches_exceeded() diff --git a/eris/eris/worker.py b/eris/eris/worker.py index dfb3923..dfb5b9a 100755 --- a/eris/eris/worker.py +++ b/eris/eris/worker.py @@ -68,7 +68,7 @@ class Worker: log.log_message(Worker.AUTOSAVE_MESSAGE) screen.save() if self.is_being_tested: - os.kill(os.getpid(), signal.SIGINT) + fill3.SHUTDOWN_EVENT.set() jobs_added_event.clear() def kill(self): diff --git a/eris/tests/__main___test.py b/eris/tests/__main___test.py index 3e360e1..f49e1c1 100755 --- a/eris/tests/__main___test.py +++ b/eris/tests/__main___test.py @@ -33,12 +33,6 @@ def _assert_widget_appearance(widget, golden_path, dimensions=_DIMENSIONS): golden.assertGolden(_widget_to_string(widget, dimensions), golden_path_absolute) -class _MockMainLoop: - - def add_reader(self, foo, bar): - pass - - class ScreenWidgetTestCase(unittest.TestCase): def setUp(self): @@ -50,7 +44,7 @@ class ScreenWidgetTestCase(unittest.TestCase): jobs_added_event = asyncio.Event() summary = __main__.Summary(project_dir, jobs_added_event) log = __main__.Log() - self.main_widget = __main__.Screen(summary, log, _MockMainLoop()) + self.main_widget = __main__.Screen(summary, log) def tearDown(self): shutil.rmtree(self.temp_dir) @@ -111,6 +105,7 @@ class SummaryCursorTest(unittest.TestCase): class SummarySyncWithFilesystemTestCase(unittest.TestCase): def setUp(self): + fill3.APPEARANCE_CHANGED_EVENT = asyncio.Event() self.temp_dir = tempfile.mkdtemp() self.foo_path = os.path.join(self.temp_dir, "foo") self.bar_path = os.path.join(self.temp_dir, "bar.md") @@ -198,31 +193,31 @@ def _tmp_total(): class MainTestCase(unittest.TestCase): def test_main_and_restart_and_no_leaks_and_is_relocatable(self): - def test_run(root_path, loop): + def test_run(root_path): mount_total = _mount_total() tmp_total = _tmp_total() foo_path = os.path.join(root_path, "foo") open(foo_path, "w").close() __main__.manage_cache(root_path) with __main__.chdir(root_path): + loop = asyncio.get_event_loop() with contextlib.redirect_stdout(io.StringIO()): - __main__.main(root_path, loop, worker_count=2, is_being_tested=True) + loop.run_until_complete(__main__.main( + root_path, worker_count=2, is_being_tested=True)) for file_name in ["summary.pickle", "creation_time", "foo-metadata", "foo-contents"]: self.assertTrue(os.path.exists(".eris/" + file_name)) self.assertEqual(_mount_total(), mount_total) self.assertEqual(_tmp_total(), tmp_total) + temp_dir = tempfile.mkdtemp() try: - loop = asyncio.get_event_loop() first_dir = os.path.join(temp_dir, "first") os.mkdir(first_dir) - test_run(first_dir, loop) + test_run(first_dir) second_dir = os.path.join(temp_dir, "second") os.rename(first_dir, second_dir) - test_run(second_dir, loop) - loop.close() - loop.stop() + test_run(second_dir) finally: shutil.rmtree(temp_dir) diff --git a/fill3/fill3/__init__.py b/fill3/fill3/__init__.py index b51abb4..cd1cd99 100755 --- a/fill3/fill3/__init__.py +++ b/fill3/fill3/__init__.py @@ -406,7 +406,6 @@ class Fixed: ########################## -APPEARANCE_CHANGED_EVENT = asyncio.Event() _LAST_APPEARANCE = [] @@ -429,10 +428,10 @@ def patch_screen(widget): async def update_screen(screen_widget): while True: + patch_screen(screen_widget) + await asyncio.sleep(0.01) # Limit the update rate await APPEARANCE_CHANGED_EVENT.wait() APPEARANCE_CHANGED_EVENT.clear() - patch_screen(screen_widget) - await asyncio.sleep(0.01) def on_terminal_input(screen_widget): @@ -453,21 +452,21 @@ def signal_handler(loop, signal_, func): loop.remove_signal_handler(signal_) -@contextlib.contextmanager -def context(loop, screen_widget, exit_loop=None): - APPEARANCE_CHANGED_EVENT.set() - if exit_loop is None: - exit_loop = loop.stop +async def tui(screen_widget): + global APPEARANCE_CHANGED_EVENT + global SHUTDOWN_EVENT + APPEARANCE_CHANGED_EVENT = asyncio.Event() + SHUTDOWN_EVENT = asyncio.Event() + loop = asyncio.get_running_loop() with (signal_handler(loop, signal.SIGWINCH, lambda: draw_screen(screen_widget)), - signal_handler(loop, signal.SIGINT, exit_loop), - signal_handler(loop, signal.SIGTERM, exit_loop), terminal.alternate_buffer(), - terminal.interactive(), terminal.mouse_tracking()): - update_task = loop.create_task( - update_screen(screen_widget)) + signal_handler(loop, signal.SIGINT, SHUTDOWN_EVENT.set), + signal_handler(loop, signal.SIGTERM, SHUTDOWN_EVENT.set), + terminal.alternate_buffer(), terminal.interactive(), terminal.mouse_tracking()): + update_task = asyncio.create_task(update_screen(screen_widget)) try: loop.add_reader(sys.stdin, on_terminal_input, screen_widget) try: - yield + await SHUTDOWN_EVENT.wait() finally: loop.remove_reader(sys.stdin) finally: @@ -487,7 +486,7 @@ class _Screen: def on_keyboard_input(self, term_code): if term_code in ["q", terminal.ESC]: - asyncio.get_event_loop().stop() + SHUTDOWN_EVENT.set() else: self.content = Filler(Text(repr(term_code))) APPEARANCE_CHANGED_EVENT.set() @@ -498,11 +497,5 @@ class _Screen: APPEARANCE_CHANGED_EVENT.set() -def _main(): - loop = asyncio.get_event_loop() - with context(loop, _Screen()): - loop.run_forever() - - if __name__ == "__main__": - _main() + asyncio.run(tui(_Screen()))