Files
omarchy/plugins/omaplug/runtime-state.py
T
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

265 lines
10 KiB
Python

#!/usr/bin/env python3
"""Private state and bounded process supervision for Omaplug (stdlib only)."""
import contextlib
import fcntl
import json
import os
from pathlib import Path
import re
import secrets
import selectors
import signal
import stat
import subprocess
import sys
import time
LIMIT = 131072
HERE = Path(__file__).resolve().parent
class PrivateDirectory:
def __init__(self, path):
path = Path(path)
if not path.is_absolute() or ".." in path.parts or len(path.parts) < 3:
raise ValueError("unsafe state directory")
fd = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
try:
for part in path.parts[1:]:
try:
os.mkdir(part, 0o700, dir_fd=fd)
except FileExistsError:
pass
child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
os.close(fd)
fd = child
info = os.fstat(fd)
sticky_root = info.st_mode & stat.S_ISVTX
if (info.st_uid not in (0, os.getuid()) and not sticky_root) or (info.st_mode & 0o022 and not sticky_root):
raise ValueError("unsafe state ancestor")
info = os.fstat(fd)
if info.st_uid != os.getuid() or info.st_mode & 0o022:
raise ValueError("state directory must be privately owned")
# Safely migrate an owned 0755 directory from previous versions.
os.fchmod(fd, 0o700)
self.fd = fd
except BaseException:
os.close(fd)
raise
def close(self):
os.close(self.fd)
def check(self, name):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", name):
raise ValueError("invalid state name")
try:
info = os.stat(name, dir_fd=self.fd, follow_symlinks=False)
except FileNotFoundError:
return
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1:
raise ValueError("unsafe state file")
if info.st_mode & 0o022:
raise ValueError("writable shared state file")
def write(self, name, data):
self.check(name)
if len(data) > LIMIT:
raise ValueError("state exceeds size limit")
temporary = ".tmp-" + secrets.token_hex(16)
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=self.fd)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, name, src_dir_fd=self.fd, dst_dir_fd=self.fd)
finally:
with contextlib.suppress(FileNotFoundError):
os.unlink(temporary, dir_fd=self.fd)
def read(self, name):
self.check(name)
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=self.fd)
with os.fdopen(fd, "rb") as stream:
info = os.fstat(stream.fileno())
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1:
raise ValueError("unsafe state file")
data = stream.read(LIMIT + 1)
if len(data) > LIMIT:
raise ValueError("state exceeds size limit")
return data
@contextlib.contextmanager
def lock(self, name):
self.check(name)
fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK,
0o600, dir_fd=self.fd)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1:
raise ValueError("unsafe lock file")
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
yield
finally:
os.close(fd)
def state_root():
base = os.environ.get("XDG_RUNTIME_DIR") or os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
return Path(base) / "omaplug"
def stop_child(child):
# Descendants can keep stdout open after the leader exits.
with contextlib.suppress(ProcessLookupError):
os.killpg(child.pid, signal.SIGTERM)
try:
child.wait(timeout=1)
except subprocess.TimeoutExpired:
pass
with contextlib.suppress(ProcessLookupError):
os.killpg(child.pid, signal.SIGKILL)
child.wait()
def run(command, seconds, consume):
child = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
start_new_session=True)
deadline = time.monotonic() + seconds
total = 0
try:
with selectors.DefaultSelector() as selector:
selector.register(child.stdout, selectors.EVENT_READ)
while selector.get_map():
if time.monotonic() >= deadline:
raise TimeoutError("operation timed out")
for key, _ in selector.select(0.2):
chunk = os.read(key.fileobj.fileno(), 4096)
if not chunk:
selector.unregister(key.fileobj)
continue
total += len(chunk)
if total > LIMIT:
raise ValueError("process output exceeds limit")
consume(chunk)
return child.wait(timeout=max(0.1, deadline - time.monotonic()))
finally:
stop_child(child)
child.stdout.close()
def emit(data):
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
def check_plugins(directory, store, coordinated=True):
command = ["bash", str(HERE / "plugin-state.sh"), directory]
# A fetch is bounded individually; allow the complete sequential inventory.
count = min(1024, len(os.listdir(directory))) if os.path.isdir(directory) else 0
deadline = min(3600, max(60, count * 20))
output = bytearray()
def consume(chunk):
output.extend(chunk)
emit(chunk)
if not coordinated:
return run(command, deadline, consume)
waiting = False
until = time.monotonic() + deadline
while time.monotonic() < until:
try:
with store.lock("auto-check.flock"):
if waiting:
data = json.loads(store.read("auto-check.cache"))
if data["directory"] != directory:
raise ValueError("cached check belongs to another plugin directory")
emit(data["output"].encode())
return data["code"]
code = run(command, deadline, consume)
store.write("auto-check.cache", json.dumps({"directory": directory,
"output": output.decode(errors="replace"), "code": code}).encode())
return code
except BlockingIOError:
waiting = True
time.sleep(1)
raise TimeoutError("waiting for update check timed out")
def update(status, job, ids):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,99}", job) or not 1 <= len(ids) <= 256:
raise ValueError("invalid update job")
path = Path(status)
with contextlib.closing(PrivateDirectory(path.parent)) as store:
with store.lock(path.name + ".flock"):
store.check(path.name)
output = bytearray()
def consume(chunk):
output.extend(chunk)
store.write(path.name, bytes(output))
return run(["bash", str(HERE / "update-helper.sh"), "--run", job, *ids],
min(3600, len(ids) * 140 + 30), consume)
def install(url, job, store):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,99}", job):
raise ValueError("invalid install job")
if not re.fullmatch(r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*/?", url) or ".." in url:
raise ValueError("invalid GitHub repository URL")
with store.lock("install.flock"):
store.write("install.status", json.dumps({"job": job, "running": True, "pid": os.getpid()}).encode())
output = bytearray()
try:
code = run(["timeout", "-k", "2", "180", "omarchy", "plugin", "add", url, "--yes"],
185, lambda chunk: output.extend(chunk[:max(0, 8000 - len(output))]))
result = {"running": False, "failed": code != 0, "finished": int(time.time())}
except (TimeoutError, ValueError, OSError) as error:
result = {"running": False, "failed": True, "finished": int(time.time()), "error": str(error)}
result["job"] = job
store.write("install.status", json.dumps(result).encode())
return int(result["failed"])
def main():
os.umask(0o077)
signal.signal(signal.SIGTERM, lambda *_: sys.exit(143))
action, *args = sys.argv[1:]
if action == "update":
return update(args[0], args[1], args[2:])
with contextlib.closing(PrivateDirectory(state_root())) as store:
if action in ("check", "check-manual"):
return check_plugins(args[0], store, action == "check")
if action == "install":
return install(args[0], args[1], store)
if action == "read":
emit(store.read(args[0]))
elif action == "reopen-write":
if args[0] not in ("layout", "main"):
raise ValueError("invalid reopen page")
store.write("reopen.status", json.dumps({"page": args[0], "time": time.time()}).encode())
elif action == "reopen-read":
with store.lock("reopen.flock"):
data = json.loads(store.read("reopen.status"))
os.unlink("reopen.status", dir_fd=store.fd)
if time.time() - data["time"] > 10:
return 1
print(data["page"])
elif action != "read":
raise ValueError("unknown action")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except BlockingIOError:
sys.exit(3)
except (OSError, ValueError, IndexError, KeyError, TimeoutError) as error:
print("omaplug: " + str(error), file=sys.stderr)
sys.exit(2)