Move auto restart code to a module

This commit is contained in:
Isaiah Odhner 2023-06-11 23:03:19 -04:00
parent 6946669a41
commit 0f5193dc50
2 changed files with 109 additions and 99 deletions

View File

@ -0,0 +1,105 @@
"""Automatically restarts the program when a file is changed."""
from __future__ import annotations
from typing import TYPE_CHECKING
import os
import sys
import psutil
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED
from watchdog.observers import Observer
if TYPE_CHECKING:
from .paint import PaintApp
def restart_program():
"""Restarts the current program, after resetting terminal state, and cleaning up file objects and descriptors."""
try:
_app.discard_backup()
except Exception as e:
print("Error discarding backup:", e)
try:
_app.exit()
# It's meant to eventually call this, but we need it immediately (unless we delay with asyncio perhaps)
# Otherwise the terminal will be left in a state where you can't (visibly) type anything
# if you exit the app after reloading, since the new process will pick up the old terminal state.
_app._driver.stop_application_mode() # type: ignore
except Exception as e:
print("Error stopping application mode. The command line may not work as expected. The `reset` command should restore it on Linux.", e)
try:
try:
if observer:
observer.stop()
observer.join(timeout=1)
if observer.is_alive():
print("Timed out waiting for file change observer thread to stop.")
except RuntimeError as e:
# Ignore "cannot join current thread" error
# join() might be redundant, but I'm keeping it just in case something with threading changes in the future
if str(e) != "cannot join current thread":
raise
except Exception as e:
print("Error stopping file change observer:", e)
try:
p = psutil.Process(os.getpid())
for handler in p.open_files() + p.connections():
try:
os.close(handler.fd)
except Exception as e:
print(f"Error closing file descriptor ({handler.fd}):", e)
except Exception as e:
print("Error closing file descriptors:", e)
# python = sys.executable
# os.execl(python, python, *sys.argv)
os.execl(sys.executable, *sys.orig_argv)
class RestartHandler(PatternMatchingEventHandler):
"""A handler for file changes"""
def on_any_event(self, event: FileSystemEvent):
if event.event_type in (EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED):
# These seem like they'd just cause trouble... they're not changes, are they?
return
print("Reloading due to FS change:", event.event_type, event.src_path)
_app.screen.styles.background = "red" # type: ignore
# The unsaved changes prompt seems to need call_from_thread,
# or else it gets "no running event loop",
# whereas restart_program() (inside or outside action_reload) needs to NOT use it,
# or else nothing happens.
# However, when _app.action_reload is called from the key binding,
# it seems to work fine with or without unsaved changes.
if _app.is_document_modified():
_app.call_from_thread(_app.action_reload) # type: ignore
else:
restart_program()
_app.screen.styles.background = "yellow" # type: ignore
def restart_on_changes(app: PaintApp):
"""Restarts the current program when a file is changed"""
global observer, _app
_app = app
observer = Observer()
handler = RestartHandler(
# Don't need to restart on changes to .css, since Textual will reload them in --dev mode
# Could include localization files, but I'm not actively localizing this app at this point.
# WET: WatchDog doesn't match zero directories for **, so we have to split up any patterns that use it.
patterns=[
"**/*.py", "*.py"
],
ignore_patterns=[
".history/**/*", ".history/*",
".vscode/**/*", ".vscode/*",
".git/**/*", ".git/*",
"node_modules/**/*", "node_modules/*",
"__pycache__/**/*", "__pycache__/*",
"venv/**/*", "venv/*",
],
ignore_directories=True,
)
observer.schedule(handler, path='.', recursive=True) # type: ignore
observer.start()

View File

