use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use runner_manager_domain::model::Arch;
use super::WslError;
use super::discovery::{DistributionTable, validate_distribution_name};
use super::exec::{ChildInput, CommandOutput, CommandRequest, CommandRunner, OutputLimits};
pub const LINUX_USER: &str = "root";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WslExecutable(PathBuf);
impl WslExecutable {
#[must_use]
pub fn locate() -> Self {
Self(locate_in_system32("wsl.exe"))
}
#[must_use]
pub fn at(path: impl Into<PathBuf>) -> Self {
Self(path.into())
}
#[must_use]
pub fn path(&self) -> &Path {
&self.0
}
}
impl Default for WslExecutable {
fn default() -> Self {
Self::locate()
}
}
impl fmt::Display for WslExecutable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.display())
}
}
pub(crate) fn locate_in_system32(program: &str) -> PathBuf {
locate_from(std::env::var_os("SystemRoot").map(PathBuf::from), program)
}
fn locate_from(system_root: Option<PathBuf>, program: &str) -> PathBuf {
if let Some(root) = system_root {
let candidate = root.join("System32").join(program);
if candidate.is_file() {
return candidate;
}
}
PathBuf::from(program)
}
#[derive(Debug)]
pub struct LinuxCommand {
distribution: String,
user: String,
program: String,
arguments: Vec<String>,
input: ChildInput,
timeout: Duration,
limits: OutputLimits,
}
impl LinuxCommand {
#[must_use]
pub fn new(distribution: impl Into<String>, program: impl Into<String>) -> Self {
Self {
distribution: distribution.into(),
user: LINUX_USER.to_string(),
program: program.into(),
arguments: Vec::new(),
input: ChildInput::Empty,
timeout: super::exec::DEFAULT_TIMEOUT,
limits: OutputLimits::default(),
}
}
#[must_use]
pub fn args<I, S>(mut self, arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.arguments.extend(arguments.into_iter().map(Into::into));
self
}
#[must_use]
pub fn with_input(mut self, input: ChildInput) -> Self {
self.input = input;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_limits(mut self, limits: OutputLimits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn distribution(&self) -> &str {
&self.distribution
}
#[must_use]
pub fn program(&self) -> &str {
&self.program
}
#[must_use]
pub fn wsl_arguments(&self) -> Vec<String> {
let mut argv = vec![
"--distribution".to_string(),
self.distribution.clone(),
"--user".to_string(),
self.user.clone(),
"--exec".to_string(),
self.program.clone(),
];
argv.extend(self.arguments.iter().cloned());
argv
}
}
#[derive(Debug, Clone, Copy)]
pub struct WslInvoker<'runner> {
runner: &'runner dyn CommandRunner,
executable: &'runner WslExecutable,
}
impl<'runner> WslInvoker<'runner> {
#[must_use]
pub fn new(runner: &'runner dyn CommandRunner, executable: &'runner WslExecutable) -> Self {
Self { runner, executable }
}
#[must_use]
pub fn executable(&self) -> &WslExecutable {
self.executable
}
pub fn list(&self) -> Result<DistributionTable, WslError> {
let request = CommandRequest::new(self.executable.path())
.arg("--list")
.arg("--verbose");
let output = self.runner.run(&request)?;
if !output.success() {
let table = DistributionTable::from_console_output(output.stdout());
if table.is_empty() && table.unreadable().is_empty() {
return Err(WslError::CommandFailed {
what: "list the installed WSL distributions",
program: self.executable.path().to_path_buf(),
exit_code: output.exit_code(),
detail: output.diagnostic(),
});
}
return Ok(table);
}
Ok(DistributionTable::from_console_output(output.stdout()))
}
pub fn exec(&self, command: LinuxCommand) -> Result<CommandOutput, WslError> {
validate_distribution_name(&command.distribution)?;
let request = CommandRequest::new(self.executable.path())
.args(command.wsl_arguments())
.with_timeout(command.timeout)
.with_limits(command.limits)
.with_input(command.input);
self.runner.run(&request)
}
pub fn exec_ok(
&self,
what: &'static str,
command: LinuxCommand,
) -> Result<CommandOutput, WslError> {
let program = PathBuf::from(command.program());
let output = self.exec(command)?;
if output.success() {
return Ok(output);
}
Err(WslError::CommandFailed {
what,
program,
exit_code: output.exit_code(),
detail: output.diagnostic(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SystemdState {
Running,
Degraded,
Starting,
Unavailable(String),
}
impl SystemdState {
#[must_use]
pub fn from_report(word: &str) -> Self {
match word.trim() {
"running" => Self::Running,
"degraded" => Self::Degraded,
"initializing" | "starting" => Self::Starting,
"" => Self::Unavailable("it said nothing".to_string()),
other => Self::Unavailable(other.to_string()),
}
}
#[must_use]
pub fn is_usable(&self) -> bool {
matches!(self, Self::Running | Self::Degraded | Self::Starting)
}
}
impl fmt::Display for SystemdState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Running => f.write_str("running"),
Self::Degraded => f.write_str("degraded"),
Self::Starting => f.write_str("starting"),
Self::Unavailable(word) => write!(f, "unavailable ({word})"),
}
}
}
pub fn architecture_from_uname(distribution: &str, machine: &str) -> Result<Arch, WslError> {
match machine.trim() {
"x86_64" | "amd64" => Ok(Arch::X64),
"aarch64" | "arm64" => Ok(Arch::Arm64),
other => Err(WslError::UnsupportedArchitecture {
distribution: distribution.to_string(),
reported: other.to_string(),
}),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DistributionReadiness {
name: String,
wsl_version: u8,
default: bool,
architecture: Arch,
machine: String,
systemd: SystemdState,
}
impl DistributionReadiness {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn wsl_version(&self) -> u8 {
self.wsl_version
}
#[must_use]
pub fn is_default(&self) -> bool {
self.default
}
#[must_use]
pub fn architecture(&self) -> Arch {
self.architecture
}
#[must_use]
pub fn machine(&self) -> &str {
&self.machine
}
#[must_use]
pub fn systemd(&self) -> &SystemdState {
&self.systemd
}
}
pub fn probe_readiness(
invoker: &WslInvoker<'_>,
name: &str,
) -> Result<DistributionReadiness, WslError> {
validate_distribution_name(name)?;
let table = invoker.list()?;
let installed = table.exactly(name)?;
installed.require_wsl2()?;
let wsl_version = installed.wsl_version();
let default = installed.is_default();
let identity = invoker.exec(LinuxCommand::new(name, "id").args(["-u"]))?;
let reported = identity.stdout_text();
if !identity.success() || reported != "0" {
return Err(WslError::NoRootAccess {
distribution: name.to_string(),
detail: if identity.success() {
format!("`id -u` answered {reported}, not 0")
} else {
identity.diagnostic()
},
});
}
let uname = invoker.exec_ok(
"read the distribution's architecture",
LinuxCommand::new(name, "uname").args(["-m"]),
)?;
let machine = uname.stdout_text();
let architecture = architecture_from_uname(name, &machine)?;
let systemd_report =
invoker.exec(LinuxCommand::new(name, "systemctl").args(["is-system-running"]))?;
let systemd = SystemdState::from_report(&systemd_report.stdout_text());
if !systemd.is_usable() {
return Err(WslError::SystemdUnavailable {
distribution: name.to_string(),
detail: match &systemd {
SystemdState::Unavailable(word) => {
let stderr = systemd_report.stderr_text();
if stderr.is_empty() {
format!("`systemctl is-system-running` answered `{word}`")
} else {
format!("`systemctl is-system-running` answered `{word}`: {stderr}")
}
}
other => other.to_string(),
},
});
}
Ok(DistributionReadiness {
name: name.to_string(),
wsl_version,
default,
architecture,
machine,
systemd,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wsl::exec::{PipedInput, ScriptedRunner};
use secrecy::{ExposeSecret, SecretString};
fn executable() -> WslExecutable {
WslExecutable::at("wsl.exe")
}
fn table() -> CommandOutput {
CommandOutput::exited(
0,
concat!(
" NAME STATE VERSION\n",
"* Ubuntu Running 2\n",
" Legacy Stopped 1\n",
),
"",
)
}
fn healthy() -> ScriptedRunner {
ScriptedRunner::new()
.always("--list --verbose", table())
.always("--exec id -u", CommandOutput::exited(0, "0\n", ""))
.always("--exec uname -m", CommandOutput::exited(0, "x86_64\n", ""))
.always(
"--exec systemctl is-system-running",
CommandOutput::exited(0, "running\n", ""),
)
}
#[test]
fn a_linux_command_is_an_argument_vector_with_no_shell_in_it() {
let command = LinuxCommand::new("Debian GNU/Linux 12", "/usr/local/bin/runner-manager")
.args(["service", "install", "--start-at", "boot"]);
assert_eq!(
command.wsl_arguments(),
vec![
"--distribution",
"Debian GNU/Linux 12",
"--user",
"root",
"--exec",
"/usr/local/bin/runner-manager",
"service",
"install",
"--start-at",
"boot",
]
);
}
#[test]
fn a_hostile_distribution_name_stays_one_argument() {
let command = LinuxCommand::new("Ubuntu; rm -rf /", "id").args(["-u"]);
let argv = command.wsl_arguments();
assert_eq!(argv[1], "Ubuntu; rm -rf /");
assert!(
argv.iter().all(|argument| argument != "rm"),
"the name must never become its own argument: {argv:?}"
);
assert!(argv.contains(&"--exec".to_string()));
}
#[test]
fn the_invoker_passes_the_vector_through_untouched() {
let runner = healthy();
let executable = executable();
let invoker = WslInvoker::new(&runner, &executable);
invoker
.exec(LinuxCommand::new("Ubuntu", "id").args(["-u"]))
.expect("scripted");
let recorded = runner.recorded();
assert_eq!(recorded.len(), 1);
assert_eq!(
recorded[0].arguments,
vec![
"--distribution",
"Ubuntu",
"--user",
"root",
"--exec",
"id",
"-u"
]
);
}
#[test]
fn a_name_that_reads_as_an_option_is_refused_before_anything_is_launched() {
let runner = ScriptedRunner::new();
let executable = executable();
let invoker = WslInvoker::new(&runner, &executable);
let error = invoker
.exec(LinuxCommand::new("--shutdown", "id"))
.expect_err("refused");
assert!(matches!(error, WslError::InvalidName { .. }), "{error:?}");
assert_eq!(runner.call_count(), 0, "nothing may be launched");
}
#[test]
fn a_piped_payload_reaches_the_child_and_no_argument() {
let secret = SecretString::from(format!("{}{}", "ghu_", "a1ProbeFixtureNotARealToken0000"));
let runner = ScriptedRunner::new();
let executable = executable();
let invoker = WslInvoker::new(&runner, &executable);
invoker
.exec(
LinuxCommand::new("Ubuntu", "/usr/local/bin/runner-manager")
.args(["auth", "receive", "--start-at", "boot"])
.with_input(ChildInput::Piped(PipedInput::from_secret_text(&secret))),
)
.expect("scripted");
assert_eq!(runner.piped_input(), secret.expose_secret().as_bytes());
assert!(
runner
.command_lines()
.iter()
.all(|line| !line.contains(secret.expose_secret())),
"the canary must not be in any command line"
);
}
#[test]
fn a_healthy_distribution_reports_every_fact_it_established() {
let runner = healthy();
let executable = executable();
let ready =
probe_readiness(&WslInvoker::new(&runner, &executable), "Ubuntu").expect("healthy");
assert_eq!(ready.name(), "Ubuntu");
assert_eq!(ready.wsl_version(), 2);
assert!(ready.is_default());
assert_eq!(ready.architecture(), Arch::X64);
assert_eq!(ready.machine(), "x86_64");
assert_eq!(ready.systemd(), &SystemdState::Running);
}
#[test]
fn a_wsl1_distribution_is_refused_before_any_linux_command_runs() {
let runner = healthy();
let executable = executable();
let error =
probe_readiness(&WslInvoker::new(&runner, &executable), "Legacy").expect_err("WSL1");
assert!(matches!(error, WslError::NotWsl2 { .. }), "{error:?}");
assert_eq!(
runner.call_count(),
1,
"only `--list` should have run: {:?}",
runner.command_lines()
);
}
#[test]
fn a_distribution_that_is_not_installed_lists_the_ones_that_are() {
let runner = healthy();
let executable = executable();
let error =
probe_readiness(&WslInvoker::new(&runner, &executable), "Fedora").expect_err("absent");
assert!(error.to_string().contains("Ubuntu"), "{error}");
}
#[test]
fn a_distribution_that_does_not_start_as_root_is_refused() {
let runner = ScriptedRunner::new()
.always("--list --verbose", table())
.always("--exec id -u", CommandOutput::exited(0, "1000\n", ""));
let executable = executable();
let error = probe_readiness(&WslInvoker::new(&runner, &executable), "Ubuntu")
.expect_err("not root");
let WslError::NoRootAccess { detail, .. } = &error else {
panic!("unexpected error: {error:?}");
};
assert!(detail.contains("1000"), "{detail}");
}
#[test]
fn an_unsupported_architecture_names_what_the_distribution_said() {
let runner = ScriptedRunner::new()
.always("--list --verbose", table())
.always("--exec id -u", CommandOutput::exited(0, "0\n", ""))
.always("--exec uname -m", CommandOutput::exited(0, "armv7l\n", ""));
let executable = executable();
let error = probe_readiness(&WslInvoker::new(&runner, &executable), "Ubuntu")
.expect_err("armv7l has no published artifact");
assert!(
matches!(&error, WslError::UnsupportedArchitecture { reported, .. } if reported == "armv7l"),
"{error:?}"
);
assert!(error.to_string().contains("armv7l"));
}
#[test]
fn architecture_mapping_covers_both_published_linux_targets_and_nothing_else() {
assert_eq!(
architecture_from_uname("d", "x86_64").expect("x64"),
Arch::X64
);
assert_eq!(
architecture_from_uname("d", "amd64").expect("x64"),
Arch::X64
);
assert_eq!(
architecture_from_uname("d", "aarch64").expect("arm64"),
Arch::Arm64
);
assert_eq!(
architecture_from_uname("d", "arm64\n").expect("arm64"),
Arch::Arm64
);
for machine in ["armv7l", "i686", "riscv64", "s390x", ""] {
assert!(
architecture_from_uname("d", machine).is_err(),
"{machine} has no published Linux artifact"
);
}
}
#[test]
fn a_distribution_without_systemd_is_refused_with_what_it_answered() {
let runner = ScriptedRunner::new()
.always("--list --verbose", table())
.always("--exec id -u", CommandOutput::exited(0, "0\n", ""))
.always("--exec uname -m", CommandOutput::exited(0, "x86_64\n", ""))
.always(
"--exec systemctl is-system-running",
CommandOutput::exited(1, "offline\n", ""),
);
let executable = executable();
let error = probe_readiness(&WslInvoker::new(&runner, &executable), "Ubuntu")
.expect_err("no systemd");
let WslError::SystemdUnavailable { detail, .. } = &error else {
panic!("unexpected error: {error:?}");
};
assert!(detail.contains("offline"), "{detail}");
}
#[test]
fn degraded_and_starting_systemd_are_usable_because_the_products_own_unit_is_what_matters() {
for word in ["degraded", "starting", "initializing"] {
let runner = ScriptedRunner::new()
.always("--list --verbose", table())
.always("--exec id -u", CommandOutput::exited(0, "0\n", ""))
.always("--exec uname -m", CommandOutput::exited(0, "x86_64\n", ""))
.always(
"--exec systemctl is-system-running",
CommandOutput::exited(1, format!("{word}\n"), ""),
);
let executable = executable();
let ready = probe_readiness(&WslInvoker::new(&runner, &executable), "Ubuntu")
.unwrap_or_else(|error| panic!("{word} should be usable: {error}"));
assert!(ready.systemd().is_usable());
}
}
#[test]
fn the_preflight_mutates_nothing() {
let runner = healthy();
let executable = executable();
probe_readiness(&WslInvoker::new(&runner, &executable), "Ubuntu").expect("healthy");
for line in runner.command_lines() {
assert!(
[
"--list --verbose",
"id -u",
"uname -m",
"systemctl is-system-running"
]
.iter()
.any(|read| line.contains(read)),
"the preflight ran something that is not a read: {line}"
);
}
}
#[test]
fn a_missing_system_root_falls_back_to_the_path_lookup() {
assert_eq!(locate_from(None, "wsl.exe"), PathBuf::from("wsl.exe"));
assert_eq!(
locate_from(Some(PathBuf::from("Q:\\NoSuchWindows")), "wsl.exe"),
PathBuf::from("wsl.exe")
);
}
#[test]
fn a_system_root_that_really_holds_the_executable_is_used() {
let root = tempfile::tempdir().expect("a temporary directory");
let system32 = root.path().join("System32");
std::fs::create_dir_all(&system32).expect("create System32");
std::fs::write(system32.join("wsl.exe"), b"not really an executable")
.expect("write the stand-in");
assert_eq!(
locate_from(Some(root.path().to_path_buf()), "wsl.exe"),
system32.join("wsl.exe")
);
}
}