Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
self-hosted-runner:
labels:
- wsl2
- wslg
53 changes: 51 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ on:
description: 'Git ref to verify; defaults to the triggering ref'
required: false
type: string
workflow_dispatch:
inputs:
run_wslg_receipt:
description: 'Run the authoritative in-distro WSL2/WSLg receipt on the labeled self-hosted runner'
required: true
default: false
type: boolean

# Cancel superseded runs on the same ref to save runner minutes.
concurrency:
Expand Down Expand Up @@ -121,5 +128,47 @@ jobs:

- name: Run platform default-shell and authority integration
run: >-
cargo test -p splice-pty --test session_contract &&
cargo test -p splice-shell-desktop --test platform_authority
cargo test -p splice-pty --test session_contract &&
cargo test -p splice-shell-desktop --test platform_authority

wslg-runtime-receipt:
name: WSL2/WSLg runtime receipt (manual, authoritative)
# Generic Linux and hosted Windows do not prove this model. Successful real WSL2/WSLg receipt is required before support may be declared.
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_wslg_receipt }}
runs-on: [self-hosted, linux, x64, wsl2, wslg]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ inputs.ref || github.ref }}

- name: Verify in-distro WSL2 and WSLg prerequisites
run: >-
grep -q '^ID=ubuntu$' /etc/os-release &&
grep -Eq '^VERSION_ID="(22.04|24.04)"$' /etc/os-release &&
test -n "$WSL_DISTRO_NAME" &&
grep -qi wsl2 /proc/sys/kernel/osrelease &&
test -n "$WAYLAND_DISPLAY" &&
test -n "$XDG_RUNTIME_DIR" &&
test -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" &&
test -x /bin/sh && command -v xdg-open

- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable

- name: Run platform authority/default-shell prerequisite
run: cargo test -p splice-shell-desktop --test platform_authority

- name: Run WSL2/WSLg PTY receipt harness
env:
SPLICE_WSL_RECEIPT: artifacts/wsl-runtime-receipt.json
SPLICE_WSL_RECEIPT_REQUIRED: 'true'
run: cargo test -p splice-pty --test wsl_runtime_receipt -- --nocapture