@ -5,16 +5,12 @@ import os
from pathlib import Path
import re
import shlex
import sys
import psutil
import argparse
import asyncio
from enum import Enum
from random import randint, random
from typing import Any, NamedTuple, Optional, Callable, Iterator
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED
from watchdog.observers import Observer
import stransi
from rich.segment import Segment
from rich.style import Style
@ -42,103 +38,12 @@ from .file_dialogs import SaveAsDialogWindow, OpenDialogWindow
from .edit_colors import EditColorsDialogWindow
from .localization.i18n import get as _, load_language, remove_hotkey
from .wallpaper import get_config_dir, set_wallpaper
from .auto_restart import restart_on_changes, restart_program
from .__init__ import __version__
MAX_FILE_SIZE = 500000 # 500 KB
observer = None
def restart_program():
"""Restarts the current program, after file objects and descriptors cleanup"""
try:
app.discard_backup()
except Exception as e:
print("Error discarding backup:", e)
try:
app.exit()
# It's meant to eventually call this, but we need it immediately (unless we delay with asyncio perhaps)
# Otherwise the terminal will be left in a state where you can't (visibly) type anything
# if you exit the app after reloading, since the new process will pick up the old terminal state.
app._driver.stop_application_mode() # type: ignore
except Exception as e:
print("Error stopping application mode. The command line may not work as expected. The `reset` command should restore it on Linux.", e)
try:
try:
if observer:
observer.stop()
observer.join(timeout=1)
if observer.is_alive():
print("Timed out waiting for file change observer thread to stop.")
except RuntimeError as e:
# Ignore "cannot join current thread" error
# join() might be redundant, but I'm keeping it just in case something with threading changes in the future
if str(e) != "cannot join current thread":
raise
except Exception as e:
print("Error stopping file change observer:", e)
try:
p = psutil.Process(os.getpid())
for handler in p.open_files() + p.connections():
try:
os.close(handler.fd)
except Exception as e:
print(f"Error closing file descriptor ({handler.fd}):", e)
except Exception as e:
print("Error closing file descriptors:", e)
# python = sys.executable
# os.execl(python, python, *sys.argv)
os.execl(sys.executable, *sys.orig_argv)
class RestartHandler(PatternMatchingEventHandler):
"""A handler for file changes"""
def on_any_event(self, event: FileSystemEvent):
if event.event_type in (EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED):
# These seem like they'd just cause trouble... they're not changes, are they?
return
print("Reloading due to FS change:", event.event_type, event.src_path)
app.screen.styles.background = "red"
# The unsaved changes prompt seems to need call_from_thread,
# or else it gets "no running event loop",
# whereas restart_program() (inside or outside action_reload) needs to NOT use it,
# or else nothing happens.
# However, when app.action_reload is called from the key binding,
# it seems to work fine with or without unsaved changes.
if app.is_document_modified():
app.call_from_thread(app.action_reload)
else:
restart_program()
app.screen.styles.background = "yellow"
def restart_on_changes():
"""Restarts the current program when a file is changed"""
global observer
observer = Observer()
handler = RestartHandler(
# Don't need to restart on changes to .css, since Textual will reload them in --dev mode
# Could include localization files, but I'm not actively localizing this app at this point.
# WET: WatchDog doesn't match zero directories for **, so we have to split up any patterns that use it.
patterns=[
"**/*.py", "*.py"
],
ignore_patterns=[
".history/**/*", ".history/*",
".vscode/**/*", ".vscode/*",
".git/**/*", ".git/*",
"node_modules/**/*", "node_modules/*",
"__pycache__/**/*", "__pycache__/*",
"venv/**/*", "venv/*",
],
ignore_directories=True,
)
observer.schedule(handler, path='.', recursive=True) # type: ignore
observer.start()
# These can go away now that args are parsed up top
ascii_only_icons = False
@ -206,9 +111,6 @@ args = parser.parse_args()
load_language(args.language)
if args.restart_on_changes:
restart_on_changes()
# Most arguments are handled at the end of the file.
class MetaGlyphFont:
@ -4464,6 +4366,9 @@ if args.backup_folder:
# Active arguments
# The backup_folder must be set before recover_from_backup() is called below.
if args.restart_on_changes:
restart_on_changes(app)
if args.filename:
# if args.filename == "-" and not sys.stdin.isatty():
# app.image = AnsiArtDocument.from_text(sys.stdin.read())