From 78d3b40c03448e4b68fe22fe3527aae9da25a031 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Tue, 25 Mar 2025 21:51:05 +0100 Subject: [PATCH] Set configuration options for processes and save to toml --- foucault/__init__.py | 0 foucault/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 196 bytes foucault/__pycache__/gui.cpython-313.pyc | Bin 0 -> 24130 bytes foucault/gui.py | 331 ++++++++++++++++++ gui.py | 133 ------- main.py | 22 ++ processes/tail.toml | 29 ++ pyproject.toml | 1 + uv.lock | 15 +- 9 files changed, 397 insertions(+), 134 deletions(-) create mode 100644 foucault/__init__.py create mode 100644 foucault/__pycache__/__init__.cpython-313.pyc create mode 100644 foucault/__pycache__/gui.cpython-313.pyc create mode 100644 foucault/gui.py delete mode 100644 gui.py create mode 100644 main.py create mode 100644 processes/tail.toml diff --git a/foucault/__init__.py b/foucault/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/foucault/__pycache__/__init__.cpython-313.pyc b/foucault/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8513b21b1b824857c80a217dea4d1c9b880b85a6 GIT binary patch literal 196 zcmey&%ge<81koRErGx0lAOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkqYY{fzwFRQ;mT zq|`ipm;B_?+|<01V*P-k{H)aE5}=rofsuiJaB6aCQD#Y{LRe;TW`3T2VqSi3Vos%Q zN@@vEReq6vT7GGAVrfnZf)O8|nU`4-AFo$Xd5gm)H$SB`C)KWq6=)~OO~oL_M`lJw J#v*1Q3jjQnHLd^v literal 0 HcmV?d00001 diff --git a/foucault/__pycache__/gui.cpython-313.pyc b/foucault/__pycache__/gui.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51e83fa1f159b9c572a860110d58a188885cf65b GIT binary patch literal 24130 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 QqIc5toAg79JV;0X9~+9JZvX%Q literal 0 HcmV?d00001 diff --git a/foucault/gui.py b/foucault/gui.py new file mode 100644 index 0000000..c890b3b --- /dev/null +++ b/foucault/gui.py @@ -0,0 +1,331 @@ +""" +Foucault allows you to govern a set of commands, which can be useful for +i.e. an art installation setting, in which different scripts need to work +together. While one can be (re)started independently of the rest. +""" + +import asyncio +from collections import defaultdict +import os +import pathlib +import random +from subprocess import Popen +import subprocess +from tempfile import mkstemp +import threading +import time +import tomllib +from typing import Optional +from nicegui import ui +import logging + +import tomli_w + +logger = logging.getLogger('procescontroller') + + +numbers = [] +processes = [] + +class Argument(): + def __init__(self, name: str, enabled: bool = True, inline: bool = False, boolean: bool = False, options: list[str] = [], selected: list[str] = []): + self.name = name + self.enabled = enabled + self.inline = inline + self.boolean = boolean + self.options = options + self.selected = selected + + + def as_list(self) -> list[str]: + if not self.enabled: + return [] + l = [] + if not self.inline: + l.append(self.name) + if not self.boolean: + l += self.selected + return l + + def select(self, option: str): + self.selected = [option] + + def toggle(self): + self.enabled = not self.enabled + + @classmethod + def from_dict(cls, data: dict): + return cls( + data['name'], + data.get('enabled', False), + data.get('inline', False), + data.get('boolean', False), + data.get('options', []), + data.get('selected', []), + ) + + def to_dict(self) -> dict: + return self.__dict__ + + + +class SubprocessController(): + def __init__(self, name: str, cmd: list[str], arguments: list[Argument], environment: dict[str,str] = {}, fn: Optional[pathlib.Path] = None): + self.cmd = cmd + self.arguments = arguments + self.name = name + self.filename = fn + # self.tmp_config_fp, self.tmp_config_filename = mkstemp(suffix=".yml", prefix=f"config_{self.name}_") + self.env_overrides = environment + # dict[str, str] = { + # "DISPLAY": ":1" + # } + self.proc=None + self.running_config_state: Optional[str] = None + self.saved_state = self.config_state() + + @classmethod + def from_toml(cls, filename: os.PathLike): + path = pathlib.Path(filename) + name = path.stem + args = [] + with path.open('rb') as fp: + data = tomllib.load(fp) + print(data) + + for arg in data['arguments']: + args.append(Argument.from_dict(arg)) + + sc = cls( + name, + [data['cmd']] if data['cmd'] is str else data['cmd'], + args, + data.get('environment', {}), + path + ) + return sc + + def to_dict(self): + return { + "cmd": self.cmd, + "arguments": [a.to_dict() for a in self.arguments], + "environment": self.env_overrides, + } + + + def update_config(self): + pass + + def get_environment(self): + default_env = os.environ.copy() + return default_env | self.env_overrides + + def get_environment_strs(self): + return [f"{k}=\"{v}\"" for k, v in self.env_overrides.items()] + + def rm_env(self, key): + if key in self.env_overrides: + del self.env_overrides[key] + + def add_env(self, key, value): + self.env_overrides[key] = value + + + + def get_arguments(self)-> list[str]: + r = [] + for a in self.arguments: + r.extend(a.as_list()) + return r + + + def as_bash_string(self): + return " " . join(self.get_environment_strs() + self.cmd + self.get_arguments()) + + + def run(self): + logger.info(f"Run: {self.as_bash_string()}") + self.running_config_state = self.as_bash_string() + self.proc = Popen( + self.cmd + self.get_arguments(), + env=self.get_environment(), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + def is_stale(self): + """Config has changed since starting the process""" + return self.is_running() and self.running_config_state != self.as_bash_string() + + + def quit(self): + if self.is_running(): + self.proc.terminate() + + def restart(self): + self.quit() + self.run() + + def return_code(self) -> Optional[int]: + if self.proc is None: + return None + return self.proc.returncode + + def is_running(self): + return self.proc is not None and self.proc.poll() is None + + def state(self): + return "::".join( + [self.name, str(self.is_running()), str(self.return_code())] + ) + "::" + self.config_state() + (self.running_config_state if self.running_config_state else "") + + def config_state(self): + return "::".join( + self.cmd + self.get_arguments() + ["+".join(a.options) for a in self.arguments] + [str(self.filename.stat().st_mtime) if self.filename else ""] + + list(self.env_overrides.values()) + ) + + def save(self) -> str: + # print(self.to_dict()) + with self.filename.open('wb') as fp: + tomli_w.dump(self.to_dict(), fp) + self.saved_state = self.config_state() + + def unsaved_changes(self): + return self.config_state() != self.saved_state + + +class Foucault(): + def __init__(self): + self.processes: list[SubprocessController] = [] + self.uis: list[SubprocessUI] = [] + + def add_process(self, sc: SubprocessController): + self.processes.append(sc) + self.uis.append(SubprocessUI(sc)) + + def run_all(self): + for p in self.processes: + p.run() + + def stop_all(self): + for p in self.processes: + p.quit() + + def restart_all(self): + # when restarting all, make sure all are stopped + # before starting, so we start with a clean slate + self.stop_all() + self.run_all() + + def ui(self): + with ui.row(): + ui.button('▶', on_click=self.run_all) + ui.button('■', on_click=self.stop_all) + ui.button('↺', on_click=self.restart_all) + + with ui.grid(columns="40em 40em 40em"): + for proc_ui in self.uis: + proc_ui.ui() + + def run(self): + + # t = threading.Thread(target=reloader, daemon=True) + t = threading.Thread(target=self.monitor, daemon=True) + t.start() + + #build ui: + self.ui() + ui.page_title("Conduct of conduct") + ui.run(show=False, favicon="🫣") + + + def monitor(self): + + states = defaultdict(lambda: "") + + i=0 + while True: + i+=1 + # if we're only interested in running, we can use os.waitpid(), but we also check config changes + time.sleep(.3) + for proc_ui in self.uis: + state = proc_ui.sc.state() + if state != states[proc_ui.sc]: + proc_ui.ui.refresh() + states[proc_ui.sc] = state + + + +class SubprocessUI: + def __init__(self, sc: SubprocessController): + self.sc = sc + + @ui.refreshable_method + def ui(self): + card_class = "bg-teal" if self.sc.is_running() else ("bg-warning" if self.sc.return_code() else "bg-gray") + with ui.card().classes(card_class): + with ui.row(align_items="stretch").classes('w-full'): + if self.sc.return_code(): + ui.label(f'⚠').classes('text-h5') + + ui.label(f'{self.sc.name}').tooltip(str(self.sc.filename)).classes('text-h5') + ui.space() + with ui.button_group(): + ui.button('▶', on_click=self.sc.run) + ui.button('■', on_click=self.sc.quit) + ui.button('↺', on_click=self.sc.restart, color='red' if self.sc.is_stale() else "primary") + + + ui.label(f'Running: {self.sc.is_running()}') + ui.code(f'{" ".join(self.sc.cmd)}') + ui.code(self.sc.as_bash_string()) + ui.separator() + with ui.row().classes('w-full'): + edit = ui.switch(value=self.sc.unsaved_changes()) + ui.space() + ui.button('🖫', on_click=self.sc.save, color="red" if self.sc.unsaved_changes() else 'lightgray').classes('text-lg') + with ui.grid().bind_visibility_from(edit, 'value'): + ui.label('Arguments').classes('text-h6') + for i, argument in enumerate(self.sc.arguments): + with ui.card().classes('w-full'): + with ui.row(align_items='center').classes('w-full'): + ui.button("⊗", on_click=lambda i=i: self.sc.arguments.pop(i)) + ui.label(f'{argument.name}').classes('text-stone-400' if argument.inline else '') + ui.space() + ui.switch(value=argument.enabled, on_change=lambda i=argument: i.toggle()) + if not argument.boolean: + with ui.dropdown_button(f"{len(argument.options)} | " + ' '.join(argument.selected), auto_close=False).classes('normal-case'): + # ui.badge('0', color='red').props('floating') + for o in argument.options: + # TODO)) Remove + ui.item(o +'i', on_click=(lambda a=argument, o=o: a.select(o) )) + with ui.item(): + ui.input(placeholder="add new").on('keydown.enter', (lambda e, a=argument: a.options.append(e.sender.value) or print(a.options))) + # ui.label(f'{argument.name}') + # with ui.row(): + with ui.card(): + with ui.row().classes('w-full'): + name = ui.input(placeholder="argument").classes('w-full')#.on("keydown.enter", ) + with ui.row(): + enabled = ui.switch('enabled', value=True) + positional = ui.checkbox('positional') #inline + boolean = ui.checkbox('boolean') + ui.button("+", on_click=lambda e, name=name, enabled=enabled, positional=positional, boolean=boolean: self.sc.arguments.append(Argument( + name.value, + enabled.value, + positional.value, + boolean.value + ))) + ui.label('Environment variables').classes('text-h6') + with ui.card(): + for k, v in self.sc.env_overrides.items(): + with ui.row(align_items="center"): + ui.button("⊗", on_click=lambda k=k: self.sc.rm_env(k)) + ui.label(f"{k}=\"{v}\"") + + with ui.row(): + name = ui.input(placeholder="name")#.on("keydown.enter", ) + value = ui.input(placeholder="value")#.on("keydown.enter", ) + ui.button("+", on_click=lambda e, name=name, enabled=value: self.sc.add_env(name.value, value.value)) + + diff --git a/gui.py b/gui.py deleted file mode 100644 index 0bd3c99..0000000 --- a/gui.py +++ /dev/null @@ -1,133 +0,0 @@ -import asyncio -import os -import pathlib -import random -from subprocess import Popen -from tempfile import mkstemp -import threading -import time -from nicegui import ui - -numbers = [] -processes = [] - -class SubprocessController(): - def __init__(self, name: str, cmd): - self.cmd = cmd - self.name = name - # self.tmp_config_fp, self.tmp_config_filename = mkstemp(suffix=".yml", prefix=f"config_{self.name}_") - self.env_overrides: dict[str, str] = { - "DISPLAY": ":1" - } - self.proc=None - pass - - def update_config(self): - pass - - def get_environment(self): - default_env = os.environ.copy() - return default_env | self.env_overrides - - def run(self): - self.proc = Popen( - self.cmd, - env=self.get_environment(), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE) - - - def quit(self): - if self.is_running(): - self.proc.terminate() - - - def is_running(self): - return self.proc is not None and self.proc.poll() is None - - -# class SubprocessUi(): -# def __init__(self, controller: SubprocessController): -# self.controller = controller - -# @ui.refreshable_method -# def card(self): -# with ui.card(): -# ui.label(self.controller.name) -# ui.label(str(self.controller.is_running())) - - - -class Foucault(): - def __init__(self): - self.processes: list[SubprocessController] = [] - self.uis: list[SubprocessUI] = [] - - def add_process(self, sc: SubprocessController): - self.processes.append(sc) - self.uis.append(SubprocessUI(sc)) - - def ui(self): - with ui.row(): - for proc_ui in self.uis: - proc_ui.ui() - - def run(self): - - i=0 - while True: - i+=1 - time.sleep(1) - for proc_ui in self.uis: - proc_ui.ui.refresh() - # if i >10: - # print('cancelling') - # sc.quit() - - -conductofconduct = Foucault() - -@ui.refreshable -def number_ui() -> None: - print(conductofconduct.processes) - ui.label("test: " + ','.join([str(process.is_running()) for process in conductofconduct.processes])) - # ui.label(', '.join(str(n) for n in sorted(conductofconduct.processes))) - -def add_number() -> None: - numbers.append(random.randint(0, 100)) - number_ui.refresh() - -def reloader(): - """Test reloading""" - for i in range(200): - print("add") - numbers.append(i) - time.sleep(1) - number_ui.refresh() - -class SubprocessUI: - def __init__(self, sc: SubprocessController): - self.sc = sc - - @ui.refreshable_method - def ui(self): - with ui.card(): - ui.label(f'{self.sc.name}') - ui.label(f'Running: {self.sc.is_running()}') - ui.button('Run', on_click=self.sc.run) - ui.button('Stop', on_click=self.sc.quit) - - -conductofconduct.add_process(SubprocessController("test1" , ["tail", '-f', "gui.py"])) -conductofconduct.add_process(SubprocessController("test2" , ["tail", '-f', "gui.py"])) - -conductofconduct.ui() - -# t = threading.Thread(target=reloader, daemon=True) -t = threading.Thread(target=conductofconduct.run, daemon=True) -t.start() - -# number_ui() -# ui.button('Add random number', on_click=add_number) - -ui.run(show=False) diff --git a/main.py b/main.py new file mode 100644 index 0000000..2e24fb4 --- /dev/null +++ b/main.py @@ -0,0 +1,22 @@ +from pathlib import Path +from foucault.gui import * + + +# print(sc) + +logging.basicConfig(level=logging.INFO) + +conductofconduct = Foucault() +sc = SubprocessController.from_toml(Path('processes/tail.toml')) +sc2 = SubprocessController.from_toml(Path('processes/tail2.toml')) +sc3 = SubprocessController.from_toml(Path('processes/tail.toml')) +conductofconduct.add_process(sc) +conductofconduct.add_process(sc2) +conductofconduct.add_process(sc3) +# conductofconduct.add_process(SubprocessController("test1" , ["tail"], ['-f', "gui.py"])) +# conductofconduct.add_process(SubprocessController("test2" , ["tail"], ['-f', "gui.py"])) +# conductofconduct.add_process(SubprocessController("test3 broken" , ["tail"], ['-f', "nonexistent.py"])) +# conductofconduct.add_process(SubprocessController("system status" , ["uv run status.py"], ['-f', "nonexistent.py"])) + +conductofconduct.run() + diff --git a/processes/tail.toml b/processes/tail.toml new file mode 100644 index 0000000..3bd5609 --- /dev/null +++ b/processes/tail.toml @@ -0,0 +1,29 @@ +cmd = [ + "tail", +] + +[[arguments]] +name = "-f" +enabled = true +inline = false +boolean = true +options = [] +selected = [] + +[[arguments]] +name = "name" +enabled = true +inline = true +boolean = false +options = [ + "gui.py", + "nonexistent.py", + "test", + "meer", +] +selected = [ + "gui.py", +] + +[environment] +":DISPLAY" = "1" diff --git a/pyproject.toml b/pyproject.toml index 2c6f2f7..9d8c178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,5 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "nicegui>=2.12.1", + "tomli-w>=1.2.0", ] diff --git a/uv.lock b/uv.lock index c5cbadd..57d3c96 100644 --- a/uv.lock +++ b/uv.lock @@ -210,10 +210,14 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "nicegui" }, + { name = "tomli-w" }, ] [package.metadata] -requires-dist = [{ name = "nicegui", specifier = ">=2.12.1" }] +requires-dist = [ + { name = "nicegui", specifier = ">=2.12.1" }, + { name = "tomli-w", specifier = ">=1.2.0" }, +] [[package]] name = "h11" @@ -646,6 +650,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675 }, +] + [[package]] name = "typing-extensions" version = "4.12.2"