pull/182/head
Anthony Leung 2024-06-25 10:13:43 +00:00
rodzic 53a6456624
commit 66b00f8fa7
1 zmienionych plików z 41 dodań i 225 usunięć

Wyświetl plik

@ -3,7 +3,9 @@ import os
import logging import logging
import glob import glob
import signal import signal
import sys
import threading import threading
import time
from . import App, AppOptions, AppListenOptions from . import App, AppOptions, AppListenOptions
help = """ help = """
@ -43,7 +45,7 @@ Options:
--task-factory-maxitems INT Pre allocated instances of Task objects for socketify, ASGI interface [default: 100000] --task-factory-maxitems INT Pre allocated instances of Task objects for socketify, ASGI interface [default: 100000]
--reload Enable auto-reload. This options also disable --workers or -w option. --reload Enable auto-reload. This options also disable --workers or -w option.
--reload-ignore-patterns Comma delimited list of ignore strings Default "__pycache__,node_modules,build,target,.git" could include gitignore? --reload-ignore-patterns Comma delimited list of ignore strings Default "__pycache__,node_modules,.git"
Example: Example:
python3 -m socketify main:app -w 8 -p 8181 python3 -m socketify main:app -w 8 -p 8181
@ -95,9 +97,10 @@ def str_bool(text):
return text == "true" return text == "true"
class ReloadState: class ReloadState:
# Class object to store reload state """ Object to store reload state
# Windows only catches (SIGTERM) but it's also used Windows only catches (SIGTERM) but it's also used
# for other purposes, so we set a switch for other purposes, so we set a switch so that execuet() knows whether
to restart or shut down """
def __init__(self): def __init__(self):
self.reload_pending = False self.reload_pending = False
@ -123,8 +126,6 @@ def load_module(file, reload=False):
def execute(args): def execute(args):
print('cli.execute')
try: try:
_execute(args) _execute(args)
except SystemExit as se: except SystemExit as se:
@ -132,13 +133,6 @@ def execute(args):
if 'reload' in str(se) and '--reload' in args and reload_state.reload_pending: if 'reload' in str(se) and '--reload' in args and reload_state.reload_pending:
logging.info('RELOADING...') logging.info('RELOADING...')
reload_state.reload_pending = False reload_state.reload_pending = False
import sys
import os
#print(args)
#print(sys.argv)
#os.execv(sys.executable, ['-m socketify'] + args[1:])
#print(sys.executable, [sys.executable, '-m', 'socketify'] + args[1:])
# The app.run has already caught SIGTERM which closes the loop then raises SystemExit. # The app.run has already caught SIGTERM which closes the loop then raises SystemExit.
# SIGTERM works across both Windows and Linux # SIGTERM works across both Windows and Linux
@ -150,8 +144,7 @@ def execute(args):
sys.exit(0) sys.exit(0)
# *ix # *ix
os.execv(sys.executable, [sys.executable, '-m', 'socketify'] + args[1:]) os.execv(sys.executable, [sys.executable, '-m', 'socketify'] + args[1:])
#os.kill(os.getpid(), signal.SIGINT) <-- this done in the file probe
#or os.popen("wmic process where processid='{}' call terminate".format(os.getpid()))
def _execute(args): def _execute(args):
arguments_length = len(args) arguments_length = len(args)
@ -178,32 +171,22 @@ def _execute(args):
options_list = args[2:] options_list = args[2:]
options = {} options = {}
selected_option = None selected_option = None
# lets try argparse in parallel
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--reload', default=False, action='store_true', help='reload the server on file changes, see --reload-ignore-patterns')
parser.add_argument('rem_args', nargs=argparse.REMAINDER) # Can move the other options here too
args = parser.parse_args()
for option in options_list: for option in options_list:
if selected_option: if selected_option:
options[selected_option] = option options[selected_option] = option
selected_option = None selected_option = None
elif option.startswith("--") or option.startswith("-"): elif option.startswith("--") or option.startswith("-"):
if selected_option is None: # ?? if selected_option is None:
selected_option = option # ?? selected_option = option # here, say i want to pass an arg to my app if you do --dev --reload you get "--dev": "--reload"')
else: # --factory, --reload etc else: # --factory, --reload etc
options[selected_option] = True options[selected_option] = True
else: else:
return print(f"Invalid option ${selected_option} see --help") return print(f"Invalid option ${selected_option} see --help")
if selected_option: # --factory, --reload etc if selected_option: # --factory, --reload etc
options[selected_option] = True options[selected_option] = True
print(options)
print('OPTIONS', flush=True)
print('BUG here, say i want to pass an arg to my app if you do --dev --reload you get "--dev": "--reload"')
print(options.get('--reload'))
print(args.reload)
print(args.rem_args)
interface = (options.get("--interface", "auto")).lower() interface = (options.get("--interface", "auto")).lower()
if interface == "auto": if interface == "auto":
@ -348,32 +331,24 @@ def _execute(args):
) )
# file watcher # file watcher
def launch_with_file_probe(run_method, user_module_function, loop, poll_frequency=20): def launch_with_file_probe(run_method, user_module_function, loop, poll_frequency=4):
import asyncio
import importlib.util import importlib.util
directory = os.path.dirname(importlib.util.find_spec(user_module_function.__module__).origin) directory = os.path.dirname(importlib.util.find_spec(user_module_function.__module__).origin)
directory_glob = os.path.join(directory, '**') directory_glob = os.path.join(directory, '**')
logging.info("Watching %s" % directory_glob)
print("Watching %s" % directory_glob, flush=True) print("Watching %s" % directory_glob, flush=True)
ignore_patterns = options.get("--reload-ignore-patterns", "node_modules,__pycache__,.git") ignore_patterns = options.get("--reload-ignore-patterns", "node_modules,__pycache__,.git")
ignore_patterns = ignore_patterns.split(',') ignore_patterns = ignore_patterns.split(',')
print(ignore_patterns) print("Ignoring Patterns %s" % ignore_patterns, flush=True)
# scandir utility functions
def _ignore(f): def _ignore(f):
for ignore_pattern in ignore_patterns: for ignore_pattern in ignore_patterns:
#if '__pycache__' in f or 'node_modules' in f: #if '__pycache__' in f or 'node_modules' in f:
if ignore_pattern in f: if ignore_pattern in f:
return True return True
# individual os.path.mtime after glob is slow, so try using scandir
def get_files():
new_files = {} # path, mtime
# [f.stat().st_mtime for f in list(os.scandir('.'))]
new_files = _get_dir(directory, new_files)
print(new_files)
return new_files
def _get_dir(path, new_files): def _get_dir(path, new_files):
print(path, flush=True)
for f_or_d in os.scandir(path): for f_or_d in os.scandir(path):
if _ignore(f_or_d.path): if _ignore(f_or_d.path):
continue continue
@ -386,204 +361,47 @@ def _execute(args):
new_files[f_path] = f_or_d.stat().st_mtime new_files[f_path] = f_or_d.stat().st_mtime
return new_files return new_files
def get_files_glob_version_slow():
new_files = {} # path, mtime
print(f"getfiles1... {datetime.now()}", flush=True) def get_files():
"""
for f in glob.glob(directory_glob, recursive=True): os.scandir caches the file stats, call it recursively
if _ignore(f): to emulate glob (which doesnt)
continue """
new_files[f] = os.path.getmtime(f) new_files = {} # store path, mtime
print(f"getfiles2... {datetime.now()}", flush=True) # [f.stat().st_mtime for f in list(os.scandir('.'))]
new_files = _get_dir(directory, new_files)
return new_files return new_files
def do_check(prev_files):
""" Get files and their modified time and compare with previous times.
def do_check(prev_files, thread): Restart the server if it has changed """
from datetime import datetime
print(f"Doing check... {datetime.now()}", flush=True)
new_files = get_files() new_files = get_files()
print(f"got new files... {datetime.now()}", flush=True) if len(new_files) > 50:
print(new_files) print(f"{len(new_files)} files being watched", new_files)
if prev_files is not None and new_files != prev_files: if prev_files is not None and new_files != prev_files:
# Call exit, the wrapper will restart the process """
print('Reload') Call exit on current process, socketify App run method has a signal handler that will stop
logging.info("Reloading files...") the uv/uwebsockets loop, then current process will exit and the cli execte() wrapper will then restart the process
reload_state.reload_pending = True #signal for Exeute to know whether it is a real SIGTERM or our own
print('running sigill')
import signal, sys
signal.raise_signal(signal.SIGTERM)
# os.kill(os.getpid(), RELOAD_SIGNAL) #doesnt work windows # call sigusr1 back on main thread which is caught by App.run()
#if sys.platform == 'win32':
# os.kill(os.getpid(),signal.SIGINT) # call sigint back on main thread which is caught by App.run()
"""print('running sysexit')
import sys
sys.exit(0)
print('ran sysexit')
""" """
print('Reloading...')
reload_state.reload_pending = True #signal for Exeute to know whether it is a real external SIGTERM or our own
import signal, sys
signal.raise_signal(signal.SIGTERM) # sigterm works on windows and posix
return new_files, thread return new_files
print('rnt 0')
def poll_check(): def poll_check():
thread = None
# poll_frequency = 1
files = None files = None
while True: while True:
#print('polling fs', flush=True)
import time
time.sleep(poll_frequency) time.sleep(poll_frequency)
files, thread = do_check(files, thread) files = do_check(files)
#await asyncio.wait_for(thread, poll_frequency)
thread = None
# thread = threading.Thread(target=run_method, kwargs={from_main_thread': False}, daemon=True)
thread = threading.Thread(target=poll_check, kwargs={}, daemon=True) thread = threading.Thread(target=poll_check, kwargs={}, daemon=True)
thread.start() thread.start()
run_method() run_method()
"""
async def launch_with_file_probe(run_method, user_module_function, loop, poll_frequency=0.5):
import asyncio
import importlib.util
directory = os.path.dirname(importlib.util.find_spec(user_module_function.__module__).origin)
logging.info("Watching %s" % directory)
def get_files():
new_files = {} # path, mtime
for f in glob.glob(directory):
if '__pycache__' in f:
continue
new_files[f] = os.path.getmtime(f)
return new_files
def run_new_thread(thread):
#try:
# loop = asyncio.get_running_loop()
#except Exception:
# loop = asyncio.new_event_loop()
#if loop and thread:
# loop.stop()
# await loop.shutdown_default_executor()
async def arun():
run_method(from_main_thread=False)
# new_task = loop.create_task(arun())
loop.call_later(delay=1, callback=check_loop, loop)
new_task = loop.run_once(arun())
print(type(new_task))
print(dir(new_task))
print('new thread')
return new_task
#new_thread = threading.Thread(target=run_method, args=[], kwargs={'from_main_thread': False}, daemon=True)
#new_thread.start()
#return new_thread
async def do_check(prev_files, thread):
new_files = get_files()
if prev_files is not None and new_files != prev_files:
thread.cancel()
try:
await thread
except asyncio.CancelledError:
pass
print('reload')
logging.info("Reloading files...")
thread = run_new_thread(thread)
return new_files, thread
import asyncio
print('rnt 0')
thread = run_new_thread(None)
files = None
poll_frequency = 0.5
while True:
import time
print('awaiting')
# time.sleep(poll_frequency)
done, pending = await asyncio.wait([thread], timeout=poll_frequency)
print(pending, done)
#await asyncio.sleep(poll_frequency)
files, thread = await do_check(files, thread)
#await asyncio.wait_for(thread, poll_frequency)
# file watcher
async def launch_with_file_probe(run_method, user_module_function, loop, poll_frequency=0.5):
import asyncio
import importlib.util
directory = os.path.dirname(importlib.util.find_spec(user_module_function.__module__).origin)
logging.info("Watching %s" % directory)
def get_files():
new_files = {} # path, mtime
for f in glob.glob(directory):
if '__pycache__' in f:
continue
new_files[f] = os.path.getmtime(f)
return new_files
def run_new_thread(thread):
#try:
# loop = asyncio.get_running_loop()
#except Exception:
# loop = asyncio.new_event_loop()
#if loop and thread:
# loop.stop()
# await loop.shutdown_default_executor()
async def arun():
run_method(from_main_thread=False)
# new_task = loop.create_task(arun())
loop.call_later(delay=1, callback=check_loop, loop)
new_task = loop.run_once(arun())
print(type(new_task))
print(dir(new_task))
print('new thread')
return new_task
#new_thread = threading.Thread(target=run_method, args=[], kwargs={'from_main_thread': False}, daemon=True)
#new_thread.start()
#return new_thread
async def do_check(prev_files, thread):
new_files = get_files()
if prev_files is not None and new_files != prev_files:
thread.cancel()
try:
await thread
except asyncio.CancelledError:
pass
print('reload')
logging.info("Reloading files...")
thread = run_new_thread(thread)
return new_files, thread
import asyncio
print('rnt 0')
thread = run_new_thread(None)
files = None
poll_frequency = 0.5
while True:
import time
print('awaiting')
# time.sleep(poll_frequency)
done, pending = await asyncio.wait([thread], timeout=poll_frequency)
print(pending, done)
#await asyncio.sleep(poll_frequency)
files, thread = await do_check(files, thread)
#await asyncio.wait_for(thread, poll_frequency)
"""
@ -611,8 +429,6 @@ def _execute(args):
if auto_reload: if auto_reload:
# there's watchfiles module but socketify currently has no external dependencies so # there's watchfiles module but socketify currently has no external dependencies so
# we'll roll our own for now... # we'll roll our own for now...
# from watchfiles import arun_process
logging.info(' LAUNCHING WITH RELOAD ')
print(' LAUNCHING WITH RELOAD ', flush=True) print(' LAUNCHING WITH RELOAD ', flush=True)
launch_with_file_probe(fork_app.run, module, fork_app.loop) launch_with_file_probe(fork_app.run, module, fork_app.loop)
else: # run normally else: # run normally