mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
`--language CODE` sets the interface language for one run of --active, --freeze, --daemon or --about without changing config.toml. It accepts the spellings `[ui] language` does and takes precedence over both WAYSCRIBER_LANGUAGE and the config. A daemon started with it gives it to the About windows it opens, and to its overlays as an optional environment input that an older overlay ignores. The process broker strips that input from every other helper, so a configurator opened from an overlay keeps previewing its own Language row. With --daemon-toggle or an overlay action the flag is an error, since a running daemon's overlays cannot be changed from outside. `--list-languages` prints each language's code and name, the configured language, and the language in effect with the setting that decided it. Languages now carry their native and English names, and the configurator's Language row takes its labels from them. The help text, README, CONFIG.md, I18N.md and config.example.toml describe both options.
1407 lines
44 KiB
Rust
1407 lines
44 KiB
Rust
use std::ffi::OsStr;
|
|
use std::os::fd::AsRawFd;
|
|
use std::os::unix::process::CommandExt;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use super::*;
|
|
|
|
fn wire_arguments(arguments: &[&str]) -> Vec<super::wire::OsWire> {
|
|
arguments
|
|
.iter()
|
|
.map(|argument| super::wire::OsWire::from_os(OsStr::new(argument)).unwrap())
|
|
.collect()
|
|
}
|
|
|
|
fn release_test_provider(
|
|
release_path: &std::path::Path,
|
|
proof_path: &std::path::Path,
|
|
provider_pid: i32,
|
|
) {
|
|
std::fs::write(release_path, b"release").unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
loop {
|
|
if std::fs::read(proof_path).is_ok_and(|bytes| bytes == b"survived") {
|
|
return;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
// SAFETY: cleanup keeps a failing regression test from leaking its provider.
|
|
unsafe {
|
|
libc::kill(provider_pid, libc::SIGKILL);
|
|
}
|
|
panic!("successful provider could not act after normal broker shutdown");
|
|
}
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn configurator_manifest_preserves_arbitrary_explicit_override_name() {
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let configured = crate::test_fake_helper::link(temp.path(), "open-wayscriber-settings");
|
|
let output = temp.path().join("unused");
|
|
let mut variables = vec![(
|
|
crate::env_vars::CONFIGURATOR_ENV,
|
|
Some(configured.as_os_str()),
|
|
)];
|
|
variables.extend(crate::test_fake_helper::environment(
|
|
&configured,
|
|
crate::test_fake_helper::Role::Exit,
|
|
&output,
|
|
));
|
|
|
|
let (result, broker_result, unexpected_result) =
|
|
crate::test_env::with_env_vars(&variables, || {
|
|
let program = super::wire::OsWire::from_os(configured.as_os_str()).unwrap();
|
|
let result =
|
|
super::manifest::validate(HelperKind::Configurator, &program, &[], &[], &[]);
|
|
let broker_result = (|| -> anyhow::Result<()> {
|
|
let guard = start_for_runtime()?;
|
|
guard.broker().spawn(
|
|
HelperKind::Configurator,
|
|
HelperLifetime::DetachedAfterExec,
|
|
configured.as_os_str(),
|
|
std::iter::empty::<&OsStr>(),
|
|
Vec::new(),
|
|
)?;
|
|
Ok(())
|
|
})();
|
|
let unexpected =
|
|
super::wire::OsWire::from_os(OsStr::new("/tmp/unrelated-program")).unwrap();
|
|
let unexpected_result =
|
|
super::manifest::validate(HelperKind::Configurator, &unexpected, &[], &[], &[]);
|
|
|
|
(result, broker_result, unexpected_result)
|
|
});
|
|
|
|
result.unwrap();
|
|
broker_result.unwrap();
|
|
assert!(unexpected_result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn update_fetcher_manifest_allows_only_curl_and_wget() {
|
|
for (program, arguments) in [
|
|
("/usr/bin/curl", &["--disable"][..]),
|
|
("/usr/bin/wget", &["--no-config"][..]),
|
|
] {
|
|
let program = super::wire::OsWire::from_os(OsStr::new(program)).unwrap();
|
|
super::manifest::validate(
|
|
HelperKind::UpdateFetcher,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let unrelated = super::wire::OsWire::from_os(OsStr::new("/usr/bin/sh")).unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::UpdateFetcher,
|
|
&unrelated,
|
|
&wire_arguments(&["--disable"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err()
|
|
);
|
|
|
|
for (program, arguments) in [
|
|
("curl", &[][..]),
|
|
("curl", &["--silent", "--disable"][..]),
|
|
("wget", &[][..]),
|
|
("wget", &["--quiet", "--no-config"][..]),
|
|
("curl", &["--disable", "--config", "/tmp/curlrc"][..]),
|
|
("curl", &["--disable", "--config=/tmp/curlrc"][..]),
|
|
("curl", &["--disable", "--conf"][..]),
|
|
("curl", &["--disable", "-K", "/tmp/curlrc"][..]),
|
|
("curl", &["--disable", "-K/tmp/curlrc"][..]),
|
|
("curl", &["--disable", "-sK/path"][..]),
|
|
("wget", &["--no-config", "--config=/tmp/wgetrc"][..]),
|
|
("wget", &["--no-config", "--conf"][..]),
|
|
("wget", &["--no-config", "--execute=header=X:1"][..]),
|
|
("wget", &["--no-config", "--execute", "header=X:1"][..]),
|
|
("wget", &["--no-config", "--exec=header=X:1"][..]),
|
|
("wget", &["--no-config", "-e", "header=X:1"][..]),
|
|
("wget", &["--no-config", "-eheader=X:1"][..]),
|
|
("wget", &["--no-config", "-qeheader=X:1"][..]),
|
|
] {
|
|
let program = super::wire::OsWire::from_os(OsStr::new(program)).unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::UpdateFetcher,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err(),
|
|
"{program:?} accepted unsafe arguments {arguments:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn swaymsg_manifest_allows_only_tree_queries() {
|
|
let program = super::wire::OsWire::from_os(OsStr::new("/usr/bin/swaymsg")).unwrap();
|
|
super::manifest::validate(
|
|
HelperKind::Swaymsg,
|
|
&program,
|
|
&wire_arguments(&["-t", "get_tree", "-r"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
|
|
for arguments in [
|
|
&[][..],
|
|
&["-t", "get_outputs", "-r"][..],
|
|
&["-t", "command", "exit"][..],
|
|
&["-r", "-t", "get_tree"][..],
|
|
] {
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Swaymsg,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err(),
|
|
"swaymsg accepted arguments {arguments:?}"
|
|
);
|
|
}
|
|
|
|
let unrelated = super::wire::OsWire::from_os(OsStr::new("/usr/bin/sh")).unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Swaymsg,
|
|
&unrelated,
|
|
&wire_arguments(&["-t", "get_tree", "-r"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Swaymsg,
|
|
&program,
|
|
&wire_arguments(&["-t", "get_tree", "-r"]),
|
|
&[],
|
|
b"command payload",
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hyprctl_window_geometry_manifest_allows_only_clients_and_monitors_json_queries() {
|
|
let program = super::wire::OsWire::from_os(OsStr::new("/usr/bin/hyprctl")).unwrap();
|
|
for arguments in [&["clients", "-j"][..], &["monitors", "-j"][..]] {
|
|
super::manifest::validate(
|
|
HelperKind::Hyprctl,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
for arguments in [
|
|
&[][..],
|
|
&["clients"][..],
|
|
&["activewindow", "-j"][..],
|
|
&["-j", "clients"][..],
|
|
&["clients", "-j", "extra"][..],
|
|
&["dispatch", "exit"][..],
|
|
] {
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Hyprctl,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err(),
|
|
"window-geometry hyprctl accepted arguments {arguments:?}"
|
|
);
|
|
}
|
|
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Hyprctl,
|
|
&program,
|
|
&wire_arguments(&["clients", "-j"]),
|
|
&[],
|
|
b"command payload",
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn active_window_hyprctl_manifest_preserves_only_its_existing_queries() {
|
|
let program = super::wire::OsWire::from_os(OsStr::new("hyprctl")).unwrap();
|
|
for arguments in [&["activewindow", "-j"][..], &["monitors", "-j"][..]] {
|
|
super::manifest::validate(
|
|
HelperKind::HyprctlActiveWindow,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
for arguments in [
|
|
&["clients", "-j"][..],
|
|
&["activewindow"][..],
|
|
&["activewindow", "-j", "extra"][..],
|
|
] {
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::HyprctlActiveWindow,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err(),
|
|
"active-window hyprctl accepted arguments {arguments:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn desktop_open_manifest_accepts_one_path_or_trusted_url_without_a_shell() {
|
|
for target in [
|
|
"/tmp/Wayscriber Captures",
|
|
"https://wayscriber.com/report#d=abc",
|
|
"https://www.wayscriber.com/docs/",
|
|
] {
|
|
let program = super::wire::OsWire::from_os(OsStr::new("xdg-open")).unwrap();
|
|
super::manifest::validate(
|
|
HelperKind::DesktopOpen,
|
|
&program,
|
|
&wire_arguments(&[target]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
for target in [
|
|
"http://wayscriber.com/report",
|
|
"https://wayscriber.com.example/report",
|
|
"https://example.com/",
|
|
"file:///etc/passwd",
|
|
"--help",
|
|
] {
|
|
let program = super::wire::OsWire::from_os(OsStr::new("xdg-open")).unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::DesktopOpen,
|
|
&program,
|
|
&wire_arguments(&[target]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err(),
|
|
"desktop-open accepted {target:?}"
|
|
);
|
|
}
|
|
|
|
let opener = super::wire::OsWire::from_os(OsStr::new("xdg-open")).unwrap();
|
|
assert!(super::manifest::validate(HelperKind::DesktopOpen, &opener, &[], &[], &[]).is_err());
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::DesktopOpen,
|
|
&opener,
|
|
&wire_arguments(&["/tmp/one", "/tmp/two"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err()
|
|
);
|
|
|
|
let shell = super::wire::OsWire::from_os(OsStr::new("cmd")).unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::DesktopOpen,
|
|
&shell,
|
|
&wire_arguments(&["/tmp/file"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err()
|
|
);
|
|
|
|
// Undecodable URI-shaped targets must not skip the trusted-host gate.
|
|
let mut evil = b"https://evil.example/".to_vec();
|
|
evil.push(0x80);
|
|
let opener = super::wire::OsWire::from_os(OsStr::new("xdg-open")).unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::DesktopOpen,
|
|
&opener,
|
|
&[super::wire::OsWire(evil)],
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn systemctl_manifest_requires_the_user_service_manager() {
|
|
let program = super::wire::OsWire::from_os(OsStr::new("systemctl")).unwrap();
|
|
super::manifest::validate(
|
|
HelperKind::Systemctl,
|
|
&program,
|
|
&wire_arguments(&["--user", "daemon-reload"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Systemctl,
|
|
&program,
|
|
&wire_arguments(&["daemon-reload"]),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err()
|
|
);
|
|
for arguments in [
|
|
&["--user", "--system", "daemon-reload"][..],
|
|
&["--user", "--syst", "daemon-reload"][..],
|
|
&["--user", "--global", "enable", "wayscriber.service"][..],
|
|
&["--user", "--glob", "enable", "wayscriber.service"][..],
|
|
&["--user", "--machine=.host", "daemon-reload"][..],
|
|
&["--user", "--machine", ".host", "daemon-reload"][..],
|
|
&["--user", "--mach=.host", "daemon-reload"][..],
|
|
&["--user", "-M", ".host", "daemon-reload"][..],
|
|
&["--user", "-M.host", "daemon-reload"][..],
|
|
] {
|
|
assert!(
|
|
super::manifest::validate(
|
|
HelperKind::Systemctl,
|
|
&program,
|
|
&wire_arguments(arguments),
|
|
&[],
|
|
&[],
|
|
)
|
|
.is_err(),
|
|
"systemctl accepted non-user manager flags {arguments:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tesseract_manifest_allows_only_the_tesseract_basename() {
|
|
for program in ["/usr/bin/tesseract", "/usr/local/bin/tesseract"] {
|
|
let program = super::wire::OsWire::from_os(OsStr::new(program)).unwrap();
|
|
super::manifest::validate(HelperKind::Tesseract, &program, &[], &[], &[]).unwrap();
|
|
}
|
|
|
|
for program in ["/usr/bin/sh", "/usr/bin/tesseract-wrapper", "/usr/bin/grim"] {
|
|
let program = super::wire::OsWire::from_os(OsStr::new(program)).unwrap();
|
|
assert!(
|
|
super::manifest::validate(HelperKind::Tesseract, &program, &[], &[], &[]).is_err(),
|
|
"{program:?} must not be allowed for OCR"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tesseract_reads_the_complete_recognized_output_rather_than_a_prefix() {
|
|
// Prefix mode stops the helper once the cap fills; OCR must instead see the
|
|
// cap as a distinct "too much text" failure, which needs Complete mode.
|
|
assert!(!super::manifest::supports_prefix_output(
|
|
HelperKind::Tesseract
|
|
));
|
|
assert_eq!(
|
|
super::manifest::input_cap(HelperKind::Tesseract),
|
|
super::wire::MAX_INPUT_BYTES
|
|
);
|
|
}
|
|
|
|
/// A launcher click must not stall behind a long helper, and nothing else may
|
|
/// be made to fail by that policy.
|
|
///
|
|
/// `try_spawn` is issued from the Wayland callback thread while OCR keeps the
|
|
/// overlay interactive, so queueing behind a thirty-second recognition would
|
|
/// hold input and redraw dispatch for its whole run. Plain `spawn` — the
|
|
/// daemon's overlay start, the tray, startup detach — must keep queueing, or an
|
|
/// unrelated background helper would turn a short wait into a spurious failure
|
|
/// and a retry backoff.
|
|
#[test]
|
|
fn only_the_callback_thread_spawn_declines_to_wait_for_the_transport() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let broker = guard.broker().clone();
|
|
let (running_tx, running_rx) = std::sync::mpsc::channel();
|
|
|
|
let long_helper = std::thread::spawn(move || {
|
|
running_tx.send(()).unwrap();
|
|
broker.run(
|
|
HelperKind::TestSleep,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("1")],
|
|
Vec::new(),
|
|
Duration::from_secs(10),
|
|
1024,
|
|
)
|
|
});
|
|
running_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
|
// The helper thread has to hold the exchange before the policy is testable.
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
|
|
let started = Instant::now();
|
|
let declined = guard.broker().try_spawn(
|
|
HelperKind::TestSleep,
|
|
HelperLifetime::OperationBound,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("0")],
|
|
Vec::new(),
|
|
);
|
|
let waited = started.elapsed();
|
|
|
|
let error = declined.expect_err("try_spawn must not queue behind the long helper");
|
|
assert!(
|
|
crate::process_broker::error_kind(&error)
|
|
== Some(crate::process_broker::BrokerErrorKind::Busy),
|
|
"unexpected try_spawn failure: {error:#}"
|
|
);
|
|
assert!(
|
|
waited < Duration::from_millis(250),
|
|
"try_spawn slept for {waited:?} instead of declining"
|
|
);
|
|
|
|
// The queueing path waits it out and still succeeds.
|
|
guard
|
|
.broker()
|
|
.spawn(
|
|
HelperKind::TestSleep,
|
|
HelperLifetime::OperationBound,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("0")],
|
|
Vec::new(),
|
|
)
|
|
.expect("spawn must queue behind the long helper rather than fail");
|
|
|
|
let _ = long_helper.join();
|
|
}
|
|
|
|
#[test]
|
|
fn prelock_broker_runs_bounded_helpers_and_owns_reaping() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let output = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::TestSleep,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("0")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
1024,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
assert!(!output.timed_out);
|
|
|
|
let child = guard
|
|
.broker()
|
|
.spawn(
|
|
HelperKind::TestSleep,
|
|
HelperLifetime::OwnedChild,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("30")],
|
|
Vec::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(child.try_wait().unwrap().is_none());
|
|
child.signal(libc::SIGTERM).unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
while child.try_wait().unwrap().is_none() {
|
|
assert!(Instant::now() < deadline);
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn process_group_guard_cleans_up_before_ownership_transfer() {
|
|
let mut command = std::process::Command::new("sleep");
|
|
command.arg("30").process_group(0);
|
|
let child = super::execution::OwnedProcess::process_group(command.spawn().unwrap());
|
|
let pid = i32::try_from(child.id()).unwrap();
|
|
|
|
drop(child);
|
|
|
|
// SAFETY: signal zero only probes the test-owned helper PID after guard cleanup.
|
|
assert_ne!(unsafe { libc::kill(pid, 0) }, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn initial_detach_child_remains_eligible_to_create_a_session() {
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let helper = crate::test_fake_helper::link(temp.path(), "wayscriber-detach-probe");
|
|
let proof = temp.path().join("detach-state");
|
|
let variables = crate::test_fake_helper::environment(
|
|
&helper,
|
|
crate::test_fake_helper::Role::ReportProcessGroup,
|
|
&proof,
|
|
);
|
|
|
|
let observed = crate::test_env::with_env_vars(&variables, || {
|
|
let guard = start_for_runtime().unwrap();
|
|
let _child = guard
|
|
.broker()
|
|
.spawn(
|
|
HelperKind::InitialDetach,
|
|
HelperLifetime::DetachedAfterExec,
|
|
helper.as_os_str(),
|
|
std::iter::empty::<&OsStr>(),
|
|
Vec::new(),
|
|
)
|
|
.unwrap();
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
loop {
|
|
if let Ok(value) = std::fs::read_to_string(&proof) {
|
|
break value;
|
|
}
|
|
assert!(Instant::now() < deadline, "detach probe did not complete");
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
});
|
|
|
|
assert_eq!(observed, "session-eligible");
|
|
}
|
|
|
|
#[test]
|
|
fn broker_rejects_cross_kind_programs_and_enforces_timeout() {
|
|
let guard = start_for_runtime().unwrap();
|
|
assert!(
|
|
guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::Grim,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("0")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
1024,
|
|
)
|
|
.is_err()
|
|
);
|
|
let timed = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::TestSleep,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("30")],
|
|
Vec::new(),
|
|
Duration::from_millis(20),
|
|
1024,
|
|
)
|
|
.unwrap();
|
|
assert!(timed.timed_out);
|
|
}
|
|
|
|
#[test]
|
|
fn broker_transfers_large_input_and_output_through_sealed_memfds() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let input = (0..(128 * 1024))
|
|
.map(|index| (index % 251) as u8)
|
|
.collect::<Vec<_>>();
|
|
let output = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::TestCat,
|
|
OsStr::new("cat"),
|
|
std::iter::empty::<&OsStr>(),
|
|
input.clone(),
|
|
Duration::from_secs(2),
|
|
input.len(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
assert!(!output.timed_out);
|
|
assert_eq!(output.stdout, input);
|
|
assert!(output.stderr.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn broker_transfers_capture_output_beyond_the_legacy_limit() {
|
|
const CAPTURE_BYTES: usize = 16 * 1024 * 1024 + 1;
|
|
let guard = start_for_runtime().unwrap();
|
|
let output = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[OsStr::new("-c"), OsStr::new("head -c 16777217 /dev/zero")],
|
|
Vec::new(),
|
|
Duration::from_secs(5),
|
|
CAPTURE_BYTES,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
assert_eq!(output.stdout.len(), CAPTURE_BYTES);
|
|
}
|
|
|
|
#[test]
|
|
fn broker_rejects_output_that_exceeds_the_requested_cap() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let result = guard.broker().run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[OsStr::new("-c"), OsStr::new("printf 12345")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
4,
|
|
);
|
|
assert!(result.is_err(), "broker returned silently truncated output");
|
|
}
|
|
|
|
#[test]
|
|
fn broker_stops_an_endless_stream_when_it_reaches_the_output_cap() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let started = Instant::now();
|
|
let result = guard.broker().run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new("while :; do printf 1234567890; done"),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(5),
|
|
1024,
|
|
);
|
|
|
|
assert!(result.is_err(), "broker accepted an endless response");
|
|
assert!(
|
|
started.elapsed() < Duration::from_secs(2),
|
|
"broker waited for the operation timeout instead of stopping at the cap"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn broker_prefix_read_returns_the_requested_prefix_without_weakening_strict_runs() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let output = guard
|
|
.broker()
|
|
.run_prefix(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[OsStr::new("-c"), OsStr::new("printf 123456789")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
5,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(output.stdout, b"12345");
|
|
assert!(output.stdout_limit_reached);
|
|
assert!(!output.timed_out);
|
|
assert!(
|
|
guard
|
|
.broker()
|
|
.run_prefix(
|
|
HelperKind::TestCat,
|
|
OsStr::new("cat"),
|
|
std::iter::empty::<&OsStr>(),
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
5,
|
|
)
|
|
.is_err(),
|
|
"prefix output must stay restricted to wl-paste"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn broker_guard_preempts_an_active_operation_and_kills_its_group() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let broker = guard.broker().clone();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let pid_path = temp.path().join("helper.pid");
|
|
let run = std::thread::spawn({
|
|
let pid_path = pid_path.clone();
|
|
move || {
|
|
broker.run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new("echo $$ > \"$1\"; sleep 30"),
|
|
OsStr::new("sh"),
|
|
pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2),
|
|
1024,
|
|
)
|
|
}
|
|
});
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
while !pid_path.exists() {
|
|
assert!(Instant::now() < deadline, "helper did not start");
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
let helper_pid = std::fs::read_to_string(&pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
|
|
let started = Instant::now();
|
|
drop(guard);
|
|
assert!(
|
|
started.elapsed() < Duration::from_millis(500),
|
|
"broker shutdown waited for the active operation"
|
|
);
|
|
assert!(run.join().unwrap().is_err());
|
|
// SAFETY: signal zero only probes the test-owned helper PID.
|
|
assert_ne!(unsafe { libc::kill(helper_pid, 0) }, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let watchdog = crate::daemon::protocol_v2::open_daemon_watchdog().unwrap();
|
|
let child = guard
|
|
.broker()
|
|
.spawn_with_watchdog(
|
|
HelperKind::TestSleep,
|
|
HelperLifetime::OwnedChild,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("30")],
|
|
Vec::new(),
|
|
watchdog.as_raw_fd(),
|
|
)
|
|
.unwrap();
|
|
let inherited_pidfd = std::fs::read_dir(format!("/proc/{}/fd", child.id()))
|
|
.unwrap()
|
|
.filter_map(Result::ok)
|
|
.filter_map(|entry| std::fs::read_link(entry.path()).ok())
|
|
.any(|target| target.as_os_str() == "anon_inode:[pidfd]");
|
|
assert!(inherited_pidfd, "owned child should retain daemon pidfd");
|
|
child.kill_wait().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn helpers_do_not_inherit_the_internal_process_markers() {
|
|
const MARKERS: [&str; 4] = [
|
|
crate::env_vars::OVERLAY_CHILD_GENERATION_ENV,
|
|
crate::env_vars::DETACHED_ENV,
|
|
crate::RESUME_SESSION_ENV,
|
|
crate::env_vars::OVERLAY_LANGUAGE_ENV,
|
|
];
|
|
let _environment = crate::test_env::lock();
|
|
let previous = MARKERS.map(std::env::var_os);
|
|
for marker in MARKERS {
|
|
// SAFETY: serialized by the test environment mutex held above.
|
|
unsafe { std::env::set_var(marker, "inherited") };
|
|
}
|
|
|
|
let guard = start_for_runtime().unwrap();
|
|
let script = MARKERS
|
|
.map(|marker| format!("${{{marker}-unset}}"))
|
|
.join(",");
|
|
let output = guard.broker().run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new(&format!("printf '%s' \"{script}\"")),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(5),
|
|
1024,
|
|
);
|
|
drop(guard);
|
|
for (marker, value) in MARKERS.into_iter().zip(previous) {
|
|
// SAFETY: serialized by the test environment mutex held above.
|
|
unsafe {
|
|
match value {
|
|
Some(value) => std::env::set_var(marker, value),
|
|
None => std::env::remove_var(marker),
|
|
}
|
|
}
|
|
}
|
|
|
|
let output = output.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
assert_eq!(output.stdout, b"unset,unset,unset,unset");
|
|
}
|
|
|
|
#[test]
|
|
fn operation_bound_run_terminates_descendants_that_retain_pipes() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let started = Instant::now();
|
|
let output = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[OsStr::new("-c"), OsStr::new("sleep 30 & echo $!")],
|
|
Vec::new(),
|
|
Duration::from_secs(2),
|
|
1024,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
assert!(!output.timed_out);
|
|
assert!(started.elapsed() < Duration::from_secs(1));
|
|
assert!(
|
|
std::str::from_utf8(&output.stdout)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.is_ok()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_descendant_outside_the_helper_group_cannot_hold_the_broker_on_its_pipes() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let pid_path = temp.path().join("descendant.pid");
|
|
// The helper waits until the descendant has written its pid, which it
|
|
// does only after setsid(). Exiting sooner lets the group kill reach the
|
|
// descendant before it leaves the group, and then nothing holds the pipes.
|
|
let script = format!(
|
|
"setsid sh -c 'echo $$ > \"$0\"; exec sleep 30' '{path}' & \
|
|
while [ ! -s '{path}' ]; do sleep 0.01; done",
|
|
path = pid_path.display()
|
|
);
|
|
|
|
let started = Instant::now();
|
|
let result = guard.broker().run(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[OsStr::new("-c"), OsStr::new(&script)],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
1024,
|
|
);
|
|
let elapsed = started.elapsed();
|
|
if let Some(pid) = std::fs::read_to_string(&pid_path)
|
|
.ok()
|
|
.and_then(|pid| pid.trim().parse::<libc::pid_t>().ok())
|
|
{
|
|
// SAFETY: cleanup of the descendant this test started in its own session.
|
|
unsafe {
|
|
libc::kill(pid, libc::SIGKILL);
|
|
}
|
|
}
|
|
|
|
let error = result.expect_err("output still held open must not read as complete");
|
|
assert!(
|
|
format!("{error:#}").contains("pipe is still open"),
|
|
"{error:#}"
|
|
);
|
|
assert!(elapsed < Duration::from_secs(5), "{elapsed:?}");
|
|
assert!(guard.broker().is_healthy());
|
|
guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::TestSleep,
|
|
OsStr::new("sleep"),
|
|
[OsStr::new("0")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
1024,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn normal_broker_shutdown_releases_successful_provider_descendant() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let release_path = temp.path().join("release-provider");
|
|
let proof_path = temp.path().join("provider-survived");
|
|
let pid_path = temp.path().join("provider.pid");
|
|
let output = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new(
|
|
"(while [ ! -e \"$1\" ]; do sleep 0.01; done; printf survived > \"$2\") & echo $! > \"$3\"",
|
|
),
|
|
OsStr::new("sh"),
|
|
release_path.as_os_str(),
|
|
proof_path.as_os_str(),
|
|
pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2)
|
|
)
|
|
.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
assert!(!output.timed_out);
|
|
let provider_pid = std::fs::read_to_string(pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
// SAFETY: signal zero only checks the test-owned provider.
|
|
assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0);
|
|
|
|
drop(guard);
|
|
|
|
release_test_provider(&release_path, &proof_path, provider_pid);
|
|
}
|
|
|
|
#[test]
|
|
fn shutdown_channel_peer_loss_kills_retained_provider() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let pid_path = temp.path().join("provider.pid");
|
|
let output = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new("sleep 30 & echo $! > \"$1\""),
|
|
OsStr::new("sh"),
|
|
pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(output.status, 0);
|
|
let provider_pid = std::fs::read_to_string(pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
// SAFETY: signal zero only checks the test-owned provider.
|
|
assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0);
|
|
|
|
// Simulate abrupt parent loss without writing the graceful-shutdown packet.
|
|
// SAFETY: the descriptor belongs to this test's live broker guard.
|
|
assert_eq!(
|
|
unsafe { libc::shutdown(guard.broker().inner.shutdown.as_raw_fd(), libc::SHUT_RDWR) },
|
|
0
|
|
);
|
|
drop(guard);
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
loop {
|
|
// SAFETY: signal zero only probes the recorded test provider PID.
|
|
if unsafe { libc::kill(provider_pid, 0) } != 0 {
|
|
break;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
// SAFETY: cleanup keeps a failing regression test from leaking its provider.
|
|
unsafe {
|
|
libc::kill(provider_pid, libc::SIGKILL);
|
|
}
|
|
panic!("provider survived abnormal broker channel loss");
|
|
}
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn retained_publication_replacement_disposes_the_previous_provider() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let first_pid_path = temp.path().join("first-provider.pid");
|
|
let second_pid_path = temp.path().join("second-provider.pid");
|
|
let second_release_path = temp.path().join("release-second-provider");
|
|
let second_proof_path = temp.path().join("second-provider-survived");
|
|
let first = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new("sleep 30 & echo $! > \"$1\""),
|
|
OsStr::new("sh"),
|
|
first_pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(first.status, 0);
|
|
let second = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new(
|
|
"(while [ ! -e \"$1\" ]; do sleep 0.01; done; printf survived > \"$2\") & echo $! > \"$3\"",
|
|
),
|
|
OsStr::new("sh"),
|
|
second_release_path.as_os_str(),
|
|
second_proof_path.as_os_str(),
|
|
second_pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2)
|
|
)
|
|
.unwrap();
|
|
assert_eq!(second.status, 0);
|
|
|
|
let first_pid = std::fs::read_to_string(first_pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
let second_pid = std::fs::read_to_string(second_pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
loop {
|
|
// SAFETY: signal zero only probes the recorded test provider PID.
|
|
if unsafe { libc::kill(first_pid, 0) } != 0 {
|
|
break;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
// SAFETY: cleanup keeps a failing regression test from leaking its helpers.
|
|
unsafe {
|
|
libc::kill(first_pid, libc::SIGKILL);
|
|
libc::kill(second_pid, libc::SIGKILL);
|
|
}
|
|
panic!("replaced publication provider remained alive");
|
|
}
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
// SAFETY: signal zero only checks the current test-owned provider.
|
|
assert_eq!(unsafe { libc::kill(second_pid, 0) }, 0);
|
|
|
|
drop(guard);
|
|
release_test_provider(&second_release_path, &second_proof_path, second_pid);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_publication_replacement_preserves_the_current_provider() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let current_pid_path = temp.path().join("current-provider.pid");
|
|
let failed_pid_path = temp.path().join("failed-provider.pid");
|
|
let current_release_path = temp.path().join("release-current-provider");
|
|
let current_proof_path = temp.path().join("current-provider-survived");
|
|
let current = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new(
|
|
"(while [ ! -e \"$1\" ]; do sleep 0.01; done; printf survived > \"$2\") & echo $! > \"$3\"",
|
|
),
|
|
OsStr::new("sh"),
|
|
current_release_path.as_os_str(),
|
|
current_proof_path.as_os_str(),
|
|
current_pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2)
|
|
)
|
|
.unwrap();
|
|
assert_eq!(current.status, 0);
|
|
|
|
let failed = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new("sleep 30 & echo $! > \"$1\"; exit 7"),
|
|
OsStr::new("sh"),
|
|
failed_pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(failed.status, 7);
|
|
|
|
let current_pid = std::fs::read_to_string(current_pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
let failed_pid = std::fs::read_to_string(failed_pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
// SAFETY: signal zero only checks the current test-owned provider.
|
|
assert_eq!(unsafe { libc::kill(current_pid, 0) }, 0);
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
loop {
|
|
// SAFETY: signal zero only probes the failed test provider PID.
|
|
if unsafe { libc::kill(failed_pid, 0) } != 0 {
|
|
break;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
// SAFETY: cleanup keeps a failing regression test from leaking its helpers.
|
|
unsafe {
|
|
libc::kill(current_pid, libc::SIGKILL);
|
|
libc::kill(failed_pid, libc::SIGKILL);
|
|
}
|
|
panic!("failed replacement provider survived cleanup");
|
|
}
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
|
|
drop(guard);
|
|
release_test_provider(¤t_release_path, ¤t_proof_path, current_pid);
|
|
}
|
|
|
|
#[test]
|
|
fn retained_publication_kills_failed_or_input_stalled_provider_groups() {
|
|
let guard = start_for_runtime().unwrap();
|
|
for (script, input) in [
|
|
("sleep 30 <&0 & echo $! > \"$1\"; exit 7", Vec::new()),
|
|
(
|
|
"sleep 30 <&0 & echo $! > \"$1\"; exit 0",
|
|
vec![b'x'; 1024 * 1024],
|
|
),
|
|
] {
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let pid_path = temp.path().join("provider.pid");
|
|
let result = guard.broker().publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new(script),
|
|
OsStr::new("sh"),
|
|
pid_path.as_os_str(),
|
|
],
|
|
input,
|
|
Duration::from_millis(100),
|
|
);
|
|
let provider_pid = std::fs::read_to_string(pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
if script.ends_with("exit 7") {
|
|
assert_eq!(result.unwrap().status, 7);
|
|
} else {
|
|
assert!(result.is_err());
|
|
}
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
loop {
|
|
// SAFETY: signal zero only probes the recorded test child PID.
|
|
if unsafe { libc::kill(provider_pid, 0) } != 0 {
|
|
break;
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"failed provider survived cleanup"
|
|
);
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn retained_publication_rejects_incomplete_input_after_successful_exit() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let result = guard.broker().publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[OsStr::new("-c"), OsStr::new("exit 0")],
|
|
vec![b'x'; 1024 * 1024],
|
|
Duration::from_secs(1),
|
|
);
|
|
|
|
assert!(result.is_err(), "incomplete publication input was accepted");
|
|
}
|
|
|
|
#[test]
|
|
fn broker_shutdown_preempts_retained_publication_stdin_writer() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let broker = guard.broker().clone();
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let current_pid_path = temp.path().join("current-provider.pid");
|
|
let current_release_path = temp.path().join("release-current-provider");
|
|
let current_proof_path = temp.path().join("current-provider-survived");
|
|
let pid_path = temp.path().join("provider.pid");
|
|
let current = guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new(
|
|
"(while [ ! -e \"$1\" ]; do sleep 0.01; done; printf survived > \"$2\") & echo $! > \"$3\"",
|
|
),
|
|
OsStr::new("sh"),
|
|
current_release_path.as_os_str(),
|
|
current_proof_path.as_os_str(),
|
|
current_pid_path.as_os_str(),
|
|
],
|
|
Vec::new(),
|
|
Duration::from_secs(2)
|
|
)
|
|
.unwrap();
|
|
assert_eq!(current.status, 0);
|
|
let current_pid = std::fs::read_to_string(current_pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
let publication = std::thread::spawn({
|
|
let pid_path = pid_path.clone();
|
|
move || {
|
|
broker.publish(
|
|
HelperKind::TestShell,
|
|
OsStr::new("sh"),
|
|
[
|
|
OsStr::new("-c"),
|
|
OsStr::new("sleep 30 <&0 & echo $! > \"$1\"; exit 0"),
|
|
OsStr::new("sh"),
|
|
pid_path.as_os_str(),
|
|
],
|
|
vec![b'x'; 1024 * 1024],
|
|
Duration::from_secs(2),
|
|
)
|
|
}
|
|
});
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
while !pid_path.exists() {
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"publication helper did not start"
|
|
);
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
let provider_pid = std::fs::read_to_string(pid_path)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<i32>()
|
|
.unwrap();
|
|
|
|
let started = Instant::now();
|
|
drop(guard);
|
|
assert!(
|
|
started.elapsed() < Duration::from_millis(500),
|
|
"broker shutdown waited for the publication deadline"
|
|
);
|
|
assert!(publication.join().unwrap().is_err());
|
|
release_test_provider(¤t_release_path, ¤t_proof_path, current_pid);
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
loop {
|
|
// SAFETY: signal zero only probes the recorded test child PID.
|
|
if unsafe { libc::kill(provider_pid, 0) } != 0 {
|
|
break;
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"publication provider survived broker shutdown"
|
|
);
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wl_copy_publication_accepts_capture_sized_input() {
|
|
const PUBLICATION_BYTES: usize = 16 * 1024 * 1024 + 1;
|
|
let temp = crate::test_temp::tempdir().unwrap();
|
|
let helper = crate::test_fake_helper::link(temp.path(), "wl-copy");
|
|
let count_path = temp.path().join("published-bytes");
|
|
let variables = crate::test_fake_helper::environment(
|
|
&helper,
|
|
crate::test_fake_helper::Role::CountInput,
|
|
&count_path,
|
|
);
|
|
|
|
let output = crate::test_env::with_env_vars(&variables, || {
|
|
let guard = start_for_runtime().unwrap();
|
|
guard
|
|
.broker()
|
|
.publish(
|
|
HelperKind::WlCopy,
|
|
helper.as_os_str(),
|
|
std::iter::empty::<&OsStr>(),
|
|
vec![b'x'; PUBLICATION_BYTES],
|
|
Duration::from_secs(30),
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
assert_eq!(output.status, 0);
|
|
assert!(!output.timed_out);
|
|
assert_eq!(
|
|
std::fs::read_to_string(count_path).unwrap().trim(),
|
|
PUBLICATION_BYTES.to_string()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_executable_category_survives_real_broker_transport_and_context() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let error = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::SessionZenity,
|
|
OsStr::new("/definitely-missing-wayscriber-test/zenity"),
|
|
[OsStr::new("--file-selection")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
1024,
|
|
)
|
|
.unwrap_err()
|
|
.context("caller can add diagnostic context");
|
|
|
|
assert_eq!(
|
|
crate::process_broker::error_kind(&error),
|
|
Some(crate::process_broker::BrokerErrorKind::MissingExecutable)
|
|
);
|
|
assert!(format!("{error:#}").contains("broker helper spawn failed"));
|
|
}
|
|
|
|
#[test]
|
|
fn rejected_diagnostic_with_missing_file_words_is_not_a_missing_executable() {
|
|
let guard = start_for_runtime().unwrap();
|
|
let error = guard
|
|
.broker()
|
|
.run(
|
|
HelperKind::SessionZenity,
|
|
OsStr::new("No such file"),
|
|
[OsStr::new("--file-selection")],
|
|
Vec::new(),
|
|
Duration::from_secs(1),
|
|
1024,
|
|
)
|
|
.unwrap_err()
|
|
.context("chooser context");
|
|
|
|
assert!(format!("{error:#}").contains("No such file"));
|
|
assert_eq!(
|
|
crate::process_broker::error_kind(&error),
|
|
Some(crate::process_broker::BrokerErrorKind::Rejected)
|
|
);
|
|
}
|