- power: RAII DoneGuard sets done=true on every wait() exit path, so the timeout thread no longer sleeps its full 30 s holding a spawn_blocking slot when child.wait() errors. A separate timed_out AtomicBool marks our own SIGKILL so we do not misclassify an external OOM-kill. Memory ordering on the flags is now Release/Acquire. - i18n: detect_locale now reads LC_ALL, LC_MESSAGES, LANG in POSIX priority order before falling back to /etc/locale.conf, so systems installed in English with LC_ALL=de_DE.UTF-8 pick up the correct UI. - panel: execute_action desensitizes button_box on entry and re-enables it on error paths, so double-click or keyboard repeat cannot fire the same power action twice. - config: accept_wallpaper helper applies an extension allowlist (jpg, jpeg, png, webp) plus symlink rejection and a 10 MB size cap, applied to both the user-configured path and the Moonarch ecosystem fallback. Bounds worst-case decode latency and narrows the gdk-pixbuf parser attack surface.
9.7 KiB
9.7 KiB
Decisions
Architectural and design decisions for Moonset, in reverse chronological order.
2026-04-24 – Audit MEDIUM fixes: timeout guard, POSIX locale, button desensitize, wallpaper allowlist (v0.8.4)
- Who: ClaudeCode, Dom
- Why: Five MEDIUM findings: (1)
run_command's timeout thread leaked a 30 s gio::spawn_blocking slot ifchild.wait()errored, becausedone.store(true)ran after the?. (2) Timeout detection comparedstatus.signal() == Some(9)— a hardcoded signal number that also misclassifies OOM-killer SIGKILL as our timeout. (3)execute_actionnever desensitized the button_box, so a double-click or accidental keyboard repeat fired the action twice. (4)detect_localeread onlyLANG, ignoring POSIX priority order (LC_ALL>LC_MESSAGES>LANG) — a common dual-language setup picked the wrong UI language. (5) The wallpaper path was passed to gdk-pixbuf without extension or size restriction, widening the image-parser attack surface and allowing unbounded decode latency. - Tradeoffs: The extension allowlist (
jpg,jpeg,png,webp) rejects exotic formats users might have used before. The 10 MB size cap rejects uncompressed/high-quality 4K wallpapers; acceptable for a power menu. Memory ordering on thedoneflag is nowRelease/Acquireinstead ofRelaxed— no runtime cost but correct across threads. - How: (1) RAII
DoneGuardstruct setsdone.store(true, Release)in itsDrop, so the flag fires on every function exit path. A secondtimed_outAtomicBool distinguishes our SIGKILL from an external one. (2) ReplaceSome(9)with thetimed_outflag check. (3)execute_actionnow takesbutton_box: >k::Box, callsset_sensitive(false)on entry and re-enables it on error paths; success paths that quit skip the re-enable. All call sites updated. (4)detect_localereadsLC_ALL,LC_MESSAGES,LANGin order, picking the first non-empty value before falling back to/etc/locale.conf. (5)accept_wallpaperhelper applies extension allowlist + symlink rejection +MAX_WALLPAPER_FILE_SIZE = 10 MB, and is called for both config-path and Moonarch fallback.
2026-04-24 – Audit fix: avoid latent stdout pipe deadlock in run_command (v0.8.3)
- Who: ClaudeCode, Dom
- Why: Audit found that
run_commandpiped the child'sstdoutbut never drained it, then called blockingchild.wait(). A child writing more than one OS pipe buffer (~64 KB on Linux) would block onwrite()while the parent blocked inwait()— classic pipe deadlock, broken only by the 30 s SIGKILL timeout. Current callers (systemctl,niri msg,loginctl) do not emit that much output, but the structure was fragile and would bite on any future command or changed behaviour. - Tradeoffs: stdout is now fully discarded. If a future caller needs stdout, it will have to drain it concurrently with
wait()(separate reader thread). - How: Replace
.stdout(Stdio::piped())with.stdout(Stdio::null())inrun_command.stderrstays piped — it is drained afterwait(), which is safe becausewait()already reaped the child and no further writes can occur.
2026-03-31 – Fourth audit: release profile, GResource compression, lock stderr, sync markers
- Who: ClaudeCode, Dom
- Why: Fourth triple audit found missing release profile (LTO/strip), uncompressed GResource assets, moonlock stderr suppressed (errors invisible), and duplicated code without sync markers.
- Tradeoffs: moonlock stderr now inherited instead of null — errors appear in moonset's journal context. Acceptable for debugging, no security leak since moonlock logs to its own journal identifier.
- How: (1)
[profile.release]with LTO, codegen-units=1, strip. (2)compressed="true"on GResource entries. (3)Stdio::inherit()for moonlock stderr in lock(). (4) SYNC comments on duplicated blur/background functions.
2026-03-28 – Remove wallpaper from GResource bundle
- Who: ClaudeCode, Dom
- Why: All three Moon projects (moonset, moongreet, moonlock) embedded a 374kB fallback wallpaper in the binary via GResource. Moonarch already installs
/usr/share/moonarch/wallpaper.jpgat system setup time, making the embedded fallback unnecessary dead weight (~2MB in binary size). - Tradeoffs: If
/usr/share/moonarch/wallpaper.jpgis missing and no user config exists, moonset shows a solid CSS background instead of a wallpaper. Acceptable — the power menu is functional without a wallpaper image. - How: Removed
wallpaper.jpgfrom GResource XML and resources directory.resolve_background_pathreturnsOption<PathBuf>. All wallpaper-related functions handleNonegracefully. Binary size dropped from ~3.2MB to ~1.3MB.
2026-03-28 – Switch from env_logger to systemd-journal-logger
- Who: ClaudeCode, Dom
- Why: moonlock and moongreet already use systemd-journal-logger. moonset used env_logger which writes to stderr — not useful for a GUI app launched via keybind. Journal integration enables
journalctl -t moonsetand consistent troubleshooting across all three Moon projects. - Tradeoffs: Requires systemd at runtime. Graceful fallback to eprintln if journal logger fails. Acceptable since Moonarch targets systemd-based Arch Linux.
- How: Replace
env_loggerdep withsystemd-journal-logger, addsetup_logging()withMOONSET_DEBUGenv var for debug-level output. Same pattern as moonlock/moongreet.
2026-03-28 – Replace action name dispatch with quit_after field
- Who: ClaudeCode, Dom
- Why: Post-action behavior (quit the app or not) was controlled by comparing
action_name == "lock"— a magic string duplicated from the action definition. Renaming an action would silently break the dispatch. - Tradeoffs: Adds a field to
ActionDefthat most actions set tofalse. Acceptable because it makes the contract explicit and testable. - How:
ActionDef.quit_after: bool—truefor lock and logout,falsefor hibernate/reboot/shutdown.
2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
- Who: ClaudeCode, Dom
- Why: CPU-side Gaussian blur (
imagecrate) blocked startup and added caching complexity. moonlock already migrated to GPU blur. - Tradeoffs: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes
imagecrate dependency entirely. No disk cache needed. - How:
Snapshot::push_blur()+GskRenderer::render_texture()onconnect_realize. Blur happens once on the GPU when the widget gets its renderer. Symmetric with moonlock and moongreet.
2026-03-28 – Use absolute paths for system binaries
- Who: ClaudeCode, Dom
- Why: Security audit flagged PATH hijacking risk — relative binary names allow a malicious
$PATHentry to interceptsystemctl,loginctl, etc. - Tradeoffs: Hardcoded paths reduce portability to non-Arch distros where binaries may live elsewhere (e.g.
/sbin/). Acceptable because Moonarch targets Arch Linux exclusively. - How: All five power action wrappers now use
/usr/bin/prefixed paths.
2026-03-28 – Implement power action timeout via try_wait polling
- Who: ClaudeCode, Dom
- Why:
POWER_TIMEOUTandPowerError::Timeoutwere declared but never wired up. A hangingsystemctl hibernate(e.g. blocked NFS mount) would freeze the power menu indefinitely. - Tradeoffs: Polling with
try_wait()+ 100ms sleep is slightly less efficient than a dedicated timeout crate, but avoids adding a dependency for a single use case. - How:
run_commandnow pollschild.try_wait()against a 30s deadline, kills the child on timeout.
2026-03-28 – Centralize GRESOURCE_PREFIX
- Who: ClaudeCode, Dom
- Why: The string
/dev/moonarch/moonsetwas duplicated inconfig.rs,users.rs, and as literal strings inpanel.rsandmain.rs. Changing the application ID would require edits in 4+ locations. - Tradeoffs: Modules now depend on
crate::GRESOURCE_PREFIX— tighter coupling to main.rs, but acceptable for an internal constant. - How: Single
pub(crate) const GRESOURCE_PREFIXinmain.rs, referenced everywhere else.
2026-03-28 – Remove journal.md
- Who: ClaudeCode, Dom
- Why: One-time development notes from the Rust rewrite, never updated after initial session. Overlapped with memory system and git history.
- Tradeoffs: Historical context lost from the file, but the information is preserved in git history and the memory system.
- How: Deleted. Useful technical learnings migrated to persistent memory.
2026-03-27 – OVERLAY layer instead of TOP
- Who: ClaudeCode, Dom
- Why: Waybar occupies the TOP layer. The power menu must appear above it.
- Tradeoffs: OVERLAY is the highest layer — nothing can render above moonset while it's open. This is intentional for a session power menu.
- How:
setup_layer_shellusesgtk4_layer_shell::Layer::Overlayfor the panel window.
2026-03-27 – Lock without confirmation
- Who: ClaudeCode, Dom
- Why: Lock is immediately reversible (just unlock). All other actions (logout, hibernate, reboot, shutdown) are destructive or disruptive.
- Tradeoffs: One less click for the most common action. Risk of accidental lock is negligible since unlocking is trivial.
- How:
ActionDef.needs_confirm = falsefor lock; all others require inline confirmation.
2026-03-27 – Niri-specific logout via niri msg action quit
- Who: ClaudeCode, Dom
- Why: Moonarch is built exclusively for the Niri compositor. Generic Wayland logout mechanisms don't exist — each compositor has its own.
- Tradeoffs: Hard dependency on Niri. If the compositor changes,
power::logout()must be updated. - How:
Command::new("/usr/bin/niri").args(["msg", "action", "quit"]).