- name: Upload WSL2/WSLg machine-readable receipt
if: ${{ always() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wsl2-wslg-runtime-receipt
path: artifacts/wsl-runtime-receipt.json
if-no-files-found: error
91 changes: 91 additions & 0 deletions crates/splice-pty/tests/wsl_runtime_receipt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
mod wsl_runtime_support;

use std::{fs, path::PathBuf};

use wsl_runtime_support::{
classify, require_authoritative_receipt, run_receipt, write_receipt, WslFacts, WslRuntime,
};

#[test]
fn deterministic_fixtures_distinguish_ready_wslg_from_named_skips() {
let ready = classify(WslFacts {
os_release: "ID=ubuntu\nVERSION_ID=\"24.04\"\n",
kernel_release: "6.8.0-microsoft-standard-WSL2",
distro_name: Some("Ubuntu"),
wayland_display: Some("wayland-0"),
wayland_socket: true,
});
assert_eq!(ready, WslRuntime::Ready);

let generic_linux = classify(WslFacts {
kernel_release: "6.8.0-generic",
..WslFacts::supported()
});
assert_eq!(
generic_linux,
WslRuntime::Skip("not a WSL2 kernel; generic Linux must not claim a WSL receipt")
);

let missing_wslg = classify(WslFacts {
wayland_display: None,
wayland_socket: false,
..WslFacts::supported()
});
assert_eq!(
missing_wslg,
WslRuntime::Skip("WSLg Wayland display is unavailable")
);
}

#[test]
fn required_mode_rejects_a_skipped_receipt() {
let skipped = WslRuntime::Skip("deterministic unsupported fixture");
assert!(require_authoritative_receipt(skipped, false).is_ok());
assert_eq!(
require_authoritative_receipt(skipped, true),
Err("deterministic unsupported fixture")
);
}

#[test]
fn guarded_wsl_receipt_workflow_requires_a_real_self_hosted_wslg_host() {
let workflow = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.github/workflows/ci.yml");
let workflow = fs::read_to_string(workflow).expect("CI workflow is readable");

for required_contract in [
"run_wslg_receipt",
"WSL2/WSLg runtime receipt (manual, authoritative)",
"runs-on: [self-hosted, linux, x64, wsl2, wslg]",
"SPLICE_WSL_RECEIPT: artifacts/wsl-runtime-receipt.json",
"SPLICE_WSL_RECEIPT_REQUIRED: 'true'",
"grep -q '^ID=ubuntu$' /etc/os-release",
"grep -Eq '^VERSION_ID=\"(22.04|24.04)\"$' /etc/os-release",
"cargo test -p splice-pty --test wsl_runtime_receipt -- --nocapture",
"cargo test -p splice-shell-desktop --test platform_authority",
"actions/upload-artifact",
"Successful real WSL2/WSLg receipt is required before support may be declared.",
] {
assert!(
workflow.contains(required_contract),
"WSL receipt workflow must declare: {required_contract}"
);
}

let actionlint =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.github/actionlint.yaml");
let actionlint = fs::read_to_string(actionlint).expect("actionlint custom-runner metadata");
assert!(actionlint.contains("- wsl2") && actionlint.contains("- wslg"));
}

#[test]
fn runtime_harness_emits_a_named_skip_or_a_real_wslg_receipt() {
let status = run_receipt();
write_receipt(status);
match status {
WslRuntime::Ready => println!("RECEIPT: WSL2/WSLg PTY runtime passed"),
WslRuntime::Skip(reason) => println!("SKIP: WSL2/WSLg PTY runtime: {reason}"),
}
let required = std::env::var("SPLICE_WSL_RECEIPT_REQUIRED").is_ok_and(|value| value == "true");
require_authoritative_receipt(status, required)
.expect("authoritative WSL2/WSLg receipt must not skip");
}
213 changes: 213 additions & 0 deletions crates/splice-pty/tests/wsl_runtime_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use std::{
fs,
path::Path,
sync::mpsc,
time::{Duration, Instant},
};

use splice_pty::{PtySession, TerminalSize};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WslRuntime {
Ready,
Skip(&'static str),
}

pub struct WslFacts<'a> {
pub os_release: &'a str,
pub kernel_release: &'a str,
pub distro_name: Option<&'a str>,
pub wayland_display: Option<&'a str>,
pub wayland_socket: bool,
}

impl<'a> WslFacts<'a> {
pub fn supported() -> Self {
Self {
os_release: "ID=ubuntu\nVERSION_ID=\"24.04\"\n",
kernel_release: "6.8.0-microsoft-standard-WSL2",
distro_name: Some("Ubuntu"),
wayland_display: Some("wayland-0"),
wayland_socket: true,
}
}
}

pub fn classify(facts: WslFacts<'_>) -> WslRuntime {
if !facts.kernel_release.to_ascii_lowercase().contains("wsl2") {
return WslRuntime::Skip("not a WSL2 kernel; generic Linux must not claim a WSL receipt");
}
if facts.distro_name.is_none_or(str::is_empty) {
return WslRuntime::Skip("WSL_DISTRO_NAME is required for an in-distro WSL receipt");
}
if !facts.os_release.lines().any(|line| line == "ID=ubuntu")
|| !facts
.os_release
.lines()
.any(|line| matches!(line, "VERSION_ID=\"22.04\"" | "VERSION_ID=\"24.04\""))
{
return WslRuntime::Skip("WSL receipt requires Ubuntu 22.04 or 24.04");
}
if facts.wayland_display.is_none_or(str::is_empty) || !facts.wayland_socket {
return WslRuntime::Skip("WSLg Wayland display is unavailable");
}
WslRuntime::Ready
}

pub fn require_authoritative_receipt(
status: WslRuntime,
required: bool,
) -> Result<(), &'static str> {
match (required, status) {
(true, WslRuntime::Skip(reason)) => Err(reason),
_ => Ok(()),
}
}

fn receive_until(receiver: &mpsc::Receiver<String>, marker: &str) -> String {
let deadline = Instant::now() + Duration::from_secs(5);
let mut output = String::new();
while Instant::now() < deadline && !output.contains(marker) {
if let Ok(chunk) = receiver.recv_timeout(Duration::from_millis(250)) {
output.push_str(&chunk);
}
}
output
}

fn is_alive(pid: &str) -> bool {
std::process::Command::new("kill")
.args(["-0", pid])
.status()
.is_ok_and(|status| status.success())
}

#[cfg(unix)]
fn is_wayland_socket(path: &Path) -> bool {
use std::os::unix::fs::FileTypeExt;

fs::metadata(path).is_ok_and(|metadata| metadata.file_type().is_socket())
}

#[cfg(not(unix))]
fn is_wayland_socket(_: &Path) -> bool {
false
}

fn skip_or_ready() -> WslRuntime {
let kernel_release = fs::read_to_string("/proc/sys/kernel/osrelease").unwrap_or_default();
let os_release = fs::read_to_string("/etc/os-release").unwrap_or_default();
let display = std::env::var("WAYLAND_DISPLAY").ok();
let socket = std::env::var("XDG_RUNTIME_DIR")
.ok()
.zip(display.as_deref())
.is_some_and(|(runtime_dir, display)| {
is_wayland_socket(&Path::new(&runtime_dir).join(display))
});
classify(WslFacts {
os_release: &os_release,
kernel_release: &kernel_release,
distro_name: std::env::var("WSL_DISTRO_NAME").ok().as_deref(),
wayland_display: display.as_deref(),
wayland_socket: socket,
})
}

pub fn run_receipt() -> WslRuntime {
let status = skip_or_ready();
if status != WslRuntime::Ready {
return status;
}
if !Path::new("/bin/sh").is_file() {
return WslRuntime::Skip("WSL default shell prerequisite /bin/sh is unavailable");
}
if std::env::var_os("PATH").is_none() {
return WslRuntime::Skip("PATH is required for WSL platform authority and path reveal");
}
if !matches!(std::process::Command::new("sh")
.args(["-c", "command -v xdg-open"])
.status(), Ok(status) if status.success())
{
return WslRuntime::Skip("xdg-open is required for WSL path reveal");
}

let size = TerminalSize::new(80, 24).expect("fixture terminal size");
let (sender, receiver) = mpsc::channel();
let session = PtySession::spawn(
"/bin/sh",
&["-c", "printf 'ready utf8=caf茅\\n'; stty size; read value; printf 'input=%s\\n' \"$value\"; stty size"],
size,
move |_, output| { let _ = sender.send(output); },
|_| {},
).expect("WSL default shell starts");
assert!(session.is_running().expect("liveness is available"));
assert!(receive_until(&receiver, "24 80").contains("utf8=caf茅"));
session
.resize(TerminalSize::new(132, 43).expect("resize size"))
.expect("resize succeeds");
session.write("wsl-input\n").expect("input succeeds");
assert!(receive_until(&receiver, "43 132").contains("input=wsl-input"));
session.close();
assert!(!session.is_running().expect("liveness after close"));

let (sender, receiver) = mpsc::channel();
let session = PtySession::spawn(
"/bin/sh",
&[
"-c",
"trap 'printf interrupted; exit' INT; printf ready; while :; do sleep 1; done",
],
size,
move |_, output| {
let _ = sender.send(output);
},
|_| {},
)
.expect("WSL interrupt shell starts");
assert!(receive_until(&receiver, "ready").contains("ready"));
session.interrupt().expect("interrupt succeeds");
assert!(receive_until(&receiver, "interrupted").contains("interrupted"));
session.close();

let (sender, receiver) = mpsc::channel();
let session = PtySession::spawn(
"/bin/sh",
&["-c", "trap '' HUP TERM; sh -c 'trap \"\" HUP TERM; echo child=$$; while :; do sleep 1; done' & wait"],
size,
move |_, output| { let _ = sender.send(output); },
|_| {},
).expect("WSL teardown shell starts");
let output = receive_until(&receiver, "child=");
let child = output
.split("child=")
.nth(1)
.expect("child pid")
.lines()
.next()
.expect("child pid line")
.trim();
session.close();
session.close();
assert!(
!is_alive(child),
"close must tear down the WSL process group"
);
WslRuntime::Ready
}

pub fn write_receipt(status: WslRuntime) {
let (outcome, reason) = match status {
WslRuntime::Ready => ("passed", "all WSL2/WSLg PTY probes passed"),
WslRuntime::Skip(reason) => ("skipped", reason),
};
let receipt = format!(
"{{\"target\":\"linux-native-wsl2-wslg\",\"status\":\"{outcome}\",\"reason\":\"{reason}\",\"probes\":[\"startup\",\"utf8\",\"input\",\"resize\",\"interrupt\",\"process_group_teardown\",\"liveness\",\"close\"],\"platform_prerequisites\":[\"platform_authority\",\"default_shell\",\"PATH\",\"xdg-open\"]}}"
);
if let Some(path) = std::env::var_os("SPLICE_WSL_RECEIPT") {
let path = Path::new(&path);
fs::create_dir_all(path.parent().expect("receipt has a parent"))
.expect("receipt directory");
fs::write(path, &receipt).expect("machine-readable WSL receipt");
}
println!("WSL_RECEIPT: {receipt}");
}
Loading