use std::path::{Path, PathBuf};
pub const ENV: &str = "YAH_WIN_INTERACTIVE";
pub const BRIDGE: &str = include_str!("win_interactive.sh");
const INTEROP_MARKER: &str = "/proc/sys/fs/binfmt_misc/WSLInterop";
pub fn interop_available() -> bool {
Path::new(INTEROP_MARKER).exists()
}
pub fn bridge_path(state_dir: &Path) -> PathBuf {
state_dir.join(".win-interactive").join("win-interactive")
}
pub async fn materialize(state_dir: &Path, interop: bool) -> std::io::Result<Option<PathBuf>> {
if !interop {
return Ok(None);
}
let path = bridge_path(state_dir);
let current = tokio::fs::read(&path).await.ok();
if current.as_deref() != Some(BRIDGE.as_bytes()) {
let write_path = path.clone();
tokio::task::spawn_blocking(move || -> std::io::Result<()> {
if let Some(dir) = write_path.parent() {
std::fs::create_dir_all(dir)?;
}
crate::atomic_file::write_atomic(&write_path, BRIDGE.as_bytes())
})
.await
.map_err(std::io::Error::other)??;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = tokio::fs::metadata(&path).await?.permissions().mode();
if mode & 0o755 != 0o755 {
tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).await?;
}
}
Ok(Some(path))
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
#[tokio::test]
async fn a_host_without_interop_gets_no_bridge() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(materialize(tmp.path(), false).await.unwrap(), None);
assert!(!bridge_path(tmp.path()).exists());
}
#[tokio::test]
async fn an_interop_host_gets_an_executable_current_bridge() {
let tmp = tempfile::tempdir().unwrap();
let path = materialize(tmp.path(), true).await.unwrap().unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), BRIDGE);
assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o755);
std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
materialize(tmp.path(), true).await.unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), BRIDGE);
}
fn fake_system32(rows: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let write = |name: &str, body: &str| {
let p = dir.path().join(name);
std::fs::write(&p, body).unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
};
write("cmd.exe", "#!/bin/sh\nexit 0\n");
write("query.exe", &format!("#!/bin/sh\nprintf '%s' '{rows}'\nexit 1\n"));
dir
}
fn run_bridge(sys32: &Path, args: &[&str]) -> std::process::Output {
let tmp = tempfile::tempdir().unwrap();
let script = tmp.path().join("win-interactive");
std::fs::write(&script, BRIDGE).unwrap();
Command::new("/bin/sh")
.arg(&script)
.args(args)
.env("YAH_WIN_SYSTEM32", sys32)
.output()
.unwrap()
}
#[test]
fn the_bridge_parses_as_posix_sh() {
let tmp = tempfile::tempdir().unwrap();
let script = tmp.path().join("win-interactive");
std::fs::write(&script, BRIDGE).unwrap();
let out = Command::new("/bin/sh").arg("-n").arg(&script).output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn no_arguments_is_a_usage_error() {
let sys32 = fake_system32("");
assert_eq!(run_bridge(sys32.path(), &[]).status.code(), Some(64));
}
#[test]
fn a_host_without_cmd_exe_is_a_bridge_failure_not_a_job_failure() {
let empty = tempfile::tempdir().unwrap();
let out = run_bridge(empty.path(), &["notepad.exe"]);
assert_eq!(out.status.code(), Some(70));
assert!(String::from_utf8_lossy(&out.stderr).contains("no Windows interop"));
}
#[test]
fn no_active_user_session_exits_69_and_says_so() {
let sys32 = fake_system32(
" SESSIONNAME USERNAME ID STATE TYPE DEVICE \r\n\
>services 0 Disc \r\n\
\x20 struc 2 Disc \r\n\
\x20console 3 Conn \r\n\
\x20rdp-tcp 65536 Listen \r\n",
);
let out = run_bridge(sys32.path(), &["notepad.exe"]);
assert_eq!(out.status.code(), Some(69));
assert!(String::from_utf8_lossy(&out.stderr).contains("no interactive session available"));
}
}