pub mod artifact;
pub mod discovery;
pub mod exec;
pub mod probe;
pub mod record;
pub mod task;
use std::fmt;
use std::path::PathBuf;
use exec::{CommandRunner, HostCommandRunner};
use probe::{WslExecutable, WslInvoker};
use task::LifecycleTaskControl;
#[derive(Debug, thiserror::Error)]
pub enum WslError {
#[error(
"{operation} is a Windows feature: WSL runs on Windows, and this is a {} build. \
Manage this host's own operating system with the ordinary commands instead.",
std::env::consts::OS
)]
UnsupportedPlatform {
operation: &'static str,
},
#[error("cannot start {}: {source}", program.display())]
Spawn {
program: PathBuf,
#[source]
source: std::io::Error,
},
#[error("cannot control {}: {source}", program.display())]
ChildControl {
program: PathBuf,
#[source]
source: std::io::Error,
},
#[error(
"refusing to start {}: the value meant for this process's stdin also appears in \
{location}, which would put it in this machine's process listing. Pass it on stdin \
only (`03-security-and-lifecycle.md`, item 3).",
program.display()
)]
SecretInCommandLine {
program: PathBuf,
location: String,
},
#[error("cannot {what} using {}: {detail}", program.display())]
CommandFailed {
what: &'static str,
program: PathBuf,
exit_code: Option<i32>,
detail: String,
},
#[error("{requested:?} is not a usable distribution name: {reason}")]
InvalidName {
requested: String,
reason: String,
},
#[error(
"no WSL distribution named {requested:?} is installed{}",
if available.is_empty() {
". This host has none.".to_string()
} else {
format!(". This host has: {}. Names are matched exactly.", available.join(", "))
}
)]
NotInstalled {
requested: String,
available: Vec<String>,
},
#[error(
"`wsl --list --verbose` reports {requested:?} twice, so this cannot tell which one \
was meant. Rename one of them."
)]
AmbiguousName {
requested: String,
},
#[error(
"{distribution} is WSL version {version}; this feature supports WSL2 only, because a \
WSL1 distribution has neither systemd nor a Linux kernel. \
Convert it with `wsl --set-version {distribution} 2`."
)]
NotWsl2 {
distribution: String,
version: u8,
},
#[error(
"{distribution} does not start as root, so the provider cannot install a system \
service or write /usr/local/bin in it: {detail}"
)]
NoRootAccess {
distribution: String,
detail: String,
},
#[error(
"{distribution} reports the architecture {reported:?}, and runner-manager publishes no \
Linux release for it. Only x86-64 and 64-bit ARM are published."
)]
UnsupportedArchitecture {
distribution: String,
reported: String,
},
#[error(
"{distribution} is not running systemd, and the Linux runner-manager service is a \
systemd unit: {detail}. Enable it with `systemd=true` under `[boot]` in \
/etc/wsl.conf inside the distribution, then `wsl --terminate {distribution}`."
)]
SystemdUnavailable {
distribution: String,
detail: String,
},
#[error("the release checksum document cannot be used: {detail}")]
UnreadableChecksums {
detail: String,
},
#[error(
"the release publishes no {triple} archive for version {version} (it publishes \
{published} assets), so there is no Linux binary to install that matches this \
Windows build."
)]
NoSuchArtifact {
version: String,
triple: String,
published: usize,
},
#[error(
"the release publishes {count} {triple} archives for version {version}; refusing to \
guess which one is meant."
)]
AmbiguousArtifact {
version: String,
triple: String,
count: usize,
},
#[error("the release archive at {} cannot be used: {detail}", path.display())]
UnreadableArchive {
path: PathBuf,
detail: String,
},
#[error(
"the release archive at {} hashes to {actual}, and the release says it should be \
{expected}. Nothing has been installed.",
path.display()
)]
DigestMismatch {
path: PathBuf,
expected: String,
actual: String,
},
#[error("{path:?} is not a usable Linux destination: {reason}")]
InvalidDestination {
path: String,
reason: String,
},
#[error(
"the unpacked binary reports {reported:?}, not version {expected}. It has not been \
installed and the existing binary is untouched."
)]
VersionMismatch {
expected: String,
reported: String,
},
#[error("the scheduled task {name} is not this product's, so it will not be changed: {detail}")]
ForeignTask {
name: String,
detail: String,
},
#[error("no scheduled task named {name} is registered on this host")]
NoSuchTask {
name: String,
},
#[error("cannot {operation} the scheduled task {name}: {detail}")]
TaskControl {
operation: &'static str,
name: String,
detail: String,
},
#[error(
"cannot {operation} the scheduled task {name} without elevation: {detail}. Run this \
command from an elevated prompt."
)]
NeedsElevation {
operation: &'static str,
name: String,
detail: String,
},
#[error("cannot {operation} the provider record at {}: {detail}", path.display())]
Record {
operation: &'static str,
path: PathBuf,
detail: String,
},
#[error(
"the provider record at {} was written under schema version {found}, and this build \
understands version {supported}. Refusing to read it rather than silently dropping \
what it does not understand; upgrade runner-manager.",
path.display()
)]
RecordSchema {
path: PathBuf,
found: u32,
supported: u32,
},
}
impl WslError {
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
Self::UnsupportedPlatform { .. } => "unsupported_platform",
Self::Spawn { .. } => "spawn",
Self::ChildControl { .. } => "child_control",
Self::SecretInCommandLine { .. } => "secret_in_command_line",
Self::CommandFailed { .. } => "command_failed",
Self::InvalidName { .. } => "invalid_name",
Self::NotInstalled { .. } => "not_installed",
Self::AmbiguousName { .. } => "ambiguous_name",
Self::NotWsl2 { .. } => "not_wsl2",
Self::NoRootAccess { .. } => "no_root_access",
Self::UnsupportedArchitecture { .. } => "unsupported_architecture",
Self::SystemdUnavailable { .. } => "systemd_unavailable",
Self::UnreadableChecksums { .. } => "unreadable_checksums",
Self::NoSuchArtifact { .. } => "no_such_artifact",
Self::AmbiguousArtifact { .. } => "ambiguous_artifact",
Self::UnreadableArchive { .. } => "unreadable_archive",
Self::DigestMismatch { .. } => "digest_mismatch",
Self::InvalidDestination { .. } => "invalid_destination",
Self::VersionMismatch { .. } => "version_mismatch",
Self::ForeignTask { .. } => "foreign_task",
Self::NoSuchTask { .. } => "no_such_task",
Self::TaskControl { .. } => "task_control",
Self::NeedsElevation { .. } => "needs_elevation",
Self::Record { .. } => "record",
Self::RecordSchema { .. } => "record_schema",
}
}
#[must_use]
pub fn is_preflight(&self) -> bool {
matches!(
self,
Self::UnsupportedPlatform { .. }
| Self::SecretInCommandLine { .. }
| Self::InvalidName { .. }
| Self::NotInstalled { .. }
| Self::AmbiguousName { .. }
| Self::NotWsl2 { .. }
| Self::NoRootAccess { .. }
| Self::UnsupportedArchitecture { .. }
| Self::SystemdUnavailable { .. }
| Self::UnreadableChecksums { .. }
| Self::NoSuchArtifact { .. }
| Self::AmbiguousArtifact { .. }
| Self::UnreadableArchive { .. }
| Self::DigestMismatch { .. }
| Self::InvalidDestination { .. }
)
}
}
pub fn require_windows(operation: &'static str) -> Result<(), WslError> {
if cfg!(windows) {
return Ok(());
}
Err(WslError::UnsupportedPlatform { operation })
}
pub struct WslHost {
runner: Box<dyn CommandRunner>,
executable: WslExecutable,
}
impl WslHost {
pub fn on_this_host(operation: &'static str) -> Result<Self, WslError> {
require_windows(operation)?;
Ok(Self {
runner: Box::new(HostCommandRunner),
executable: WslExecutable::locate(),
})
}
#[must_use]
pub fn with_runner(runner: Box<dyn CommandRunner>, executable: WslExecutable) -> Self {
Self { runner, executable }
}
#[must_use]
pub fn executable(&self) -> &WslExecutable {
&self.executable
}
#[must_use]
pub fn invoker(&self) -> WslInvoker<'_> {
WslInvoker::new(self.runner.as_ref(), &self.executable)
}
#[must_use]
pub fn tasks(&self) -> LifecycleTaskControl<'_> {
LifecycleTaskControl::new(self.runner.as_ref())
}
}
impl fmt::Debug for WslHost {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WslHost")
.field("executable", &self.executable)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_non_windows_build_refuses_with_a_sentence_rather_than_not_compiling() {
let result = require_windows("`runner-manager wsl install`");
if cfg!(windows) {
assert!(result.is_ok());
} else {
let error = result.expect_err("not Windows");
assert_eq!(error.kind(), "unsupported_platform");
let message = error.to_string();
assert!(
message.contains("`runner-manager wsl install`"),
"{message}"
);
assert!(message.contains(std::env::consts::OS), "{message}");
}
}
#[test]
fn the_whole_model_is_available_on_every_platform() {
let identity = task::LifecycleTaskIdentity::for_distribution("Ubuntu").expect("valid");
assert!(identity.name().starts_with(task::LIFECYCLE_TASK_PREFIX));
assert!(!discovery::DistributionTable::parse(" Ubuntu Running 2\n").is_empty());
assert_eq!(
artifact::LinuxBinaryPath::default().as_path(),
artifact::DEFAULT_LINUX_DESTINATION
);
}
#[test]
fn every_error_has_a_distinct_stable_kind() {
let kinds = [
WslError::UnsupportedPlatform { operation: "x" }.kind(),
WslError::InvalidName {
requested: String::new(),
reason: String::new(),
}
.kind(),
WslError::NotInstalled {
requested: String::new(),
available: Vec::new(),
}
.kind(),
WslError::NotWsl2 {
distribution: String::new(),
version: 1,
}
.kind(),
WslError::ForeignTask {
name: String::new(),
detail: String::new(),
}
.kind(),
WslError::RecordSchema {
path: PathBuf::new(),
found: 2,
supported: 1,
}
.kind(),
];
let mut unique = kinds.to_vec();
unique.sort_unstable();
unique.dedup();
assert_eq!(unique.len(), kinds.len(), "{kinds:?}");
}
#[test]
fn a_preflight_failure_says_it_changed_nothing() {
assert!(
WslError::NotWsl2 {
distribution: "Legacy".to_string(),
version: 1,
}
.is_preflight()
);
assert!(
!WslError::TaskControl {
operation: "register",
name: String::new(),
detail: String::new(),
}
.is_preflight()
);
}
#[test]
fn a_host_over_a_scripted_runner_works_on_any_platform() {
let runner = exec::ScriptedRunner::new().always(
"--list --verbose",
exec::CommandOutput::exited(0, "* Ubuntu Running 2\n", ""),
);
let host = WslHost::with_runner(Box::new(runner), WslExecutable::at("wsl.exe"));
let table = host.invoker().list().expect("scripted");
assert_eq!(table.names(), ["Ubuntu"]);
assert!(format!("{host:?}").contains("wsl.exe"));
}
}