mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
229 lines
6.8 KiB
Rust
229 lines
6.8 KiB
Rust
//! Spawn `bsk daemon start` (default detached mode), wait for daemon.json,
|
|
//! then `bsk daemon stop` and verify the daemon is gone.
|
|
|
|
#![cfg(unix)]
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::{Command, Stdio};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use tempfile::TempDir;
|
|
|
|
fn bsk_bin() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_BIN_EXE_bsk"))
|
|
}
|
|
|
|
fn run(home: &std::path::Path, args: &[&str]) -> std::process::Output {
|
|
Command::new(bsk_bin())
|
|
.env("BSK_HOME", home)
|
|
.env("RUST_LOG", "warn")
|
|
.args(args)
|
|
.output()
|
|
.expect("spawn bsk command")
|
|
}
|
|
|
|
fn wait_for_pid_exit(pid: i32, timeout: Duration) -> bool {
|
|
let deadline = Instant::now() + timeout;
|
|
while Instant::now() < deadline {
|
|
let alive = unsafe { libc::kill(pid, 0) } == 0;
|
|
if !alive {
|
|
return true;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
}
|
|
false
|
|
}
|
|
|
|
#[test]
|
|
fn detached_start_and_stop_round_trip() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
// Use port 0 so concurrent test runs (or any existing daemon on
|
|
// 52800) don't collide with us; the daemon writes the chosen port
|
|
// back into daemon.json.
|
|
let out = run(
|
|
&home,
|
|
&["daemon", "start", "--port", "0", "--daemon-idle", "60s"],
|
|
);
|
|
assert!(
|
|
out.status.success(),
|
|
"daemon start failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
// Daemon.json should now exist with a live pid.
|
|
let info_path = home.join("daemon.json");
|
|
assert!(info_path.exists(), "daemon.json should be written");
|
|
let info: serde_json::Value =
|
|
serde_json::from_slice(&std::fs::read(&info_path).unwrap()).unwrap();
|
|
let pid = info["pid"].as_u64().expect("pid number") as i32;
|
|
assert!(pid > 0);
|
|
|
|
// Stop the daemon.
|
|
let out = run(&home, &["daemon", "stop"]);
|
|
assert!(
|
|
out.status.success(),
|
|
"daemon stop failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
assert!(
|
|
wait_for_pid_exit(pid, Duration::from_secs(5)),
|
|
"daemon pid {pid} should disappear after stop"
|
|
);
|
|
assert!(
|
|
!home.join("daemon.json").exists(),
|
|
"daemon.json should be cleaned up after stop"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stop_when_no_daemon_running_is_noop() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
let out = run(&home, &["daemon", "stop"]);
|
|
assert!(out.status.success(), "stop should succeed when no daemon");
|
|
}
|
|
|
|
#[test]
|
|
fn stop_refuses_live_pid_that_is_not_verified_daemon() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(home.join("run")).unwrap();
|
|
|
|
let mut child = Command::new("sleep")
|
|
.arg("30")
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("spawn controlled non-daemon child");
|
|
|
|
let info = serde_json::json!({
|
|
"pid": child.id(),
|
|
"sock_path": home.join("run").join("not-a-daemon.sock"),
|
|
"ws_port": 0,
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
"started_at_epoch_secs": 1
|
|
});
|
|
std::fs::write(
|
|
home.join("daemon.json"),
|
|
serde_json::to_vec_pretty(&info).unwrap(),
|
|
)
|
|
.unwrap();
|
|
|
|
let out = run(&home, &["daemon", "stop"]);
|
|
assert!(
|
|
!out.status.success(),
|
|
"stop should refuse unverified daemon.json"
|
|
);
|
|
assert!(
|
|
child.try_wait().unwrap().is_none(),
|
|
"non-daemon child must not be terminated"
|
|
);
|
|
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
|
|
#[test]
|
|
fn start_with_conflicting_port_refuses_existing_daemon() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
let out = run(
|
|
&home,
|
|
&["daemon", "start", "--port", "0", "--daemon-idle", "60s"],
|
|
);
|
|
assert!(
|
|
out.status.success(),
|
|
"daemon start failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let info_path = home.join("daemon.json");
|
|
let info: serde_json::Value =
|
|
serde_json::from_slice(&std::fs::read(&info_path).unwrap()).unwrap();
|
|
let pid = info["pid"].as_u64().expect("pid number") as i32;
|
|
let existing_port = info["ws_port"].as_u64().expect("ws_port") as u16;
|
|
let conflicting_port = if existing_port == 52801 { 52802 } else { 52801 };
|
|
let conflicting_port = conflicting_port.to_string();
|
|
|
|
let out = run(&home, &["daemon", "start", "--port", &conflicting_port]);
|
|
assert!(
|
|
!out.status.success(),
|
|
"start should fail when an existing daemon has different config"
|
|
);
|
|
assert!(
|
|
unsafe { libc::kill(pid, 0) } == 0,
|
|
"existing daemon should remain alive"
|
|
);
|
|
|
|
let _ = run(&home, &["daemon", "stop"]);
|
|
assert!(wait_for_pid_exit(pid, Duration::from_secs(5)));
|
|
}
|
|
|
|
#[test]
|
|
fn restart_propagates_stop_error_and_does_not_run_start() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let home = tmp.path().join("bsk");
|
|
std::fs::create_dir_all(home.join("run")).unwrap();
|
|
|
|
let mut child = Command::new("sleep")
|
|
.arg("30")
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("spawn controlled non-daemon child");
|
|
|
|
let info = serde_json::json!({
|
|
"pid": child.id(),
|
|
"sock_path": home.join("run").join("not-a-daemon.sock"),
|
|
"ws_port": 0,
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
"started_at_epoch_secs": 1
|
|
});
|
|
std::fs::write(
|
|
home.join("daemon.json"),
|
|
serde_json::to_vec_pretty(&info).unwrap(),
|
|
)
|
|
.unwrap();
|
|
|
|
let out = run(&home, &["daemon", "restart"]);
|
|
assert!(
|
|
!out.status.success(),
|
|
"restart should fail when stop phase fails"
|
|
);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
stderr.contains("restart failed during stop phase")
|
|
|| stderr.contains("restart failed during stop phase".to_lowercase().as_str()),
|
|
"restart error should mention stop phase: {stderr}"
|
|
);
|
|
|
|
// The decoy must still be alive — start should never have run.
|
|
assert!(
|
|
child.try_wait().unwrap().is_none(),
|
|
"decoy child must not be terminated"
|
|
);
|
|
|
|
// daemon.json should still point to the decoy (start did not run);
|
|
// also confirm no new daemon was actually started by checking that
|
|
// the recorded pid is still the decoy.
|
|
let after: serde_json::Value =
|
|
serde_json::from_slice(&std::fs::read(home.join("daemon.json")).unwrap()).unwrap();
|
|
assert_eq!(
|
|
after["pid"].as_u64().unwrap(),
|
|
child.id() as u64,
|
|
"daemon.json should be untouched after failed restart"
|
|
);
|
|
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|