diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0e961a8..ca4f450d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ concurrency: jobs: release-please: - runs-on: ubuntu-latest + runs-on: [ubuntu-latest, windows-latest, macos-latest] if: > github.ref == 'refs/heads/main' && github.repository_owner == 'ipvm-wg' && diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index 796bee8c..1d9a79b9 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -16,12 +16,13 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] rust-toolchain: - stable - nightly # minimum version - - 1.67 + # 1.68 fixes: https://github.com/rust-lang/cargo/pull/11347 + - 1.68 runs-on: ${{ matrix.os }} steps: - name: Checkout Repository diff --git a/Cargo.toml b/Cargo.toml index 6e71762c..c93d48ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ authors = [ ] edition = "2021" license = "Apache" -rust-version = "1.67.0" +rust-version = "1.68" [workspace.dependencies] anyhow = { version = "1.0", features = ["backtrace"] } diff --git a/homestar-runtime/fixtures/test_v4.toml b/homestar-runtime/fixtures/test_v4.toml new file mode 100644 index 00000000..cea9b576 --- /dev/null +++ b/homestar-runtime/fixtures/test_v4.toml @@ -0,0 +1,9 @@ +[monitoring] +process_collector_interval = 10 + +[node] + +[node.network] +events_buffer_len = 1000 +rpc_port = 9999 +rpc_host = "127.0.0.1" diff --git a/homestar-runtime/src/daemon.rs b/homestar-runtime/src/daemon.rs index fa40400d..a26bc6c6 100644 --- a/homestar-runtime/src/daemon.rs +++ b/homestar-runtime/src/daemon.rs @@ -3,6 +3,7 @@ use anyhow::Result; use std::path::PathBuf; +#[cfg(not(windows))] const PID_FILE: &str = "homestar.pid"; /// Start the Homestar runtime as a daemon. @@ -18,6 +19,6 @@ pub fn start(dir: PathBuf) -> Result<()> { /// Start the Homestar runtime as a daemon. #[cfg(windows)] -pub fn start(dir: PathBuf) -> Result<()> { +pub fn start(_dir: PathBuf) -> Result<()> { Err(anyhow::anyhow!("Daemonizing is not supported on Windows")) } diff --git a/homestar-runtime/src/runner.rs b/homestar-runtime/src/runner.rs index 8e1b30eb..86e6148d 100644 --- a/homestar-runtime/src/runner.rs +++ b/homestar-runtime/src/runner.rs @@ -21,9 +21,12 @@ use libipld::Cid; #[cfg(not(test))] use std::sync::atomic::{AtomicUsize, Ordering}; use std::{ops::ControlFlow, rc::Rc, sync::Arc, task::Poll}; +#[cfg(not(windows))] +use tokio::signal::unix::{signal, SignalKind}; +#[cfg(windows)] +use tokio::signal::windows; use tokio::{ runtime, select, - signal::unix::{signal, SignalKind}, sync::{mpsc, oneshot}, task::{AbortHandle, JoinHandle}, time, @@ -416,6 +419,7 @@ impl Runner { /// Captures shutdown signals for [Runner]. #[allow(dead_code)] + #[cfg(not(windows))] async fn shutdown_signal() -> Result<()> { let mut sigint = signal(SignalKind::interrupt())?; let mut sigterm = signal(SignalKind::terminate())?; @@ -425,7 +429,22 @@ impl Runner { _ = sigint.recv() => info!("SIGINT received, shutting down"), _ = sigterm.recv() => info!("SIGTERM received, shutting down"), } + Ok(()) + } + + #[allow(dead_code)] + #[cfg(windows)] + async fn shutdown_signal() -> Result<()> { + let mut sigint = windows::ctrl_close()?; + let mut sigterm = windows::ctrl_shutdown()?; + let mut sighup = windows::ctrl_break()?; + select! { + _ = tokio::signal::ctrl_c() => info!("CTRL-C received, shutting down"), + _ = sigint.recv() => info!("SIGINT received, shutting down"), + _ = sigterm.recv() => info!("SIGTERM received, shutting down"), + _ = sighup.recv() => info!("SIGHUP received, shutting down") + } Ok(()) } diff --git a/homestar-runtime/tests/cli.rs b/homestar-runtime/tests/cli.rs index 33cfadfb..186a2fec 100644 --- a/homestar-runtime/tests/cli.rs +++ b/homestar-runtime/tests/cli.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use assert_cmd::{crate_name, prelude::*}; +#[cfg(not(windows))] use nix::{ sys::signal::{self, Signal}, unistd::Pid, @@ -9,13 +10,12 @@ use predicates::prelude::*; use retry::{delay::Fixed, retry}; use serial_test::serial; use std::{ - fs, - net::{IpAddr, Ipv6Addr, Shutdown, SocketAddr, TcpStream}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpStream}, path::PathBuf, process::{Command, Stdio}, time::Duration, }; -use sysinfo::{PidExt, ProcessExt, SystemExt}; +use sysinfo::{ProcessExt, SystemExt}; use wait_timeout::ChildExt; static BIN: Lazy = Lazy::new(|| assert_cmd::cargo::cargo_bin(crate_name!())); @@ -87,7 +87,10 @@ fn test_server_not_running_serial() -> Result<()> { .arg("ping") .assert() .failure() - .stderr(predicate::str::contains("Connection refused")); + .stderr( + predicate::str::contains("Connection refused") + .or(predicate::str::contains("No connection could be made")), + ); Command::new(BIN.as_os_str()) .arg("ping") @@ -95,7 +98,10 @@ fn test_server_not_running_serial() -> Result<()> { .arg("::1") .assert() .failure() - .stderr(predicate::str::contains("Connection refused")); + .stderr( + predicate::str::contains("Connection refused") + .or(predicate::str::contains("No connection could be made")), + ); Command::new(BIN.as_os_str()) .arg("ping") @@ -105,7 +111,8 @@ fn test_server_not_running_serial() -> Result<()> { .failure() .stderr( predicate::str::contains("No route to host") - .or(predicate::str::contains("Network is unreachable")), + .or(predicate::str::contains("Network is unreachable") + .or(predicate::str::contains("unreachable network"))), ); Command::new(BIN.as_os_str()) @@ -114,7 +121,8 @@ fn test_server_not_running_serial() -> Result<()> { .failure() .stderr( predicate::str::contains("Connection refused") - .or(predicate::str::contains("server was already shutdown")), + .or(predicate::str::contains("server was already shutdown") + .or(predicate::str::contains("No connection could be made"))), ); let _ = stop_bin(); @@ -164,7 +172,10 @@ fn test_server_serial() -> Result<()> { .arg("9999") .assert() .failure() - .stderr(predicate::str::contains("Connection refused")); + .stderr( + predicate::str::contains("Connection refused") + .or(predicate::str::contains("No connection could be made")), + ); let _ = Command::new(BIN.as_os_str()).arg("stop").output(); @@ -192,7 +203,7 @@ fn test_workflow_run_serial() -> Result<()> { .arg("start") .arg("--db") .arg("homestar.db") - //.stdout(Stdio::piped()) + .stdout(Stdio::piped()) .spawn() .unwrap(); @@ -257,6 +268,9 @@ fn test_workflow_run_serial() -> Result<()> { #[serial] #[cfg(not(windows))] fn test_daemon_serial() -> Result<()> { + use std::fs; + use sysinfo::PidExt; + let _ = stop_bin(); Command::new(BIN.as_os_str()) @@ -309,3 +323,159 @@ fn test_daemon_serial() -> Result<()> { Ok(()) } + +#[test] +#[serial] +#[cfg(windows)] +fn test_signal_kill_serial() -> Result<()> { + let _ = stop_bin(); + + Command::new(BIN.as_os_str()) + .arg("start") + .arg("--db") + .arg("homestar.db") + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let system = sysinfo::System::new_all(); + let pid = system + .processes_by_exact_name("homestar-runtime.exe") + .collect::>() + .first() + .map(|x| x.pid()) + .unwrap(); + + let socket = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 3030); + let result = retry(Fixed::from_millis(500), || { + TcpStream::connect(socket).map(|stream| stream.shutdown(Shutdown::Both)) + }); + + if result.is_err() { + panic!("Homestar server/runtime failed to start in time"); + } + + Command::new(BIN.as_os_str()) + .arg("ping") + .assert() + .success() + .stdout(predicate::str::contains("::1")) + .stdout(predicate::str::contains("pong")); + + if let Some(process) = system.process(pid) { + process.kill(); + }; + + Command::new(BIN.as_os_str()).arg("ping").assert().failure(); + let _ = stop_bin(); + + Ok(()) +} + +#[test] +#[serial] +#[cfg(windows)] +fn test_server_v4_serial() -> Result<()> { + let _ = stop_bin(); + + let mut homestar_proc = Command::new(BIN.as_os_str()) + .arg("start") + .arg("-c") + .arg("fixtures/test_v4.toml") + .arg("--db") + .arg("homestar.db") + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9999); + let result = retry(Fixed::from_millis(500), || { + TcpStream::connect(socket).map(|stream| stream.shutdown(Shutdown::Both)) + }); + + if result.is_err() { + homestar_proc.kill().unwrap(); + panic!("Homestar server/runtime failed to start in time"); + } + + Command::new(BIN.as_os_str()) + .arg("ping") + .arg("--host") + .arg("127.0.0.1") + .arg("-p") + .arg("9999") + .assert() + .success() + .stdout(predicate::str::contains("127.0.0.1")) + .stdout(predicate::str::contains("pong")); + + let _ = stop_bin(); + + Ok(()) +} + +#[test] +#[serial] +#[cfg(not(windows))] +fn test_daemon_v4_serial() -> Result<()> { + use std::fs; + use sysinfo::PidExt; + + let _ = stop_bin(); + + Command::new(BIN.as_os_str()) + .arg("start") + .arg("-c") + .arg("fixtures/test_v4.toml") + .arg("-d") + .env("DATABASE_URL", "homestar.db") + .stdout(Stdio::piped()) + .assert() + .success(); + + let system = sysinfo::System::new_all(); + let pid = system + .processes_by_exact_name("homestar-runtime") + .collect::>() + .first() + .map(|p| p.pid().as_u32()) + .unwrap_or( + fs::read_to_string("/tmp/homestar.pid") + .expect("Should have a PID file") + .trim() + .parse::() + .unwrap(), + ); + + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9999); + let result = retry(Fixed::from_millis(500), || { + TcpStream::connect(socket).map(|stream| stream.shutdown(Shutdown::Both)) + }); + + if result.is_err() { + panic!("Homestar server/runtime failed to start in time"); + } + + Command::new(BIN.as_os_str()) + .arg("ping") + .arg("--host") + .arg("127.0.0.1") + .arg("-p") + .arg("9999") + .assert() + .success() + .stdout(predicate::str::contains("127.0.0.1")) + .stdout(predicate::str::contains("pong")); + + let _result = signal::kill(Pid::from_raw(pid.try_into().unwrap()), Signal::SIGTERM); + let _result = retry(Fixed::from_millis(500), || { + Command::new(BIN.as_os_str()) + .arg("ping") + .assert() + .try_failure() + }); + + let _ = stop_bin(); + + Ok(()) +}