Files
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

150 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reading and writing the JSON state files, safely.
live.json is rewritten twice a second and is all the bar widget ever looks
at; recent.json is a pre-downsampled 30-minute window so the panel's default
graphs paint without a query; apps.json is per-application traffic. All are
written to a temp file and renamed, so a reader never sees a half-written
file. The QML side does not open any of them itself (0.1.9): `nexthop
stream` reads them through `read_text_bounded` — no symlink following, a
regular file or nothing, a size cap on the read itself — and hands the shell
one re-serialised line per record.
"""
import json
import os
import stat
import tempfile
import time
from pathlib import Path
from .paths import APPS, LIVE, RECENT
def write_atomic(path: Path, payload: dict, durable: bool = False):
"""Write a JSON file so a reader sees the old one or the new one.
The temp file plus rename is what gives readers that guarantee, and it
costs nothing. The fsync is a different promise — that the bytes
survive a power cut — and none of the snapshots written here needs
it: each is replaced within seconds of the daemon starting. On btrfs
that fsync was 25× the payload at the block layer (62.5 KiB per 2.5 KB
write, measured), twice a second. `durable` keeps it for a caller
that genuinely wants it.
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.")
try:
with os.fdopen(fd, "w") as f:
json.dump(payload, f, separators=(",", ":"))
f.flush()
if durable:
os.fsync(f.fileno())
os.replace(tmp, path)
except BaseException:
try:
os.unlink(tmp)
except OSError:
pass
raise
def read_text_bounded(path: Path, max_bytes: int):
"""Read a state file, enforcing every property on the fd actually read.
`O_NOFOLLOW` refuses a symlinked path outright, `O_NONBLOCK` means a
FIFO left at the path returns instead of stalling the caller, `fstat`
on the descriptor proves it is a regular file, and the cap bounds the
read itself rather than trusting a size sampled beforehand. Returns
(text, stamp) or None; `stamp` is (mtime_ns, size), enough for a
caller to skip re-reading an unchanged file.
This is the only way state reaches a reader — the QML side consumes it
through `nexthop stream` rather than opening these paths itself, so an
oversized or non-regular file can never allocate or block inside the
long-lived shell process.
"""
try:
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK)
except OSError:
return None
try:
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode):
return None
chunks, total = [], 0
while total <= max_bytes:
try:
chunk = os.read(fd, min(65536, max_bytes + 1 - total))
except BlockingIOError:
break
except OSError:
return None
if not chunk:
break
chunks.append(chunk)
total += len(chunk)
if total > max_bytes:
return None
stamp = (st.st_mtime_ns, st.st_size)
finally:
os.close(fd)
try:
return b"".join(chunks).decode("utf-8"), stamp
except UnicodeDecodeError:
return None
def read_json(path: Path, default=None, max_bytes: int = 4 * 1024 * 1024):
"""Bounded read of a state file, parsed. The bound lives on the read,
not on a prior stat, for the same reason as the daemon's config read."""
got = read_text_bounded(path, max_bytes)
if got is None:
return default
try:
return json.loads(got[0])
except ValueError:
return default
def retire_legacy_snapshots(old_dir: Path, new_dir: Path, now: float = None):
"""The snapshots moved to the runtime dir in 0.2.22; tidy the old place.
`nexthop stream` is a long-lived process that resolved its paths when it
started, so a reader from before the move keeps watching the state dir
for as long as it lives — through the daemon handover, until the shell
reloads the QML. Deleting live.json there would leave that reader
holding the last number it saw, as if it were current. It is rewritten
once instead, as a tombstone: state "no-daemon", no index, and no pid,
so the old bar shows "no data" and the old version watch finds nothing
to retire. recent.json and apps.json are simply removed.
"""
old_dir, new_dir = Path(old_dir), Path(new_dir)
if old_dir == new_dir:
return
now = time.time() if now is None else now
for name in (RECENT, APPS):
try:
(old_dir / name).unlink()
except OSError:
pass
live = old_dir / LIVE
got = read_text_bounded(live, 256 * 1024)
if got is None:
return
try:
payload = json.loads(got[0])
except ValueError:
payload = None
if not isinstance(payload, dict):
try:
live.unlink()
except OSError:
pass
return
for key in ("pid", "pid_start", "daemon_version"):
payload.pop(key, None)
payload.update({"t": round(now, 3), "state": "no-daemon", "index": None,
"band": None, "down_since": None})
write_atomic(live, payload, durable=True)