rework and use json for config
This commit is contained in:
parent
206032e797
commit
81d50cf827
|
@ -1,5 +1,5 @@
|
|||
__script__ = 'PyReloader'
|
||||
__version__ = (0, 3, 0)
|
||||
__version__ = (0, 4, 0)
|
||||
__year__ = 2021
|
||||
__author__ = 'Zoey Mae'
|
||||
__url__ = 'htts://git.barkshark.xyz/izaliamae/reload'
|
||||
|
@ -13,100 +13,77 @@ from pathlib import Path
|
|||
class DotDict(dict):
|
||||
def __init__(self, value=None, **kwargs):
|
||||
super().__init__()
|
||||
self.__setattr__ = self.__setitem__
|
||||
|
||||
if type(value) in [str, bytes]:
|
||||
self.fromJson(value)
|
||||
if isinstance(value, (str, bytes)):
|
||||
self.from_json(value)
|
||||
|
||||
elif type(value) in [dict, DotDict]:
|
||||
elif isinstance(value, dict):
|
||||
self.update(value)
|
||||
|
||||
elif value:
|
||||
raise TypeError('The value must be a JSON string, dict, or another DotDict object, not', value.__class__)
|
||||
raise TypeError(f'The value must be a JSON string, list, dict, or another DotDict object, not {value.__class__}')
|
||||
|
||||
if kwargs:
|
||||
self.update(kwargs)
|
||||
|
||||
|
||||
def __getattr__(self, key):
|
||||
def __getattr__(self, k):
|
||||
try:
|
||||
val = super().__getattribute__(key)
|
||||
return super().__getitem__(k)
|
||||
|
||||
except AttributeError:
|
||||
val = self.get(key, KeyError)
|
||||
|
||||
if val == KeyError:
|
||||
raise KeyError(f'Invalid key: {key}')
|
||||
|
||||
return DotDict(val) if type(val) == dict else val
|
||||
except KeyError:
|
||||
raise AttributeError(f'{self.__class__.__name__} object has no attribute {k}') from None
|
||||
|
||||
|
||||
def __delattr__(self, key):
|
||||
if self.get(key):
|
||||
del self[key]
|
||||
|
||||
super().__delattr__(key)
|
||||
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key.startswith('_'):
|
||||
super().__setattr__(key, value)
|
||||
|
||||
else:
|
||||
super().__setitem__(key, value)
|
||||
|
||||
|
||||
def __parse_item__(self, k, v):
|
||||
if type(v) == dict:
|
||||
def __setitem__(self, k, v):
|
||||
if isinstance(v, dict):
|
||||
v = DotDict(v)
|
||||
|
||||
if not k.startswith('_'):
|
||||
return (k, v)
|
||||
super().__setitem__(k, v)
|
||||
|
||||
|
||||
def get(self, key, default=None):
|
||||
value = super().get(key, default)
|
||||
return DotDict(value) if type(value) == dict else value
|
||||
def __delattr__(self, k):
|
||||
try:
|
||||
dict.__delitem__(self, k)
|
||||
except KeyError:
|
||||
raise AttributeError(f'{self.__class__.__name__} object has no attribute {k}') from None
|
||||
|
||||
|
||||
def items(self):
|
||||
data = []
|
||||
|
||||
for k, v in super().items():
|
||||
new = self.__parse_item__(k, v)
|
||||
|
||||
if new:
|
||||
data.append(new)
|
||||
|
||||
return data
|
||||
def copy(self):
|
||||
return DotDict(self)
|
||||
|
||||
|
||||
def values(self):
|
||||
return list(super().values())
|
||||
def update(self, data):
|
||||
for k,v in data.items():
|
||||
self.__setitem__(k, v)
|
||||
|
||||
|
||||
def keys(self):
|
||||
return list(super().keys())
|
||||
def to_json(self, indent=None, **kwargs):
|
||||
if 'cls' not in kwargs:
|
||||
kwargs['cls'] = JsonEncoder
|
||||
|
||||
return json.dumps(dict(self), indent=indent, **kwargs)
|
||||
|
||||
|
||||
def asDict(self):
|
||||
return dict(self)
|
||||
|
||||
|
||||
def toJson(self, indent=None, **kwargs):
|
||||
data = {}
|
||||
|
||||
for k,v in self.items():
|
||||
if type(k) in [DotDict, Path]:
|
||||
k = str(k)
|
||||
|
||||
if type(v) in [DotDict, Path]:
|
||||
v = str(v)
|
||||
|
||||
data[k] = v
|
||||
|
||||
return json.dumps(data, indent=indent, **kwargs)
|
||||
|
||||
|
||||
def fromJson(self, string):
|
||||
def from_json(self, string):
|
||||
data = json.loads(string)
|
||||
self.update(data)
|
||||
|
||||
|
||||
def load_json(self, path):
|
||||
with open(path) as fd:
|
||||
self.update(json.load(fd))
|
||||
|
||||
|
||||
def save_json(self, path, indent=None):
|
||||
with open(path, 'w') as fp:
|
||||
fp.write(self.to_json(indent=indent))
|
||||
|
||||
|
||||
class JsonEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if not any(map(isinstance, [obj], [str, int, float, dict])):
|
||||
return str(obj)
|
||||
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import os, shlex, sys, logging as logger
|
||||
import json, os, shlex, sys, logging as logger
|
||||
|
||||
from configobj import ConfigObj
|
||||
from pathlib import Path
|
||||
|
||||
from . import DotDict
|
||||
|
||||
args = ' '.join(sys.argv[1:])
|
||||
|
||||
defaults = {
|
||||
'exec': 'insert command here' if not args else args,
|
||||
'path': os.getcwd(),
|
||||
'bin': None,
|
||||
'args': [],
|
||||
'env': {},
|
||||
'path': Path.cwd(),
|
||||
'watch_ext': ['py', 'env'],
|
||||
'ignore_dirs': ['build', 'config', 'data'],
|
||||
'ignore_files': ['reload.py', 'test.py'],
|
||||
|
@ -16,73 +17,47 @@ defaults = {
|
|||
}
|
||||
|
||||
|
||||
configpath = Path.cwd().joinpath('reload.cfg')
|
||||
configfile = ConfigObj(indent_type='\t')
|
||||
configfile.filename = str(configpath)
|
||||
def setup_config(configfile=None):
|
||||
configfile = Path(configfile) if configfile else Path.cwd().joinpath('reload.json')
|
||||
config = DotDict(**defaults)
|
||||
|
||||
if not configpath.exists():
|
||||
msg = 'Creating config file and exiting...' if not args else 'Creating config file'
|
||||
print('Cannot find "reload.cfg" in working directory. ' + msg)
|
||||
if not configfile.exists():
|
||||
print('Cannot find "reload.json" in working directory. Creating config file and exiting...')
|
||||
|
||||
for setting in defaults:
|
||||
configfile[setting] = defaults[setting]
|
||||
new_config = defaults.copy()
|
||||
new_config['path'] = str(config.path)
|
||||
|
||||
try:
|
||||
configfile.write()
|
||||
except PermissionError:
|
||||
print('Failed to write config file because of permission error:', configpath)
|
||||
try:
|
||||
config.save_json(configfile, indent=4)
|
||||
|
||||
except PermissionError:
|
||||
print('Failed to write config file because of permission error')
|
||||
|
||||
if not args:
|
||||
sys.exit()
|
||||
|
||||
configfile.reload()
|
||||
config = DotDict()
|
||||
config.load_json(configfile)
|
||||
config.path = Path(config.path or os.getcwd())
|
||||
config.log_level = getattr(logger, config.log_level.upper())
|
||||
config.ignore_dirs = [s.strip('/') for s in config.ignore_dirs]
|
||||
config.bin = Path(config.get('bin')).expanduser()
|
||||
|
||||
if not config.bin:
|
||||
print('You forgot to specify a command')
|
||||
sys.exit()
|
||||
|
||||
for k,v in defaults.items():
|
||||
value = configfile.get(k)
|
||||
key = k
|
||||
if not config.path.exists():
|
||||
print('Watch path doesn\'t exist:', config.path)
|
||||
sys.exit()
|
||||
|
||||
if value == None:
|
||||
if key == 'exec':
|
||||
print('You forgot to specify a command')
|
||||
sys.exit()
|
||||
console.level = config.log_level
|
||||
|
||||
if key == 'watch_ext':
|
||||
print('You forgot to specify a file extension to watch')
|
||||
sys.exit()
|
||||
|
||||
if key == 'path':
|
||||
value = os.getcwd()
|
||||
|
||||
if key == 'exec':
|
||||
value = shlex.split(value if not args else args)
|
||||
|
||||
elif k == 'path':
|
||||
value = Path(value).resolve()
|
||||
|
||||
elif type(v) != list and key != 'log_level':
|
||||
value = [s.strip() for s in value.split(',')]
|
||||
|
||||
elif k == 'ignore_dirs':
|
||||
value = [s.strip('/') for s in value]
|
||||
|
||||
config[key] = value
|
||||
|
||||
|
||||
if not config.path.exists():
|
||||
print('Watch path doesn\'t exist:', config.path)
|
||||
sys.exit()
|
||||
|
||||
|
||||
#if len(sys.argv) > 1:
|
||||
#command += sys.argv[1:]
|
||||
return config
|
||||
|
||||
|
||||
logging = logger.getLogger('reloader')
|
||||
logging.setLevel(logger.DEBUG)
|
||||
console = logger.StreamHandler()
|
||||
console.name = 'Console Log'
|
||||
console.level = eval(f'logger.{config.log_level}')
|
||||
console.level = logger.INFO
|
||||
console.formatter = logger.Formatter('%(asctime)s WATCHER: %(message)s', '%H:%M:%S')
|
||||
logging.addHandler(console)
|
||||
|
|
142
reload/script.py
142
reload/script.py
|
@ -3,107 +3,115 @@ import os, random, subprocess, sys, time
|
|||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
from .config import config, logging
|
||||
from .config import setup_config, logging
|
||||
|
||||
|
||||
proc = None
|
||||
last_restart = None
|
||||
config = setup_config()
|
||||
|
||||
|
||||
def not_in(var, string):
|
||||
return string not in var
|
||||
class WatchHandler(PatternMatchingEventHandler):
|
||||
patterns = [f'*.{ext}' for ext in config.watch_ext]
|
||||
|
||||
|
||||
def killProc(proc):
|
||||
if proc.poll() != None:
|
||||
return
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
logging.info(f'Terminating process {proc.pid}')
|
||||
proc.terminate()
|
||||
sec = 0
|
||||
|
||||
while proc.poll() == None:
|
||||
time.sleep(1)
|
||||
sec += 1
|
||||
|
||||
if sec >= 5:
|
||||
logging.error('Failed to terminate. Killing process...')
|
||||
proc.kill()
|
||||
break
|
||||
|
||||
logging.info('Process terminated')
|
||||
self.proc = None
|
||||
self.last_restart = None
|
||||
|
||||
|
||||
def run(restart=False):
|
||||
global proc
|
||||
global last_restart
|
||||
|
||||
timestamp = datetime.timestamp(datetime.now())
|
||||
last_restart = timestamp if not last_restart else last_restart
|
||||
|
||||
if restart == True and proc.pid != '':
|
||||
if timestamp - 1 < last_restart:
|
||||
logging.debug('Process restarted less than 1 sec ago')
|
||||
def kill_proc(self):
|
||||
if self.proc.poll() != None:
|
||||
return
|
||||
|
||||
killProc(proc)
|
||||
logging.info(f'Terminating process {self.proc.pid}')
|
||||
self.proc.terminate()
|
||||
sec = 0
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(config.exec, preexec_fn=os.setpgrp)
|
||||
except FileNotFoundError:
|
||||
logging.error(f'Cannot find executable:', config.exec.join(' '))
|
||||
sys.exit()
|
||||
while self.proc.poll() == None:
|
||||
time.sleep(1)
|
||||
sec += 1
|
||||
|
||||
last_restart = timestamp
|
||||
logging.info(f'Started process with PID {proc.pid}')
|
||||
if sec >= 5:
|
||||
logging.error('Failed to terminate. Killing process...')
|
||||
self.proc.kill()
|
||||
break
|
||||
|
||||
logging.info('Process terminated')
|
||||
|
||||
|
||||
def run_proc(self, restart=False):
|
||||
timestamp = datetime.timestamp(datetime.now())
|
||||
self.last_restart = timestamp if not self.last_restart else 0
|
||||
|
||||
if restart == True and self.proc.pid != '':
|
||||
if timestamp - 3 < self.last_restart:
|
||||
logging.debug('Process restarted less than 1 sec ago')
|
||||
return
|
||||
|
||||
self.kill_proc()
|
||||
|
||||
cmd = [config.bin]
|
||||
|
||||
if config.env:
|
||||
env = ['env']
|
||||
|
||||
for k,v in config.env.items():
|
||||
env.append(f'{k}={v}')
|
||||
|
||||
cmd = [*env, *cmd]
|
||||
|
||||
try:
|
||||
self.proc = subprocess.Popen([*cmd, *config.args], preexec_fn=os.setpgrp, stdin=subprocess.PIPE)
|
||||
except FileNotFoundError:
|
||||
logging.error(f'Cannot find executable: {config.bin}')
|
||||
os.exit()
|
||||
|
||||
self.last_restart = timestamp
|
||||
logging.info(f'Started process with PID {self.proc.pid}')
|
||||
|
||||
|
||||
class MyHandler(FileSystemEventHandler):
|
||||
def on_any_event(self, event):
|
||||
if event.event_type not in ['modified', 'created', 'deleted']:
|
||||
logging.debug(f'Ignored event: {event.event_type}')
|
||||
return
|
||||
|
||||
path = Path(event.src_path)
|
||||
name = path.name
|
||||
directory = str(path.parent).replace(str(config.path), '')
|
||||
ext = path.suffix.strip('.')
|
||||
|
||||
if ext or any(map(not_in, [directory, name], ['__pycache__'])):
|
||||
logging.debug(f'{event.event_type}, {directory}, {name}, {ext}')
|
||||
|
||||
if event.event_type in ['modified', 'created'] and ext in config.watch_ext:
|
||||
for direc in config.ignore_dirs:
|
||||
if f'/{direc}/' in directory:
|
||||
logging.debug(f'Ignored directory: {directory}')
|
||||
return
|
||||
|
||||
if not set(config.ignore_dirs).intersection(directory.split('/')):
|
||||
for filename in config.ignore_files:
|
||||
if name == filename:
|
||||
if path.name == filename:
|
||||
logging.debug(f'Ignored file name: {filename}')
|
||||
return
|
||||
|
||||
run(restart=True)
|
||||
self.run_proc(restart=True)
|
||||
|
||||
|
||||
def main():
|
||||
logging.info('Starting process watcher')
|
||||
logging.debug(f'Watch dir: {config.path}')
|
||||
|
||||
run()
|
||||
handler = WatchHandler()
|
||||
handler.run_proc()
|
||||
|
||||
observer = Observer()
|
||||
observer.schedule(MyHandler(), str(config.path), recursive=True)
|
||||
observer.start()
|
||||
watcher = Observer()
|
||||
watcher.schedule(handler, str(config.path), recursive=True)
|
||||
watcher.start()
|
||||
|
||||
timeout = None
|
||||
timer = 0
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(random.randint(60 * 30, 60 * 60))
|
||||
logging.info('*notices your running process* OwO what\'s this?')
|
||||
for line in sys.stdin:
|
||||
proc.stdin.write(line.encode('UTF-8'))
|
||||
proc.stdin.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
killProc(proc)
|
||||
print()
|
||||
handler.kill_proc()
|
||||
logging.debug('Stopping process watcher')
|
||||
observer.stop()
|
||||
observer.join()
|
||||
watcher.stop()
|
||||
watcher.join()
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
configobj>=5.0.6
|
||||
watchdog>=0.8.3
|
||||
watchdog>=2.1.3
|
||||
|
|
Loading…
Reference in a new issue