From c568d8d9a742a8e37935f4764c51fd0f92be10e4 Mon Sep 17 00:00:00 2001 From: Siarhei Siniak Date: Wed, 3 Dec 2025 17:38:12 +0300 Subject: [PATCH] [+] update oom_firefox 1. fix .pid attributes for dict; 2. reformat; 3. migrate into pr34 repo; 4. update Makefile for fetch, deploy; --- Makefile | 2 +- .../home/nartes/.local/bin/oom_firefox | 441 ----------------- python/meson.build | 2 +- python/online/fxreader/pr34/oom_firefox.py | 468 ++++++++++++++++++ python/pyproject.toml | 1 + ...ne_fxreader_pr34-0.1.5.42-py3-none-any.whl | 3 + 6 files changed, 474 insertions(+), 443 deletions(-) delete mode 100755 platform_dotfiles/ideapad_slim_3_15arp10/home/nartes/.local/bin/oom_firefox create mode 100644 python/online/fxreader/pr34/oom_firefox.py create mode 100644 releases/whl/online_fxreader_pr34-0.1.5.42-py3-none-any.whl diff --git a/Makefile b/Makefile index b1fbf02..386da80 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ python_put_pr34: -U \ online.fxreader.pr34 ln -sf $(INSTALL_ROOT)/env3/bin/online-fxreader-pr34-commands $(INSTALL_ROOT)/commands + ln -sf $(INSTALL_ROOT)/env3/bin/oom_firefox $(INSTALL_ROOT)/oom_firefox PYTHON_PROJECTS_NAMES ?= online.fxreader.pr34 @@ -122,7 +123,6 @@ dotfiles_fetch_platform: mkdir -p platform_dotfiles/$(PLATFORM) tar -cvf - \ /etc/udev/rules.d/ \ - ~/.local/bin/oom_firefox \ ~/.local/bin/systemd_gtk \ ~/.local/bin/gnome-shortcuts-macbook-air \ /usr/local/bin \ diff --git a/platform_dotfiles/ideapad_slim_3_15arp10/home/nartes/.local/bin/oom_firefox b/platform_dotfiles/ideapad_slim_3_15arp10/home/nartes/.local/bin/oom_firefox deleted file mode 100755 index 864538e..0000000 --- a/platform_dotfiles/ideapad_slim_3_15arp10/home/nartes/.local/bin/oom_firefox +++ /dev/null @@ -1,441 +0,0 @@ -#!/usr/bin/env python3 -import psutil -import logging -import os -import signal -import subprocess -import threading -import time -import argparse -import re -import sys - -from prompt_toolkit.application import Application -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.layout import Layout, HSplit, FloatContainer, Float -from prompt_toolkit.layout.containers import Window -from prompt_toolkit.layout.controls import FormattedTextControl -from prompt_toolkit.widgets import TextArea, Frame, Dialog, Button, Label -from prompt_toolkit.styles import Style - -from typing import (TypedDict,) -from collections import OrderedDict - -logger = logging.getLogger(__name__) - -__version__ = "0.6.1" -__created__ = "2025-11-21" - -# — Helper for cgroup / slice matching — - -def get_cgroup_path(pid): - try: - with open(f"/proc/{pid}/cgroup", "r") as f: - for line in f: - parts = line.strip().split(":", 2) - if len(parts) == 3: - return parts[2] - except Exception: - logger.exception('') - return None - return None - -def slice_matches(cpath, target_slice): - if not cpath or not target_slice: - return False - comps = cpath.strip("/").split("/") - tgt = target_slice.lower() - for comp in comps: - name = comp.lower() - if name.endswith(".slice"): - name = name[:-6] - if tgt == name: - return True - return False - -# — Memory‑management logic — - -class get_firefox_procs_ps_t: - class res_t: - class entry_t(TypedDict): - rss: int - pid: int - ppid: int - cgroup: str - cmd: str - -def get_firefox_procs_ps(slice_name=None) -> list[get_firefox_procs_ps_t.res_t.entry_t]: - entries : dict[int, dict[str, Any]] = dict() - - for regex, columns in [ - ( - re.compile(r'^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$'), - OrderedDict( - pid=lambda x: int(x[1]), - rss=lambda x: int(x[2]) * 1024, - ppid=lambda x: int(x[3]), - cmd=lambda x: x[4], - ), - ), - ( - re.compile(r'^\s*(\d+)\s+(.*)$'), - OrderedDict( - pid=lambda x: int(x[1]), - cgroup=lambda x: x[2], - ), - ), - ]: - lines = subprocess.check_output([ - 'ps', '-ax', '-o', ','.join(columns.keys()), - ]).decode('utf-8').splitlines()[1:] - - for line in lines: - r = re.compile(regex) - # print([r, line]) - match = r.match(line) - - assert match - - entry = { - k : v(match) - for k, v in columns.items() - } - - if not entry['pid'] in entries: - entries[entry['pid']] = dict() - - entries[entry['pid']].update(entry) - - filtered_entries : list[dict[str, Any]] = [] - - for entry in entries.values(): - if not 'cgroup' in entry or not 'rss' in entry: - continue - - if not slice_name is None: - if not slice_name in entry['cgroup']: - continue - filtered_entries.append(entry) - - return filtered_entries - -def get_firefox_procs(slice_name=None): - procs = [] - for p in psutil.process_iter(['pid', 'name', 'cmdline', 'memory_info']): - try: - name = p.info['name'] - cmd = p.info['cmdline'] - if not cmd: - continue - if 'firefox' not in name and not (cmd and 'firefox' in cmd[0]): - continue - if slice_name: - cpath = get_cgroup_path(p.pid) - if not slice_matches(cpath, slice_name): - continue - procs.append(p) - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - return procs - -def total_rss_mb(procs: list['get_firefox_procs_ps_t.res_t.entry_t']): - total = 0 - for p in procs: - try: - # total += p.memory_info().rss - total += p['rss'] - except Exception: - logger.exception('') - pass - return total / (1024 * 1024) - -def is_main_firefox(p): - try: - # for arg in p.cmdline(): - #for arg in p['cmd'].split(): - # if "contentproc" in arg: - # return False - if 'contentproc' in p['cmd']: - return False - return True - except Exception: - logger.exception('') - return False - -def kill_prioritized( - procs: list['get_firefox_procs_ps_t.res_t.entry_t'], - to_free_mb, - low_priority_pids -): - candidates = [] - for p in procs: - if is_main_firefox(p): - continue - try: - # rss_mb = p.memory_info().rss / (1024 * 1024) - rss_mb = p['rss'] / (1024 * 1024) - candidates.append((p, rss_mb)) - except Exception: - logger.exception('') - continue - - candidates.sort(key=lambda x: ((x[0].pid in low_priority_pids), -x[1])) - - freed = 0.0 - killed = [] - for p, rss in candidates: - if freed >= to_free_mb: - break - try: - os.kill(p.pid, signal.SIGTERM) - killed.append(p.pid) - freed += rss - except Exception as e: - logger.exception(f"Error killing pid {p.pid}") - # print(f"Error killing pid {p.pid}: {e}", file=sys.stderr) - return killed, freed - -# — systemd-run logic — - -def launch_firefox_with_limits(base_cmd, memory_high, swap_max, extra_args, unit_name): - cmd = [ - "systemd-run", - "--user", - "--scope", - "-p", f"MemoryHigh={int(memory_high)}M", - ] - if swap_max is not None: - cmd += ["-p", f"MemorySwapMax={int(swap_max)}M"] - if unit_name: - cmd += ["--unit", unit_name] - - cmd += base_cmd - cmd += extra_args - - devnull = subprocess.DEVNULL - proc = subprocess.Popen(cmd, stdin=devnull, stdout=devnull, stderr=devnull) - print("Launched Firefox via systemd-run, PID:", proc.pid, file=sys.stderr) - return proc - -# — Main + TUI + Monitoring — - -def main(): - logging.basicConfig(level=logging.INFO) - - parser = argparse.ArgumentParser(description="Firefox memory manager with slice + graceful shutdown") - parser.add_argument("--max-mb", type=float, required=True, - help="Memory threshold in MB (used for killing logic & MemoryHigh)") - parser.add_argument("--kill-percent", type=float, default=70.0, - help="If over max, kill until usage ≤ this percent of max") - parser.add_argument("--swap-max-mb", type=float, default=None, - help="MemorySwapMax (MB) for the systemd scope") - parser.add_argument("--interval", type=float, default=1.0, - help="Monitoring interval in seconds") - parser.add_argument("--unit-name", type=str, default="firefox-limited", - help="Name for systemd transient unit") - parser.add_argument("--firefox-extra", action="append", default=[], - help="Extra CLI args to pass to Firefox (can repeat)") - parser.add_argument("firefox_cmd", nargs=argparse.REMAINDER, - help="Firefox command + args (if launching it)") - - args = parser.parse_args() - - low_priority_pids = set() - body = TextArea(focusable=False, scrollbar=True) - - terminate_flag = threading.Event() - - lock = threading.Lock() - - firefox_proc = None - - def terminate(): - terminate_flag.set() - app.exit() - - def stop(): - with lock: - if firefox_proc: - try: - firefox_proc.terminate() - firefox_proc.wait(timeout=5) - except Exception: - logger.exception('') - try: - firefox_proc.kill() - except Exception: - logger.exception('') - pass - # app.exit() - - # signal.signal(signal.SIGINT, lambda s, f: terminate()) - # signal.signal(signal.SIGTERM, lambda s, f: terminate()) - - def refresh_body(): - nonlocal firefox_proc - - with lock: - procs = get_firefox_procs_ps(slice_name=args.unit_name) - total = total_rss_mb(procs) - limit = args.max_mb - kill_to = args.kill_percent / 100.0 * limit - - lines = [ - f"Firefox RSS (slice={args.unit_name}): {total:.1f} MB", - f"Threshold (max): {limit:.1f} MB", - f"Kill‑to target: {kill_to:.1f} MB ({args.kill_percent}%)", - f"Low‑priority PIDs: {sorted(low_priority_pids)}" - ] - - if total > limit: - to_free = total - kill_to - killed, freed = kill_prioritized(procs, to_free, low_priority_pids) - lines.append(f"Killed: {killed}") - lines.append(f"Freed ≈ {freed:.1f} MB") - else: - lines.append("Within limit — no kill") - - if firefox_proc and firefox_proc.poll() is not None: - print("Firefox died — restarting …", file=sys.stderr) - firefox_proc = launch_firefox_with_limits( - args.firefox_cmd, - memory_high=args.max_mb, - swap_max=args.swap_max_mb, - extra_args=args.firefox_extra, - unit_name=args.unit_name - ) - - body.text = "\n".join(lines) - - dialog_float = [None] - root_floats = [] - - def open_pid_dialog(): - ta = TextArea(text="", multiline=True, scrollbar=True) - - def on_ok(): - txt = ta.text - for m in re.finditer(r"\((\d+)\)", txt): - low_priority_pids.add(int(m.group(1))) - close_dialog() - refresh_body() - - def on_cancel(): - close_dialog() - - dialog = Dialog( - title="Enter low‑priority PIDs", - body=ta, - buttons=[Button(text="OK", handler=on_ok), Button(text="Cancel", handler=on_cancel)], - width=60, - modal=True - ) - f = Float(content=dialog, left=2, top=2) - dialog_float[0] = f - root_floats.append(f) - app.layout.focus(ta) - - def open_message(title, message): - def on_close(): - close_dialog() - - dialog = Dialog( - title=title, - body=Label(text=message), - buttons=[Button(text="Close", handler=on_close)], - width=50, - modal=True - ) - f = Float(content=dialog, left=4, top=4) - dialog_float[0] = f - root_floats.append(f) - app.layout.focus(dialog) - - def close_dialog(): - f = dialog_float[0] - if f in root_floats: - root_floats.remove(f) - dialog_float[0] = None - app.layout.focus(body) - - kb = KeyBindings() - - @kb.add("q") - def _(event): - terminate() - - @kb.add("m") - def _(event): - open_pid_dialog() - - @kb.add("h") - def _(event): - open_message("Help", "Keys: m=add PIDs, s=settings, a=about, q=quit") - - @kb.add("s") - def _(event): - open_message("Settings", - f"max_mb = {args.max_mb}\n" - f"kill_percent = {args.kill_percent}\n" - f"slice = {args.unit_name}\n" - f"swap_max_mb = {args.swap_max_mb}\n" - f"extra firefox args = {args.firefox_extra}") - - @kb.add("a") - def _(event): - open_message("About", f"Version {__version__}\nCreated {__created__}") - - root = FloatContainer( - content=HSplit([ - Frame(body, title="Firefox Memory Manager"), - Window(height=1, content=FormattedTextControl("q=quit, m=PID, h=help, s=setting, a=about")) - ]), - floats=root_floats, - modal=True - ) - - style = Style.from_dict({ - "frame.border": "ansicyan", - "dialog.body": "bg:#444444", - "dialog": "bg:#888888", - }) - - app = Application( - layout=Layout(root), - key_bindings=kb, - style=style, - full_screen=True, - refresh_interval=args.interval, - ) - - if args.firefox_cmd: - firefox_proc = launch_firefox_with_limits( - args.firefox_cmd, - memory_high=args.max_mb, - swap_max=args.swap_max_mb, # **fixed here** - extra_args=args.firefox_extra, - unit_name=args.unit_name - ) - - def monitor_loop(): - nonlocal firefox_proc - while not terminate_flag.is_set(): - refresh_body() - - time.sleep(args.interval) - - # stop() - - terminate_flag = threading.Event() - t = threading.Thread(target=monitor_loop, daemon=True) - t.start() - - # refresh_body() - app.run(handle_sigint=True) # from prompt‑toolkit API :contentReference[oaicite:0]{index=0} - - t.join() - - stop() - -if __name__ == "__main__": - main() diff --git a/python/meson.build b/python/meson.build index 5191121..805b2e4 100644 --- a/python/meson.build +++ b/python/meson.build @@ -5,7 +5,7 @@ project( ).stdout().strip('\n'), # 'online.fxreader.uv', # ['c', 'cpp'], - version: '0.1.5.41', + version: '0.1.5.42', # default_options: [ # 'cpp_std=c++23', # # 'prefer_static=true', diff --git a/python/online/fxreader/pr34/oom_firefox.py b/python/online/fxreader/pr34/oom_firefox.py new file mode 100644 index 0000000..d50eca6 --- /dev/null +++ b/python/online/fxreader/pr34/oom_firefox.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +import psutil +import pathlib +import logging +import logging.handlers +import os +import signal +import subprocess +import threading +import time +import argparse +import re +import sys + +from prompt_toolkit.application import Application +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout import Layout, HSplit, FloatContainer, Float +from prompt_toolkit.layout.containers import Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.widgets import TextArea, Frame, Dialog, Button, Label +from prompt_toolkit.styles import Style + +from typing import ( + TypedDict, + Any, +) +from collections import OrderedDict + +logger = logging.getLogger(__name__) + +__version__ = '0.6.1' +__created__ = '2025-11-21' + +# — Helper for cgroup / slice matching — + + +def get_cgroup_path(pid): + try: + with open(f'/proc/{pid}/cgroup', 'r') as f: + for line in f: + parts = line.strip().split(':', 2) + if len(parts) == 3: + return parts[2] + except Exception: + logger.exception('') + return None + return None + + +def slice_matches(cpath, target_slice): + if not cpath or not target_slice: + return False + comps = cpath.strip('/').split('/') + tgt = target_slice.lower() + for comp in comps: + name = comp.lower() + if name.endswith('.slice'): + name = name[:-6] + if tgt == name: + return True + return False + + +# — Memory‑management logic — + + +class get_firefox_procs_ps_t: + class res_t: + class entry_t(TypedDict): + rss: int + pid: int + ppid: int + cgroup: str + cmd: str + + +def get_firefox_procs_ps(slice_name=None) -> list[get_firefox_procs_ps_t.res_t.entry_t]: + entries: dict[int, dict[str, Any]] = dict() + + for regex, columns in [ + ( + re.compile(r'^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$'), + OrderedDict( + pid=lambda x: int(x[1]), + rss=lambda x: int(x[2]) * 1024, + ppid=lambda x: int(x[3]), + cmd=lambda x: x[4], + ), + ), + ( + re.compile(r'^\s*(\d+)\s+(.*)$'), + OrderedDict( + pid=lambda x: int(x[1]), + cgroup=lambda x: x[2], + ), + ), + ]: + lines = ( + subprocess.check_output( + [ + 'ps', + '-ax', + '-o', + ','.join(columns.keys()), + ] + ) + .decode('utf-8') + .splitlines()[1:] + ) + + for line in lines: + r = re.compile(regex) + # print([r, line]) + match = r.match(line) + + assert match + + entry = {k: v(match) for k, v in columns.items()} + + if not entry['pid'] in entries: + entries[entry['pid']] = dict() + + entries[entry['pid']].update(entry) + + filtered_entries: list[dict[str, Any]] = [] + + for entry in entries.values(): + if not 'cgroup' in entry or not 'rss' in entry: + continue + + if not slice_name is None: + if not slice_name in entry['cgroup']: + continue + filtered_entries.append(entry) + + return filtered_entries + + +def get_firefox_procs(slice_name=None): + procs = [] + for p in psutil.process_iter(['pid', 'name', 'cmdline', 'memory_info']): + try: + name = p.info['name'] + cmd = p.info['cmdline'] + if not cmd: + continue + if 'firefox' not in name and not (cmd and 'firefox' in cmd[0]): + continue + if slice_name: + cpath = get_cgroup_path(p.pid) + if not slice_matches(cpath, slice_name): + continue + procs.append(p) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + return procs + + +def total_rss_mb(procs: list['get_firefox_procs_ps_t.res_t.entry_t']): + total = 0 + for p in procs: + try: + # total += p.memory_info().rss + total += p['rss'] + except Exception: + logger.exception('') + pass + return total / (1024 * 1024) + + +def is_main_firefox(p): + try: + # for arg in p.cmdline(): + # for arg in p['cmd'].split(): + # if "contentproc" in arg: + # return False + if 'contentproc' in p['cmd']: + return False + return True + except Exception: + logger.exception('') + return False + + +def kill_prioritized(procs: list['get_firefox_procs_ps_t.res_t.entry_t'], to_free_mb, low_priority_pids): + candidates = [] + for p in procs: + if is_main_firefox(p): + continue + try: + # rss_mb = p.memory_info().rss / (1024 * 1024) + rss_mb = p['rss'] / (1024 * 1024) + candidates.append((p, rss_mb)) + except Exception: + logger.exception('') + continue + + candidates.sort(key=lambda x: ((x[0]['pid'] in low_priority_pids), -x[1])) + + freed = 0.0 + killed = [] + for p, rss in candidates: + if freed >= to_free_mb: + break + + logger.info( + dict( + p=p, + action='kill', + msg='started', + ) + ) + + try: + os.kill(p['pid'], signal.SIGTERM) + killed.append(p['pid']) + freed += rss + except Exception as e: + logger.exception(f'Error killing pid {p["pid"]}') + # print(f"Error killing pid {p.pid}: {e}", file=sys.stderr) + return killed, freed + + +# — systemd-run logic — + + +def launch_firefox_with_limits(base_cmd, memory_high, swap_max, extra_args, unit_name): + cmd = [ + 'systemd-run', + '--user', + '--scope', + '-p', + f'MemoryHigh={int(memory_high)}M', + ] + if swap_max is not None: + cmd += ['-p', f'MemorySwapMax={int(swap_max)}M'] + if unit_name: + cmd += ['--unit', unit_name] + + cmd += base_cmd + cmd += extra_args + + devnull = subprocess.DEVNULL + proc = subprocess.Popen(cmd, stdin=devnull, stdout=devnull, stderr=devnull) + print('Launched Firefox via systemd-run, PID:', proc.pid, file=sys.stderr) + return proc + + +# — Main + TUI + Monitoring — + + +def main(): + os.makedirs(pathlib.Path('~/.cache/oom_firefox/').expanduser(), exist_ok=True) + + logging.basicConfig( + level=logging.INFO, + handlers=[ + logging.handlers.RotatingFileHandler( + pathlib.Path('~/.cache/oom_firefox/log').expanduser(), + maxBytes=128 * 1024, + backupCount=3, + ) + ], + ) + + parser = argparse.ArgumentParser(description='Firefox memory manager with slice + graceful shutdown') + parser.add_argument('--max-mb', type=float, required=True, help='Memory threshold in MB (used for killing logic & MemoryHigh)') + parser.add_argument('--kill-percent', type=float, default=70.0, help='If over max, kill until usage ≤ this percent of max') + parser.add_argument('--swap-max-mb', type=float, default=None, help='MemorySwapMax (MB) for the systemd scope') + parser.add_argument('--interval', type=float, default=1.0, help='Monitoring interval in seconds') + parser.add_argument('--unit-name', type=str, default='firefox-limited', help='Name for systemd transient unit') + parser.add_argument('--firefox-extra', action='append', default=[], help='Extra CLI args to pass to Firefox (can repeat)') + parser.add_argument('firefox_cmd', nargs=argparse.REMAINDER, help='Firefox command + args (if launching it)') + + args = parser.parse_args() + + low_priority_pids = set() + body = TextArea(focusable=False, scrollbar=True) + + terminate_flag = threading.Event() + + lock = threading.Lock() + + firefox_proc = None + + def terminate(): + terminate_flag.set() + app.exit() + + def stop(): + with lock: + if firefox_proc: + try: + firefox_proc.terminate() + firefox_proc.wait(timeout=5) + except Exception: + logger.exception('') + try: + firefox_proc.kill() + except Exception: + logger.exception('') + pass + # app.exit() + + # signal.signal(signal.SIGINT, lambda s, f: terminate()) + # signal.signal(signal.SIGTERM, lambda s, f: terminate()) + + def refresh_body(): + nonlocal firefox_proc + + with lock: + procs = get_firefox_procs_ps(slice_name=args.unit_name) + total = total_rss_mb(procs) + limit = args.max_mb + kill_to = args.kill_percent / 100.0 * limit + + lines = [ + f'Firefox RSS (slice={args.unit_name}): {total:.1f} MB', + f'Threshold (max): {limit:.1f} MB', + f'Kill‑to target: {kill_to:.1f} MB ({args.kill_percent}%)', + f'Low‑priority PIDs: {sorted(low_priority_pids)}', + ] + + if total > limit: + to_free = total - kill_to + killed, freed = kill_prioritized(procs, to_free, low_priority_pids) + lines.append(f'Killed: {killed}') + lines.append(f'Freed ≈ {freed:.1f} MB') + else: + lines.append('Within limit — no kill') + + if firefox_proc and firefox_proc.poll() is not None: + print('Firefox died — restarting …', file=sys.stderr) + firefox_proc = launch_firefox_with_limits( + args.firefox_cmd, memory_high=args.max_mb, swap_max=args.swap_max_mb, extra_args=args.firefox_extra, unit_name=args.unit_name + ) + + body.text = '\n'.join(lines) + + dialog_float = [None] + root_floats = [] + + def open_pid_dialog(): + ta = TextArea(text='', multiline=True, scrollbar=True) + + def on_ok(): + txt = ta.text + for m in re.finditer(r'\((\d+)\)', txt): + low_priority_pids.add(int(m.group(1))) + close_dialog() + refresh_body() + + def on_cancel(): + close_dialog() + + dialog = Dialog( + title='Enter low‑priority PIDs', body=ta, buttons=[Button(text='OK', handler=on_ok), Button(text='Cancel', handler=on_cancel)], width=60, modal=True + ) + f = Float(content=dialog, left=2, top=2) + dialog_float[0] = f + root_floats.append(f) + app.layout.focus(ta) + + def open_message(title, message): + def on_close(): + close_dialog() + + dialog = Dialog(title=title, body=Label(text=message), buttons=[Button(text='Close', handler=on_close)], width=50, modal=True) + f = Float(content=dialog, left=4, top=4) + dialog_float[0] = f + root_floats.append(f) + app.layout.focus(dialog) + + def close_dialog(): + f = dialog_float[0] + if f in root_floats: + root_floats.remove(f) + dialog_float[0] = None + app.layout.focus(body) + + kb = KeyBindings() + + @kb.add('q') + def _(event): + terminate() + + @kb.add('m') + def _(event): + open_pid_dialog() + + @kb.add('h') + def _(event): + open_message('Help', 'Keys: m=add PIDs, s=settings, a=about, q=quit') + + @kb.add('s') + def _(event): + open_message( + 'Settings', + f'max_mb = {args.max_mb}\n' + f'kill_percent = {args.kill_percent}\n' + f'slice = {args.unit_name}\n' + f'swap_max_mb = {args.swap_max_mb}\n' + f'extra firefox args = {args.firefox_extra}', + ) + + @kb.add('a') + def _(event): + open_message('About', f'Version {__version__}\nCreated {__created__}') + + root = FloatContainer( + content=HSplit( + [Frame(body, title='Firefox Memory Manager'), Window(height=1, content=FormattedTextControl('q=quit, m=PID, h=help, s=setting, a=about'))] + ), + floats=root_floats, + modal=True, + ) + + style = Style.from_dict( + { + 'frame.border': 'ansicyan', + 'dialog.body': 'bg:#444444', + 'dialog': 'bg:#888888', + } + ) + + app = Application( + layout=Layout(root), + key_bindings=kb, + style=style, + full_screen=True, + refresh_interval=args.interval, + ) + + if args.firefox_cmd: + firefox_proc = launch_firefox_with_limits( + args.firefox_cmd, + memory_high=args.max_mb, + swap_max=args.swap_max_mb, # **fixed here** + extra_args=args.firefox_extra, + unit_name=args.unit_name, + ) + + def monitor_loop(): + nonlocal firefox_proc + while not terminate_flag.is_set(): + try: + refresh_body() + except: + logger.exception('') + + time.sleep(args.interval) + + # stop() + + terminate_flag = threading.Event() + t = threading.Thread(target=monitor_loop, daemon=True) + t.start() + + # refresh_body() + app.run(handle_sigint=True) # from prompt‑toolkit API :contentReference[oaicite:0]{index=0} + + t.join() + + stop() + + +if __name__ == '__main__': + main() diff --git a/python/pyproject.toml b/python/pyproject.toml index 00045e9..3a18b73 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -72,6 +72,7 @@ build-backend = "mesonpy" [project.scripts] online-fxreader-pr34-commands = 'online.fxreader.pr34.commands:commands_cli' +oom_firefox = 'online.fxreader.pr34.oom_firefox:main' [tool.ruff] diff --git a/releases/whl/online_fxreader_pr34-0.1.5.42-py3-none-any.whl b/releases/whl/online_fxreader_pr34-0.1.5.42-py3-none-any.whl new file mode 100644 index 0000000..97296d9 --- /dev/null +++ b/releases/whl/online_fxreader_pr34-0.1.5.42-py3-none-any.whl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0404b4c65982bc74364d2f07d3bfccbe46bc2fbc64ff10820a383f96c39420a1 +size 80608