Files
firezone/rust/tests/gui-smoke-test/src/main.rs
Thomas Eizinger 0825055ff2 fix(rust/gui-client): allow GUI process to read the firezone-id file from disk (#6987)
Closes #6989

- The tunnel daemon (IPC service) now explicitly sets the ID file's
perms to 0o640, even if the file already exists.
- The GUI error is now non-fatal. If the file can't be read, we just
won't get the device ID in Sentry.
- More specific error message when the GUI fails to read the ID file

We attempted to set the tunnel daemon's umask, but this caused the smoke
tests to fail. Fixing the regression is more urgent than getting the
smoke tests to match local debugging.

---------

Co-authored-by: _ <ReactorScram@users.noreply.github.com>
2024-10-09 20:04:24 +00:00

234 lines
6.2 KiB
Rust

// Invoke with `cargo run --bin gui-smoke-test`
//
// Starts up the IPC service and GUI app and lets them run for a bit
use anyhow::{bail, Context as _, Result};
use clap::Parser;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use subprocess::Exec;
#[cfg(target_os = "linux")]
const FZ_GROUP: &str = "firezone-client";
const GUI_NAME: &str = "firezone-gui-client";
const IPC_NAME: &str = "firezone-client-ipc";
#[cfg(target_os = "linux")]
const EXE_EXTENSION: &str = "";
#[cfg(target_os = "windows")]
const EXE_EXTENSION: &str = "exe";
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Run tests that can't run in CI, like tests that need access to the staging network.
#[arg(long)]
manual_tests: bool,
}
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
tracing::info!("Started logging");
let cli = Cli::try_parse()?;
let app = App::new()?;
dump_syms()?;
// Run normal smoke test
let mut ipc_service = ipc_service_command().arg("run-smoke-test").popen()?;
let mut gui = app
.gui_command(&["smoke-test"])? // Disable deep links because they don't work in the headless CI environment
.popen()?;
gui.wait()?.fz_exit_ok().context("GUI process")?;
ipc_service.wait()?.fz_exit_ok().context("IPC service")?;
// Force the GUI to crash
let mut ipc_service = ipc_service_command().arg("run-smoke-test").popen()?;
let mut gui = app.gui_command(&["--crash"])?.popen()?;
// Ignore exit status here since we asked the GUI to crash on purpose
gui.wait()?;
ipc_service.wait()?.fz_exit_ok().context("IPC service")?;
if cli.manual_tests {
manual_tests(&app)?;
}
Ok(())
}
fn manual_tests(app: &App) -> Result<()> {
// Replicate #6791
app.gui_command(&["debug", "replicate6791"])?
.popen()?
.wait()?;
let mut ipc_service = ipc_service_command().arg("run-smoke-test").popen()?;
let mut gui = app.gui_command(&["--quit-after", "10"])?.popen()?;
// Expect exit codes of 0
gui.wait()?.fz_exit_ok().context("GUI process")?;
ipc_service.wait()?.fz_exit_ok().context("IPC service")?;
Ok(())
}
struct App {
#[cfg(target_os = "linux")]
username: String,
}
#[cfg(target_os = "linux")]
impl App {
fn new() -> Result<Self> {
// Needed to manipulate the group membership inside CI
let username = std::env::var("USER")?;
// Create the firezone group if needed
Exec::cmd("sudo")
.args(&[
"groupadd", "--force", // Exit with success if the group already exists
FZ_GROUP,
])
.join()?
.fz_exit_ok()?;
// Add ourself to the firezone group
Exec::cmd("sudo")
.args(&["usermod", "--append", "--groups", FZ_GROUP, &username])
.join()?
.fz_exit_ok()?;
Ok(Self { username })
}
// `args` can't just be appended because of the `xvfb-run` wrapper
fn gui_command(&self, args: &[&str]) -> Result<Exec> {
let gui_path = gui_path().canonicalize()?;
let args: Vec<_> = [
"--auto-servernum",
gui_path
.to_str()
.context("Should be able to convert Path to &str")?, // For some reason `xvfb-run` doesn't just use our current working dir
"--no-deep-links",
]
.into_iter()
.chain(args.iter().copied())
.collect();
let xvfb = Exec::cmd("xvfb-run").args(&args).to_cmdline_lossy();
tracing::debug!(?xvfb);
let cmd = Exec::cmd("sudo") // We need `sudo` to run `su`
.args(&[
"--preserve-env",
"su", // We need `su` to get a login shell as ourself
"--login", // And we need a login shell so that the group membership will take effect immediately
"--whitelist-environment=XDG_RUNTIME_DIR",
&self.username,
"--command",
&xvfb,
])
.env("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); // Might help with CI
Ok(cmd)
}
}
#[cfg(target_os = "windows")]
impl App {
fn new() -> Result<Self> {
Ok(Self {})
}
// Strange signature needed to match Linux
fn gui_command(&self, args: &[&str]) -> Result<Exec> {
Ok(Exec::cmd(gui_path()).arg("--no-deep-links").args(args))
}
}
// Get debug symbols from the exe / pdb
fn dump_syms() -> Result<()> {
Exec::cmd("dump_syms")
.args(&[
debug_db_path().as_os_str(),
gui_path().as_os_str(),
OsStr::new("--output"),
syms_path().as_os_str(),
])
.join()?
.fz_exit_ok()?;
Ok(())
}
#[cfg(target_os = "linux")]
fn debug_db_path() -> PathBuf {
Path::new("target").join("debug").join(GUI_NAME)
}
#[cfg(target_os = "windows")]
fn debug_db_path() -> PathBuf {
Path::new("target")
.join("debug")
.join("firezone_gui_client.pdb")
}
#[cfg(target_os = "linux")]
fn ipc_service_command() -> Exec {
Exec::cmd("sudo").args(&[
"--preserve-env",
"runuser", // The `runuser` looks redundant but CI will complain if we use `sudo` directly, not sure why
"-u",
"root",
"--group",
"firezone-client",
"--whitelist-environment=RUST_LOG",
ipc_path()
.to_str()
.expect("IPC binary path should be valid Unicode"),
])
}
#[cfg(target_os = "windows")]
fn ipc_service_command() -> Exec {
Exec::cmd(ipc_path())
}
// `ExitStatus::exit_ok` is nightly, so we add an equivalent here
trait ExitStatusExt {
fn fz_exit_ok(&self) -> Result<()>;
}
impl ExitStatusExt for subprocess::ExitStatus {
fn fz_exit_ok(&self) -> Result<()> {
if !self.success() {
bail!("Subprocess should exit with success");
}
Ok(())
}
}
fn gui_path() -> PathBuf {
Path::new("target")
.join("debug")
.join(GUI_NAME)
.with_extension(EXE_EXTENSION)
}
fn ipc_path() -> PathBuf {
Path::new("target")
.join("debug")
.join(IPC_NAME)
.with_extension(EXE_EXTENSION)
}
fn syms_path() -> PathBuf {
gui_path().with_extension("syms")
}