//! `storebound-desktop --self-test`: the release smoke test of the exact artifact. It starts the bundled sidecar the //! same way the app does (next to the executable, policy::SIDECAR_ARGS), sends `session.hello` with a fresh secret, //! checks the protocol, prints one JSON line and exits. No window, no webview, no Tauri runtime: it runs headless //! (a CI runner, a mounted DMG, an SSH session) and never brings anything to the front. use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::mpsc; use std::time::{Duration, Instant}; use serde_json::{json, Value}; use crate::{confirm, policy}; pub const FLAG: &str = "--self-test"; const PROTOCOL: &str = "storebound-desktop/1"; /// The bundled sidecar: Tauri puts `externalBin` next to the main executable without its target triple /// (Contents/MacOS on macOS, the install folder on Windows, usr/bin in the AppImage and the deb). pub fn sidecar_path() -> Result { let exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?; let dir = exe.parent().ok_or("the executable has no folder")?; Ok(dir.join(format!("{}{}", policy::SIDECAR, std::env::consts::EXE_SUFFIX))) } /// Exit code of the self-test (0 = the sidecar answered with the expected protocol). pub fn run() -> i32 { match check(Duration::from_secs(30)) { Ok(v) => { println!("{v}"); 0 } Err(e) => { eprintln!("self-test failed: {e}"); 1 } } } pub fn check(timeout: Duration) -> Result { let bin = sidecar_path()?; if !bin.is_file() { return Err(format!("no sidecar next to the executable ({})", bin.file_name().unwrap_or_default().to_string_lossy())); } let started = Instant::now(); let mut child = Command::new(&bin) .args(policy::SIDECAR_ARGS) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .map_err(|e| format!("sidecar did not start: {e}"))?; let secret = confirm::new_secret()?; let hello = json!({ "jsonrpc": "2.0", "id": 1, "method": "session.hello", "params": { "secret": confirm::hex(&secret), "client": { "name": "storebound-desktop-self-test", "version": env!("CARGO_PKG_VERSION") } } }); let mut stdin = child.stdin.take().ok_or("no sidecar stdin")?; let line = format!("{hello}\n"); let stdout = child.stdout.take().ok_or("no sidecar stdout")?; let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { for l in BufReader::new(stdout).lines().map_while(Result::ok) { if let Ok(v) = serde_json::from_str::(&l) { if v.get("id").and_then(Value::as_u64) == Some(1) { let _ = tx.send(v); return; } } } }); let wrote = stdin.write_all(line.as_bytes()).and_then(|_| stdin.flush()); let answer = rx.recv_timeout(timeout); drop(stdin); let _ = child.kill(); let _ = child.wait(); wrote.map_err(|e| format!("sidecar stdin: {e}"))?; let answer = answer.map_err(|_| format!("no answer to session.hello within {} s", timeout.as_secs()))?; if let Some(e) = answer.get("error") { return Err(format!("session.hello failed: {e}")); } let r = &answer["result"]; let protocol = r["protocol"].as_str().unwrap_or_default(); if protocol != PROTOCOL { return Err(format!("unsupported sidecar protocol '{protocol}'")); } let methods = r["methods"].as_array().map(Vec::len).unwrap_or(0); if methods == 0 { return Err("the sidecar announced no methods".into()); } Ok(json!({ "ok": true, "app": env!("CARGO_PKG_VERSION"), "sidecar": r["server"]["version"], "protocol": protocol, "methods": methods, "ms": started.elapsed().as_millis() as u64, "os": std::env::consts::OS, "arch": std::env::consts::ARCH, })) }