use std::{ fs::File, io::Read, path::PathBuf, process::{Child, Command, Output}, }; fn signal(pid: u32, sig: &str) { Command::new("kill") .arg("-s") .arg(sig) .arg(format!("{}", pid)) .status() .expect("failed to execute 'kill'"); } pub struct Sway { pub sock: PathBuf, process: Child, } impl Sway { pub fn start() -> Sway { let tmp = std::env::temp_dir() .join("swaysome_tests") .join(std::thread::current().name().unwrap()); std::fs::create_dir_all(&tmp).expect("Unable to create temporary working directory"); let pwd = std::env::current_dir().expect("Unable to get current dir"); let conf_path = pwd.join("tests/sway.conf"); let swaysock_path = tmp.join("swaysock"); let sway_log = File::create(tmp.join("sway.log")).expect("Unable to create sway's log file"); let sway = Command::new("sway") .arg("-c") .arg(conf_path.clone()) .env_clear() .env("WLR_BACKENDS", "headless") .env("WLR_LIBINPUT_NO_DEVICES", "1") .env("XDG_RUNTIME_DIR", &tmp) .env("SWAYSOCK", &swaysock_path) .stderr(sway_log) .spawn() .expect("failed to execute sway"); // check that sway works correctly without using swaysome let sway = Sway { sock: swaysock_path, process: sway, }; match sway.check_connection("loaded_config_file_name") { Ok(()) => { // Let's do some common initialization of the desktop sway.send_command(["create_output"].as_slice()); sway.send_command(["create_output"].as_slice()); return sway; } Err(()) => { eprintln!("Failed to start 'sway', aborting the tests"); eprintln!("---- sway stderr ----"); let mut buffer = String::new(); let mut sway_stderr = File::open(tmp.join("sway.log")).expect("Unable to open sway's log file"); sway_stderr.read_to_string(&mut buffer).unwrap(); eprintln!("{}", buffer); eprintln!("---------------------"); panic!(); } } } pub fn send_command(&self, commands: &[&str]) -> Output { Command::new("swaymsg") .args(commands) .env_clear() .env("SWAYSOCK", self.sock.clone()) .output() .expect("Couldn't run swaymsg") } // work around https://github.com/rust-lang/rust/issues/46379 // TODO: maybe implement that: https://momori.dev/posts/organize-rust-integration-tests-without-dead-code-warning/ #[allow(dead_code)] pub fn spawn_some_apps(&self) { self.send_command(["exec", "foot -T TERM1"].as_slice()); // Make sure the app are created in the right order. // 200ms would still sometimes be racy on my Ryzen 5 PRO 4650U, so let's // take a safe bet and give plenty of time for shared CI runners. std::thread::sleep(std::time::Duration::from_millis(500)); self.send_command(["exec", "foot -T TERM2"].as_slice()); std::thread::sleep(std::time::Duration::from_millis(500)); self.send_command(["exec", "foot -T TERM3"].as_slice()); std::thread::sleep(std::time::Duration::from_millis(500)); } fn check_connection(&self, flag: &str) -> Result<(), ()> { let mut retries = 100; // wait for max 10s while retries > 0 { let version = self.send_command(["-t", "get_version"].as_slice()); if String::from_utf8(version.stdout).unwrap().contains(flag) || String::from_utf8(version.stderr).unwrap().contains(flag) { return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(100)); retries -= 1; } return Err(()); } fn stop(&mut self) { // in case some apps were spawned, kill them, and give them some time to be killed self.send_command(["[all] kill"].as_slice()); std::thread::sleep(std::time::Duration::from_millis(500)); // now terminate sway signal(self.process.id(), "TERM"); match self.check_connection("Unable to connect to") { Ok(()) => eprintln!("Sway terminated correctly on its own"), Err(_) => { self.process.kill().expect("Failed to kill sway"); eprintln!("Sway had to be killed"); } } } } impl Drop for Sway { fn drop(&mut self) { self.stop(); } }