fix(cli): dispatch from argv[0] so daemon symlinks work; harden e2e run

Multicall dispatch used std::env::current_exe(), which reads /proc/self/exe
and resolves symlinks. Every installed alias therefore reported the stem
"scythe", so scythe-daemon and vrec-daemon fell through to client mode and
opened the HUD instead of starting the engine. The aliases shipped by
install.sh, the deb/rpm/flatpak packages and the Hyprland exec_cmd line were
all affected; a stale pre-consolidation target/release/scythe-daemon masked
it during local testing.

invoked_stem() now reads argv[0] and only falls back to current_exe() when
argv[0] has no usable stem. Verified against a packaged tarball: both daemon
aliases hit the running-instance guard while scythe, scythe-ui, vrec and
vrec-ui open the client.

The e2e guard also probed liveness with `scythe --status`, which calls
ensure_daemon_running() and so spawned a daemon while probing, then chased
its own tail. It now connects to the socket directly, records the running
daemon's command line and working directory, stops it gracefully, and
restores it on exit so a developer's recorder survives a test run.

Help text now reports the unified binary and marks --gtk as a deprecated
alias, since the GTK backend was retired.
This commit is contained in:
spearzificial 2026-10-20 16:55:33 +05:30
parent e02bfefd72
commit 5988785d57
4 changed files with 92 additions and 32 deletions

View file

@ -91,6 +91,7 @@ Scythe uses a unified multicall binary dispatch model:
### Multicall Dispatching: ### 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)). - **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). - **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 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): ### Windows Cross-Compilation (from Linux Host):
```bash ```bash

View file

@ -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 ## 4b. Release Pipeline & Packaging Consolidation
- [x] **CI/CD Referenced Binaries That No Longer Build**: - [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**: - [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. - **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.
--- ---

View file

@ -384,12 +384,31 @@ fn handle_toggle_streaming() -> Result<(), Box<dyn std::error::Error + Send + Sy
} }
} }
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { /// Name the binary was invoked as.
let args: Vec<String> = env::args().collect(); ///
let exe_name = env::current_exe() /// `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() .ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string())) .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
.unwrap_or_default(); .unwrap_or_default()
}
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args: Vec<String> = env::args().collect();
let exe_name = invoked_stem();
// Multicall dispatch: scythe-daemon executable, symlink, or subcommand // Multicall dispatch: scythe-daemon executable, symlink, or subcommand
let is_daemon = exe_name == "scythe-daemon" let is_daemon = exe_name == "scythe-daemon"
@ -535,27 +554,27 @@ fn run_client(args: Vec<String>) -> Result<(), Box<dyn std::error::Error + Send
return send_command(Command::StopDaemon); return send_command(Command::StopDaemon);
} }
"--help" | "-h" => { "--help" | "-h" => {
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!("Usage:");
println!(" scythe-ui Open the interactive overlay UI menu (default)"); println!(" scythe Open the interactive overlay UI menu (default)");
println!(" scythe-ui --menu Open the interactive overlay UI menu"); println!(" scythe --menu Open the interactive overlay UI menu");
println!(" scythe-ui --gtk Open legacy GTK layer-shell overlay (Linux)"); println!(" scythe --gtk Deprecated alias for --menu (GTK backend retired)");
println!(" scythe-ui --hotkeys Run global hotkey manager in background"); println!(" scythe --hotkeys Run global hotkey manager in background");
println!(" scythe-ui --status Query current daemon status"); println!(" scythe --status Query current daemon status");
println!(" scythe-ui --save Save instant replay and show notification"); println!(" scythe --save Save instant replay and show notification");
println!(" scythe-ui --save-replay Alias for --save"); println!(" scythe --save-replay Alias for --save");
println!(" scythe-ui --notify-save Show instant replay saved notification toast"); println!(" scythe --notify-save Show instant replay saved notification toast");
println!(" scythe-ui --notify-start Show recording started notification toast"); println!(" scythe --notify-start Show recording started notification toast");
println!(" scythe-ui --notify-stop Show recording saved notification toast"); println!(" scythe --notify-stop Show recording saved notification toast");
println!(" scythe-ui --toast <t> <s> Show custom ShadowPlay notification toast"); println!(" scythe --toast <t> <s> Show custom ShadowPlay notification toast");
println!(" scythe-ui --record Toggle normal recording on/off (with notification)"); println!(" scythe --record Toggle normal recording on/off (with notification)");
println!(" scythe-ui --stream Toggle livestreaming on/off (with notification)"); println!(" scythe --stream Toggle livestreaming on/off (with notification)");
println!(" scythe-ui --cursor Toggle mouse cursor recording on/off (with notification)"); println!(" scythe --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 --show-overlay Open the HUD overlay via the daemon (no toggle)");
println!(" scythe-ui --start Start normal recording"); println!(" scythe --start Start normal recording");
println!(" scythe-ui --stop Stop normal recording"); println!(" scythe --stop Stop normal recording");
println!(" scythe-ui --reload Reload daemon configuration"); println!(" scythe --reload Reload daemon configuration");
println!(" scythe-ui --quit Stop background daemon"); println!(" scythe --quit Stop background daemon");
return Ok(()); return Ok(());
} }
_ => {} _ => {}

View file

@ -11,17 +11,39 @@ cargo build --release --quiet
SOCKET_PATH="${XDG_RUNTIME_DIR:-/tmp}/scythe.sock" 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 # 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. # 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. # Stop it gracefully, remember how it ran, and put it back when the test finishes.
if [ -S "$SOCKET_PATH" ] && target/release/scythe --status >/dev/null 2>&1; then PREVIOUS_DAEMON_CMD=""
echo "[0/6] Stopping existing scythe daemon..." 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 target/release/scythe --quit >/dev/null 2>&1 || true
for _ in $(seq 1 50); do for _ in $(seq 1 80); do
[ -S "$SOCKET_PATH" ] || break daemon_alive || break
sleep 0.1 sleep 0.1
done 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 "ERROR: A scythe daemon is running and did not stop cleanly."
echo " Refusing to unlink its socket. Stop it first: scythe --quit" echo " Refusing to unlink its socket. Stop it first: scythe --quit"
exit 1 exit 1
@ -52,6 +74,10 @@ cleanup() {
rm -f daemon_test.log rm -f daemon_test.log
if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then
systemctl --user start scythe-daemon.service 2>/dev/null || true 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 fi
} }
trap cleanup EXIT trap cleanup EXIT