diff --git a/PROJECT_GUIDE.md b/PROJECT_GUIDE.md index 459f32b..691a2c9 100644 --- a/PROJECT_GUIDE.md +++ b/PROJECT_GUIDE.md @@ -91,6 +91,7 @@ Scythe uses a unified multicall binary dispatch model: ### Multicall Dispatching: - **Daemon Mode**: Invoked when `args[1] == "daemon"`, `args[1] == "--mock"`, or when executed via symlink `scythe-daemon` / `vrec-daemon`. Runs the headless capture and recording engine ([`src/daemon.rs`](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/daemon.rs)). +- **Dispatch uses argv[0], not `current_exe()`**: `current_exe()` reads `/proc/self/exe`, which resolves symlinks to the real file. Every installed alias would therefore report `scythe` and dispatch to the client, so `scythe-daemon` would open the HUD instead of starting the engine. `invoked_stem()` reads argv[0] and falls back to `current_exe()` only when argv[0] has no usable stem. - **Client / UI Mode**: Invoked when executed as `scythe`, `scythe-ui`, or with client arguments (`save`, `record`, `screenshot`, `status`, `menu`, etc.). Runs [`src/main.rs`](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/main.rs) and launches [`src/overlay_egui.rs`](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/overlay_egui.rs). --- @@ -381,7 +382,13 @@ bash install.sh bash tests/e2e_test.sh ``` -`tests/e2e_test.sh` refuses to unlink a socket owned by a running daemon. Unix sockets cannot be re-linked: removing the path entry of a live daemon leaves the kernel socket bound but unreachable, and the user's session stays broken until it restarts. The script therefore sends `StopDaemon` and waits for the socket to disappear first, aborting if the daemon will not stop. +`tests/e2e_test.sh` treats a running daemon as a resource it borrows rather than deletes. Unix sockets cannot be re-linked: removing the path entry of a live daemon leaves the kernel socket bound but unreachable, and the session stays broken until that process dies. + +The script therefore: +1. probes liveness by connecting to the socket directly (`scythe --status` cannot be used for this, because it calls `ensure_daemon_running()` and would spawn a daemon while probing); +2. records the running daemon's command line and working directory; +3. sends `StopDaemon` and waits for the socket to disappear, aborting without unlinking if it will not stop; +4. restores that daemon on exit, so a developer's recorder survives a test run. ### Windows Cross-Compilation (from Linux Host): ```bash diff --git a/TODO_CHECKLIST.md b/TODO_CHECKLIST.md index 25163e4..42aeaf0 100644 --- a/TODO_CHECKLIST.md +++ b/TODO_CHECKLIST.md @@ -249,6 +249,14 @@ This checklist tracks known bugs, UI refinements, optimizations, and technical d --- +## 4a. Binary Dispatch + +- [x] **Symlink Aliases Dispatched to the UI Instead of the Daemon**: + - **Issue**: [`src/main.rs`](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/src/main.rs) decided daemon mode from `std::env::current_exe()`, which reads `/proc/self/exe` and resolves symlinks. Because every installed alias points at the real `scythe` file, all of them reported the stem `scythe`, so `scythe-daemon` and `vrec-daemon` fell through to client mode and opened the HUD instead of starting the recording engine. The aliases shipped by [`install.sh`](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/install.sh), the `.deb`/`.rpm`/Flatpak packages, and the Hyprland `exec_cmd("scythe-daemon")` autostart line were all affected. A stale pre-consolidation `target/release/scythe-daemon` from an older build masked the bug during local testing. + - **Resolution**: Added `invoked_stem()`, which reads argv[0] and only falls back to `current_exe()` when argv[0] has no usable stem. Verified against a packaged tarball: `scythe-daemon` and `vrec-daemon` now report the running-instance guard, while `scythe`, `scythe-ui`, `vrec` and `vrec-ui` open the client. + +--- + ## 4b. Release Pipeline & Packaging Consolidation - [x] **CI/CD Referenced Binaries That No Longer Build**: @@ -281,7 +289,7 @@ This checklist tracks known bugs, UI refinements, optimizations, and technical d - [x] **E2E Test Unlinked a Live Daemon's Socket**: - **Issue**: [`tests/e2e_test.sh`](file:///home/oiupoyt/my%20stuff%20yea/projects/scythe/tests/e2e_test.sh) ran `rm -f "$SOCKET_PATH"` unconditionally and only stopped the systemd unit. A daemon started any other way (Hyprland `execs.lua`, manual launch) kept the flock; the test removed its socket path entry, leaving the kernel socket bound but unreachable. The session stayed broken until the process was killed. - - **Resolution**: The script now queries `--status`, sends `StopDaemon`, waits for the socket to disappear, and aborts without unlinking if the daemon will not stop. + - **Resolution**: The script now probes liveness with a direct Unix-socket connect (not `--status`, which calls `ensure_daemon_running()` and spawns a daemon while probing), records the running daemon's command line and working directory, sends `StopDaemon`, waits for the socket to disappear, aborts without unlinking if it will not stop, and restores the original daemon on exit. --- diff --git a/src/main.rs b/src/main.rs index 4c898d4..06aa7f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -384,12 +384,31 @@ fn handle_toggle_streaming() -> Result<(), Box Result<(), Box> { - let args: Vec = env::args().collect(); - let exe_name = env::current_exe() +/// Name the binary was invoked as. +/// +/// `current_exe()` reads /proc/self/exe, which resolves symlinks to the real +/// file, so the installed `scythe-daemon` / `scythe-ui` aliases would all +/// report "scythe" and dispatch to the client. argv[0] keeps the invocation +/// name, which is what multicall dispatch needs. +fn invoked_stem() -> String { + if let Some(arg0) = env::args_os().next() { + let path = std::path::Path::new(&arg0); + if let Some(stem) = path.file_stem() { + let stem = stem.to_string_lossy().to_string(); + if !stem.is_empty() { + return stem; + } + } + } + env::current_exe() .ok() .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string())) - .unwrap_or_default(); + .unwrap_or_default() +} + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + let exe_name = invoked_stem(); // Multicall dispatch: scythe-daemon executable, symlink, or subcommand let is_daemon = exe_name == "scythe-daemon" @@ -535,27 +554,27 @@ fn run_client(args: Vec) -> Result<(), Box { - println!("scythe-ui - UI Overlay and Hotkey listener for scythe"); + println!("scythe {} - GPU screen recorder, instant replay and HUD overlay", scythe::updater::CURRENT_VERSION); println!("Usage:"); - println!(" scythe-ui Open the interactive overlay UI menu (default)"); - println!(" scythe-ui --menu Open the interactive overlay UI menu"); - println!(" scythe-ui --gtk Open legacy GTK layer-shell overlay (Linux)"); - println!(" scythe-ui --hotkeys Run global hotkey manager in background"); - println!(" scythe-ui --status Query current daemon status"); - println!(" scythe-ui --save Save instant replay and show notification"); - println!(" scythe-ui --save-replay Alias for --save"); - println!(" scythe-ui --notify-save Show instant replay saved notification toast"); - println!(" scythe-ui --notify-start Show recording started notification toast"); - println!(" scythe-ui --notify-stop Show recording saved notification toast"); - println!(" scythe-ui --toast Show custom ShadowPlay notification toast"); - println!(" scythe-ui --record Toggle normal recording on/off (with notification)"); - println!(" scythe-ui --stream Toggle livestreaming on/off (with notification)"); - println!(" scythe-ui --cursor Toggle mouse cursor recording on/off (with notification)"); - println!(" scythe-ui --show-overlay Open the HUD overlay via the daemon (no toggle)"); - println!(" scythe-ui --start Start normal recording"); - println!(" scythe-ui --stop Stop normal recording"); - println!(" scythe-ui --reload Reload daemon configuration"); - println!(" scythe-ui --quit Stop background daemon"); + println!(" scythe Open the interactive overlay UI menu (default)"); + println!(" scythe --menu Open the interactive overlay UI menu"); + println!(" scythe --gtk Deprecated alias for --menu (GTK backend retired)"); + println!(" scythe --hotkeys Run global hotkey manager in background"); + println!(" scythe --status Query current daemon status"); + println!(" scythe --save Save instant replay and show notification"); + println!(" scythe --save-replay Alias for --save"); + println!(" scythe --notify-save Show instant replay saved notification toast"); + println!(" scythe --notify-start Show recording started notification toast"); + println!(" scythe --notify-stop Show recording saved notification toast"); + println!(" scythe --toast Show custom ShadowPlay notification toast"); + println!(" scythe --record Toggle normal recording on/off (with notification)"); + println!(" scythe --stream Toggle livestreaming on/off (with notification)"); + println!(" scythe --cursor Toggle mouse cursor recording on/off (with notification)"); + println!(" scythe --show-overlay Open the HUD overlay via the daemon (no toggle)"); + println!(" scythe --start Start normal recording"); + println!(" scythe --stop Stop normal recording"); + println!(" scythe --reload Reload daemon configuration"); + println!(" scythe --quit Stop background daemon"); return Ok(()); } _ => {} diff --git a/tests/e2e_test.sh b/tests/e2e_test.sh index db4d4cb..1031d62 100755 --- a/tests/e2e_test.sh +++ b/tests/e2e_test.sh @@ -11,17 +11,39 @@ cargo build --release --quiet SOCKET_PATH="${XDG_RUNTIME_DIR:-/tmp}/scythe.sock" +# Liveness probe with no side effects. `scythe --status` calls +# ensure_daemon_running(), so probing with it spawns a daemon and the check +# starts chasing its own tail. +daemon_alive() { + [ -S "$SOCKET_PATH" ] || return 1 + python3 -c ' +import socket, sys +s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +s.settimeout(0.5) +try: + s.connect(sys.argv[1]) +except OSError: + sys.exit(1) +finally: + s.close() +' "$SOCKET_PATH" 2>/dev/null +} + # A live daemon owns this socket. Removing the path entry while it runs leaves the # kernel socket bound but unreachable, breaking the user's session until restart. -# Shut it down gracefully instead, and refuse to run if it will not stop. -if [ -S "$SOCKET_PATH" ] && target/release/scythe --status >/dev/null 2>&1; then - echo "[0/6] Stopping existing scythe daemon..." +# Stop it gracefully, remember how it ran, and put it back when the test finishes. +PREVIOUS_DAEMON_CMD="" +PREVIOUS_DAEMON_CWD="" +if daemon_alive; then + PREVIOUS_DAEMON_CMD=$(pgrep -af 'scythe.*daemon' 2>/dev/null | head -1 | cut -d' ' -f2- || true) + PREVIOUS_DAEMON_CWD=$(readlink -f /proc/"$(pgrep -f 'scythe.*daemon' 2>/dev/null | head -1)"/cwd 2>/dev/null || true) + echo "[0/6] Existing daemon detected; stopping it for the test run..." target/release/scythe --quit >/dev/null 2>&1 || true - for _ in $(seq 1 50); do - [ -S "$SOCKET_PATH" ] || break + for _ in $(seq 1 80); do + daemon_alive || break sleep 0.1 done - if target/release/scythe --status >/dev/null 2>&1; then + if daemon_alive; then echo "ERROR: A scythe daemon is running and did not stop cleanly." echo " Refusing to unlink its socket. Stop it first: scythe --quit" exit 1 @@ -52,6 +74,10 @@ cleanup() { rm -f daemon_test.log if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then systemctl --user start scythe-daemon.service 2>/dev/null || true + elif [ -n "$PREVIOUS_DAEMON_CMD" ]; then + echo "Restoring the daemon that was running before the test..." + ( cd "${PREVIOUS_DAEMON_CWD:-$PWD}" && setsid $PREVIOUS_DAEMON_CMD >/dev/null 2>&1 & ) || true + sleep 1 fi } trap cleanup EXIT