mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
Merge pull request #479 from l0ng-ai/perf/wsl-tab-open-cost
perf(wsl): stop re-proving the distro on every new tab
This commit is contained in:
@@ -463,19 +463,103 @@ mod wsl_tests {
|
||||
"registry default {default:?} not in {installed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Windows Terminal drops `Modern = 1` distros from this very key, because
|
||||
/// they hand it a profile fragment separately and it would otherwise list
|
||||
/// them twice. Copying that filter here would hide the ordinary distro on
|
||||
/// an up-to-date machine — on the box this was written on, the only one.
|
||||
#[test]
|
||||
fn a_modern_distro_is_still_offered() {
|
||||
let installed = super::wsl_distros();
|
||||
if installed.is_empty() {
|
||||
eprintln!("skipping: no WSL distributions installed");
|
||||
return;
|
||||
}
|
||||
let modern: Vec<String> = super::registry_user_subkeys(super::LXSS)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|guid| {
|
||||
super::registry_user_dword(&format!(r"{}\{guid}", super::LXSS), "Modern") == Some(1)
|
||||
})
|
||||
.filter_map(|guid| {
|
||||
super::registry_user_string(&format!(r"{}\{guid}", super::LXSS), "DistributionName")
|
||||
})
|
||||
.filter(|name| super::worth_offering(name))
|
||||
.collect();
|
||||
if modern.is_empty() {
|
||||
eprintln!("skipping: no modern WSL distributions installed");
|
||||
return;
|
||||
}
|
||||
for name in &modern {
|
||||
assert!(
|
||||
installed.contains(name),
|
||||
"modern distro {name:?} was dropped from {installed:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listing_the_distros_does_not_wait_on_the_wsl_service() {
|
||||
// Only the registry answer is meant to be fast. When there is none the
|
||||
// fallback to `wsl.exe` is doing exactly what it exists for, and timing
|
||||
// it would fail this test on every machine without WSL installed.
|
||||
if super::registered_wsl_distros().is_none() {
|
||||
eprintln!("skipping: the registry has no distro list to read");
|
||||
return;
|
||||
}
|
||||
let started = std::time::Instant::now();
|
||||
let _ = super::wsl_distros();
|
||||
let elapsed = started.elapsed();
|
||||
assert!(
|
||||
elapsed < std::time::Duration::from_millis(500),
|
||||
"the listing went to `wsl.exe` after all: {elapsed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The list is what the shell menu offers, so a distro that cannot open a
|
||||
/// pane must not be on it: `wsl -l -q`, which this replaced, only ever
|
||||
/// listed installed ones.
|
||||
#[test]
|
||||
fn a_distro_that_is_not_installed_is_not_offered() {
|
||||
let Some(guids) = super::registry_user_subkeys(super::LXSS) else {
|
||||
eprintln!("skipping: the registry has no distro list to read");
|
||||
return;
|
||||
};
|
||||
let half_installed: Vec<String> = guids
|
||||
.iter()
|
||||
.map(|guid| format!(r"{}\{guid}", super::LXSS))
|
||||
.filter(|key| super::registry_user_dword(key, "State").is_some_and(|state| state != 1))
|
||||
.filter_map(|key| super::registry_user_string(&key, "DistributionName"))
|
||||
.collect();
|
||||
if half_installed.is_empty() {
|
||||
eprintln!("skipping: every registered distro finished installing");
|
||||
return;
|
||||
}
|
||||
let offered = super::wsl_distros();
|
||||
for name in &half_installed {
|
||||
assert!(
|
||||
!offered.contains(name),
|
||||
"unfinished distro {name:?} was offered in {offered:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wsl_distros() -> Vec<String> {
|
||||
wsl_distros_probed().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Where `wsl.exe` registers what is installed: one subkey per distro, named by
|
||||
/// GUID, carrying `DistributionName` and `State`.
|
||||
#[cfg(windows)]
|
||||
const LXSS: &str = r"Software\Microsoft\Windows\CurrentVersion\Lxss";
|
||||
|
||||
/// The distro `wsl.exe` launches when no `--distribution` is given, read from
|
||||
/// the registry (`Lxss\DefaultDistribution` names the per-distro key that
|
||||
/// carries `DistributionName`). The registry rather than `wsl -l`: this runs
|
||||
/// on the pane-spawn path, where a microsecond read beats a subprocess.
|
||||
#[cfg(windows)]
|
||||
pub fn default_wsl_distro() -> Option<String> {
|
||||
const LXSS: &str = r"Software\Microsoft\Windows\CurrentVersion\Lxss";
|
||||
let guid = registry_user_string(LXSS, "DefaultDistribution")?;
|
||||
let name = registry_user_string(&format!(r"{LXSS}\{guid}"), "DistributionName")?;
|
||||
(!name.is_empty()).then_some(name)
|
||||
@@ -568,8 +652,161 @@ fn find_git_bash() -> Option<PathBuf> {
|
||||
#[cfg(windows)]
|
||||
const WSL_LIST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
/// Distros that exist to carry a container runtime, not to be typed into.
|
||||
#[cfg_attr(unix, allow(dead_code))]
|
||||
const NOT_FOR_TYPING: [&str; 2] = ["docker-desktop", "rancher-desktop"];
|
||||
|
||||
#[cfg_attr(unix, allow(dead_code))]
|
||||
fn worth_offering(name: &str) -> bool {
|
||||
!name.is_empty() && !NOT_FOR_TYPING.iter().any(|hidden| name.starts_with(hidden))
|
||||
}
|
||||
|
||||
/// The installed distros, from `Lxss` — the same registry key `wsl.exe` itself
|
||||
/// registers them in, and the one `default_wsl_distro` above already reads.
|
||||
///
|
||||
/// Not `wsl -l -q`, because that has to reach the WSL service, and reaching the
|
||||
/// WSL service is exactly the part that can be slow: issue #454 was a machine
|
||||
/// where it took 3.3s, past the timeout below, so the list came back empty
|
||||
/// every time and no distro was ever offered in the shell menu. A registry read
|
||||
/// is microseconds and cannot hang, because nothing is listening on it.
|
||||
///
|
||||
/// Windows Terminal made this same move in 2021 (microsoft/terminal#10967) for
|
||||
/// the same reason, but skips distros whose key carries `Modern = 1`. That is a
|
||||
/// deduplication rule specific to Terminal — modern distros ship it a profile
|
||||
/// fragment of their own, so reading both would list them twice. Nothing ships
|
||||
/// tty7 anything, so we take them all; skipping them here would hide the most
|
||||
/// ordinary distro on an up-to-date machine.
|
||||
///
|
||||
/// `State` we do read, the way Terminal does: a distro is only offered while it
|
||||
/// says 1, "installed". An install that was interrupted — `wsl --install` shut
|
||||
/// down halfway, a failed `--import`, one being uninstalled right now — leaves
|
||||
/// the key behind with a name and some other state, and `wsl -l -q` (which this
|
||||
/// replaced) never listed those. Offering one puts a distro in the shell menu
|
||||
/// that can only open a pane that dies of a WSL registration error.
|
||||
///
|
||||
/// `None` means "could not tell", never "there is nothing": the caller falls
|
||||
/// back to `wsl.exe` on it, and a caller further up keeps the last good list.
|
||||
#[cfg(windows)]
|
||||
fn registered_wsl_distros() -> Option<Vec<String>> {
|
||||
let guids = registry_user_subkeys(LXSS)?;
|
||||
let names: Vec<String> = guids
|
||||
.iter()
|
||||
.map(|guid| format!(r"{LXSS}\{guid}"))
|
||||
// A key with no `State` at all is taken at its word: the absent value
|
||||
// is not evidence of a broken install, and inventing one would be how
|
||||
// this hides a working distro.
|
||||
.filter(|key| registry_user_dword(key, "State").unwrap_or(INSTALLED) == INSTALLED)
|
||||
.filter_map(|key| registry_user_string(&key, "DistributionName"))
|
||||
.filter(|name| worth_offering(name))
|
||||
.collect();
|
||||
|
||||
// Subkeys but nothing to show for them is not an answer either: every name
|
||||
// unreadable has the shape of a permissions problem, not of a machine with
|
||||
// no distros on it — that machine has an empty `Lxss`, and says so.
|
||||
if names.is_empty() && !guids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(names)
|
||||
}
|
||||
|
||||
/// `State` of a distro that finished installing and has not started leaving.
|
||||
#[cfg(windows)]
|
||||
const INSTALLED: u32 = 1;
|
||||
|
||||
#[cfg(windows)]
|
||||
fn registry_user_dword(subkey: &str, value: &str) -> Option<u32> {
|
||||
use windows_sys::Win32::System::Registry::{HKEY_CURRENT_USER, RRF_RT_REG_DWORD, RegGetValueW};
|
||||
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
let (subkey, value) = (wide(subkey), wide(value));
|
||||
let mut data: u32 = 0;
|
||||
let mut size = std::mem::size_of::<u32>() as u32;
|
||||
// SAFETY: both names are NUL-terminated and owned here, and `data` is a
|
||||
// live u32 exactly `size` bytes long, which is what a DWORD read writes.
|
||||
let rc = unsafe {
|
||||
RegGetValueW(
|
||||
HKEY_CURRENT_USER,
|
||||
subkey.as_ptr(),
|
||||
value.as_ptr(),
|
||||
RRF_RT_REG_DWORD,
|
||||
std::ptr::null_mut(),
|
||||
(&raw mut data).cast(),
|
||||
&mut size,
|
||||
)
|
||||
};
|
||||
(rc == 0).then_some(data)
|
||||
}
|
||||
|
||||
/// The names of a key's subkeys, or `None` if they could not all be read.
|
||||
///
|
||||
/// All or nothing on purpose. The list this feeds is what the shell menu offers,
|
||||
/// and a caller that cannot tell a short list from a complete one would quietly
|
||||
/// drop distros: the walk is by index, so a key that changes underneath it —
|
||||
/// `wsl --unregister` running right now, a Store install rewriting `Lxss` —
|
||||
/// ends early, and reporting that as the answer is worse than admitting it.
|
||||
#[cfg(windows)]
|
||||
fn registry_user_subkeys(subkey: &str) -> Option<Vec<String>> {
|
||||
use windows_sys::Win32::Foundation::ERROR_NO_MORE_ITEMS;
|
||||
use windows_sys::Win32::System::Registry::{
|
||||
HKEY, HKEY_CURRENT_USER, KEY_READ, RegCloseKey, RegEnumKeyExW, RegOpenKeyExW,
|
||||
};
|
||||
|
||||
let subkey: Vec<u16> = subkey.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut key: HKEY = std::ptr::null_mut();
|
||||
// SAFETY: `subkey` is NUL-terminated and owned here; `key` is written only
|
||||
// on success and closed on every path out below.
|
||||
if unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, subkey.as_ptr(), 0, KEY_READ, &mut key) } != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut names = Vec::new();
|
||||
// A registry key name is at most 255 characters, plus the terminator.
|
||||
let mut buf = [0u16; 256];
|
||||
let mut ended_with = None;
|
||||
for index in 0.. {
|
||||
let mut len = buf.len() as u32;
|
||||
// SAFETY: `buf` really is `len` units long, and every pointer that is
|
||||
// not wanted is null, which this call documents as "do not report it".
|
||||
let rc = unsafe {
|
||||
RegEnumKeyExW(
|
||||
key,
|
||||
index,
|
||||
buf.as_mut_ptr(),
|
||||
&mut len,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if rc != 0 {
|
||||
ended_with = Some(rc);
|
||||
break;
|
||||
}
|
||||
names.push(String::from_utf16_lossy(&buf[..len as usize]));
|
||||
}
|
||||
|
||||
// SAFETY: `key` was opened above and is not used after this.
|
||||
unsafe { RegCloseKey(key) };
|
||||
// There is one honest way for the walk to end. Anything else — the key
|
||||
// deleted underneath it, a name that would not fit — leaves a list that is
|
||||
// short by an unknown amount, which nobody downstream can tell from a real
|
||||
// one, so say nothing instead.
|
||||
(ended_with == Some(ERROR_NO_MORE_ITEMS)).then_some(names)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn list_wsl_distros() -> Option<Vec<String>> {
|
||||
if let Some(registered) = registered_wsl_distros() {
|
||||
return Some(registered);
|
||||
}
|
||||
|
||||
// The registry would not answer — no `Lxss` key at all, or a walk of it that
|
||||
// ended somewhere other than the end. Either way this is not a "there are no
|
||||
// distros" to pass on, so ask the slow way rather than claim there is nothing.
|
||||
log::debug!("{LXSS} gave no usable answer; falling back to `wsl -l -q`");
|
||||
let mut cmd = std::process::Command::new("wsl.exe");
|
||||
cmd.args(["-l", "-q"]);
|
||||
let output = match crate::core::proc::output_within(
|
||||
@@ -597,7 +834,7 @@ fn parse_wsl_list(bytes: &[u8]) -> Vec<String> {
|
||||
let text = String::from_utf16_lossy(&units);
|
||||
text.lines()
|
||||
.map(|l| l.trim_matches(|c: char| c.is_whitespace() || c == '\u{feff}' || c == '\0'))
|
||||
.filter(|l| !l.is_empty() && !l.starts_with("docker-desktop"))
|
||||
.filter(|l| worth_offering(l))
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -565,10 +565,100 @@ fn install_lock(distro: &str) -> Arc<Mutex<()>> {
|
||||
lock
|
||||
}
|
||||
|
||||
/// Where a distro's server was last proved to be, so the next pane can skip the
|
||||
/// proving.
|
||||
///
|
||||
/// `Installer::run` costs five serial `wsl.exe` round trips — `uname`, `$HOME`,
|
||||
/// a stat, a liveness probe, and a look at what is running. That is a fine price
|
||||
/// to pay once for a distro, and an absurd one to pay per pane: issue #454 was a
|
||||
/// machine where one round trip took 3.3s, so opening a second tab on a distro
|
||||
/// that was already connected cost half a minute to re-learn what the first tab
|
||||
/// had just learned.
|
||||
///
|
||||
/// Nothing here expires on a timer, because the answer barely rots:
|
||||
///
|
||||
/// - The binary does not move. A tty7 upgrade renames it, but a new build is a
|
||||
/// new process and this map lives only in memory, so it starts empty.
|
||||
/// - The distro shutting down does not invalidate it either. `wsl.exe` starts a
|
||||
/// stopped distro on demand, and the bridge (`tty7-server --stdio --pane`)
|
||||
/// starts its own daemon if none is listening — so the one thing that really
|
||||
/// does stop being true, "a daemon is running in there", is repaired a layer
|
||||
/// below us without anyone asking.
|
||||
///
|
||||
/// What is left is a path that could stop existing: the distro reinstalled, the
|
||||
/// binary deleted by hand. Only the bridge discovers that, and only once it is
|
||||
/// running — so the router forgets the distro when a bridge dies without ever
|
||||
/// answering, and the pane after that proves it again. See `forget_wsl_server`.
|
||||
static READY: Mutex<Vec<(String, Proved)>> = Mutex::new(Vec::new());
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Proved {
|
||||
binary: String,
|
||||
/// The build mismatch the probe found, if it found one.
|
||||
///
|
||||
/// Kept because the warning is produced inside `Installer::run`, and the
|
||||
/// whole point of the note is that `run` does not happen again: without
|
||||
/// this, only the first pane of the daemon's lifetime would ever hear that
|
||||
/// a different build is serving the distro, and every window opened after
|
||||
/// it — including a whole new GUI session, since the daemon outlives one —
|
||||
/// would attach in silence.
|
||||
mismatch: Option<super::MismatchedRemoteDaemon>,
|
||||
}
|
||||
|
||||
fn remembered(distro: &str) -> Option<Proved> {
|
||||
READY
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.iter()
|
||||
.find(|(d, _)| d == distro)
|
||||
.map(|(_, proved)| proved.clone())
|
||||
}
|
||||
|
||||
fn remember(distro: &str, proved: Proved) {
|
||||
let mut ready = READY
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
match ready.iter_mut().find(|(d, _)| d == distro) {
|
||||
Some((_, known)) => *known = proved,
|
||||
None => ready.push((distro.to_string(), proved)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where this distro's server was last proved to be, or `None` if the next pane
|
||||
/// would have to go and ask. A hint for callers deciding whether a failure is
|
||||
/// worth re-proving; the answer itself comes from `ensure_wsl_server`.
|
||||
pub fn remembered_wsl_server(distro: &str) -> Option<String> {
|
||||
remembered(distro).map(|proved| proved.binary)
|
||||
}
|
||||
|
||||
/// Drop what we thought we knew about a distro, so the next `ensure_wsl_server`
|
||||
/// proves it again the long way.
|
||||
pub fn forget_wsl_server(distro: &str) {
|
||||
READY
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.retain(|(d, _)| d != distro);
|
||||
}
|
||||
|
||||
pub fn ensure_wsl_server(distro: &str) -> io::Result<String> {
|
||||
validate_distro(distro)?;
|
||||
|
||||
// Under the lock even when the answer is only going to be read, because the
|
||||
// note names a file that `replace_wsl_server` is in the business of moving:
|
||||
// reading it outside would let a pane spawn the very binary a replace is
|
||||
// deleting. The lock is uncontended except during an install, and waiting
|
||||
// for an install to finish is what a pane wants to do anyway.
|
||||
let lock = install_lock(distro);
|
||||
let _held = lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if let Some(proved) = remembered(distro) {
|
||||
// Re-file the warning rather than re-run the probe that found it: this
|
||||
// route's sink is a fresh one, and the client on the other end of it
|
||||
// has not heard about the mismatch yet.
|
||||
if let Some(mismatch) = proved.mismatch {
|
||||
super::record_remote_mismatches(vec![mismatch]);
|
||||
}
|
||||
return Ok(proved.binary);
|
||||
}
|
||||
|
||||
let ops = WslRemoteOps::new(distro);
|
||||
let source = BundledServerBinary::discover();
|
||||
@@ -595,6 +685,13 @@ pub fn ensure_wsl_server(distro: &str) -> io::Result<String> {
|
||||
""
|
||||
},
|
||||
);
|
||||
remember(
|
||||
distro,
|
||||
Proved {
|
||||
binary: report.paths.binary.clone(),
|
||||
mismatch: report.mismatch,
|
||||
},
|
||||
);
|
||||
Ok(report.paths.binary)
|
||||
}
|
||||
|
||||
@@ -605,6 +702,11 @@ pub fn restart_wsl_daemon(distro: &str) -> io::Result<()> {
|
||||
let confirm = install_confirm();
|
||||
let lock = install_lock(distro);
|
||||
let _held = lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
// Both of these deliberately change what is running in there, which is the
|
||||
// one thing the remembered answer is a claim about. Forget it first: if the
|
||||
// restart fails halfway, the next pane must go and look rather than trust a
|
||||
// note written before the upheaval.
|
||||
forget_wsl_server(distro);
|
||||
Installer::with_source(&ops, &source, confirm.as_ref(), host_label(distro)).restart_daemon()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -619,6 +721,7 @@ pub fn replace_wsl_server(distro: &str) -> io::Result<()> {
|
||||
let confirm = install_confirm();
|
||||
let lock = install_lock(distro);
|
||||
let _held = lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
forget_wsl_server(distro);
|
||||
Installer::with_source(&ops, &source, confirm.as_ref(), host_label(distro)).replace()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1561,6 +1664,147 @@ mod tests {
|
||||
assert!(a2.try_lock().is_ok());
|
||||
}
|
||||
|
||||
/// Names no real distribution can have, so these tests never touch one and
|
||||
/// never collide with each other when the suite runs in parallel.
|
||||
fn nowhere(test: &str) -> String {
|
||||
format!("tty7-no-such-distro-{test}")
|
||||
}
|
||||
|
||||
fn note(binary: &str) -> Proved {
|
||||
Proved {
|
||||
binary: binary.to_string(),
|
||||
mismatch: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remembered_distro_is_answered_without_asking_wsl_anything() {
|
||||
let distro = nowhere("remembered");
|
||||
let binary = "/home/me/.local/share/tty7/bin/tty7-server-c5p5";
|
||||
remember(&distro, note(binary));
|
||||
|
||||
// There is no such distribution, so a probe could only have failed:
|
||||
// getting the path back at all is what proves none ran.
|
||||
let answered = ensure_wsl_server(&distro).expect("the note is the answer");
|
||||
assert_eq!(answered, binary);
|
||||
|
||||
forget_wsl_server(&distro);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forgetting_sends_the_next_caller_back_to_the_distribution() {
|
||||
let distro = nowhere("forgotten");
|
||||
remember(&distro, note("/somewhere/tty7-server"));
|
||||
assert!(remembered_wsl_server(&distro).is_some());
|
||||
|
||||
forget_wsl_server(&distro);
|
||||
assert_eq!(remembered_wsl_server(&distro), None);
|
||||
assert!(
|
||||
ensure_wsl_server(&distro).is_err(),
|
||||
"a forgotten distro must be proved again, not assumed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_is_remembered_is_per_distro_and_replaceable() {
|
||||
let (a, b) = (nowhere("map-a"), nowhere("map-b"));
|
||||
remember(&a, note("/a/tty7-server"));
|
||||
remember(&b, note("/b/tty7-server"));
|
||||
assert_eq!(remembered_wsl_server(&a).as_deref(), Some("/a/tty7-server"));
|
||||
|
||||
forget_wsl_server(&a);
|
||||
assert_eq!(remembered_wsl_server(&a), None);
|
||||
assert_eq!(
|
||||
remembered_wsl_server(&b).as_deref(),
|
||||
Some("/b/tty7-server"),
|
||||
"forgetting one distro must not forget another"
|
||||
);
|
||||
|
||||
remember(&b, note("/b/tty7-server-newer"));
|
||||
assert_eq!(
|
||||
remembered_wsl_server(&b).as_deref(),
|
||||
Some("/b/tty7-server-newer"),
|
||||
"a later answer replaces the earlier one"
|
||||
);
|
||||
forget_wsl_server(&b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_restart_forgets_first_so_a_failed_one_leaves_no_stale_note() {
|
||||
let distro = nowhere("restart");
|
||||
remember(&distro, note("/x/tty7-server"));
|
||||
|
||||
// This cannot succeed — there is no such distribution — which is the
|
||||
// point: the note must be gone even though the work after it failed.
|
||||
let _ = restart_wsl_daemon(&distro);
|
||||
assert_eq!(remembered_wsl_server(&distro), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remembered_answer_waits_for_an_install_to_let_go_of_the_binary() {
|
||||
let distro = nowhere("locked");
|
||||
remember(&distro, note("/x/tty7-server"));
|
||||
|
||||
// Stand in for a `replace_wsl_server` in progress: it holds this lock
|
||||
// while it moves the very file the note names.
|
||||
let lock = install_lock(&distro);
|
||||
let held = lock.lock().expect("a lock nobody else has");
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let asking = {
|
||||
let distro = distro.clone();
|
||||
std::thread::spawn(move || tx.send(ensure_wsl_server(&distro)))
|
||||
};
|
||||
assert!(
|
||||
rx.recv_timeout(Duration::from_millis(250)).is_err(),
|
||||
"the note was handed out while a replace was under way"
|
||||
);
|
||||
|
||||
drop(held);
|
||||
let answered = rx
|
||||
.recv_timeout(Duration::from_secs(5))
|
||||
.expect("answered once the install let go")
|
||||
.expect("the note is the answer");
|
||||
assert_eq!(answered, "/x/tty7-server");
|
||||
let _ = asking.join();
|
||||
|
||||
forget_wsl_server(&distro);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remembered_mismatch_is_told_to_every_later_pane() {
|
||||
let distro = nowhere("mismatch");
|
||||
let entry = crate::daemon::install::MismatchedRemoteDaemon {
|
||||
host: host_label(&distro),
|
||||
running_version: Some("0.0.1".to_string()),
|
||||
running_exe: Some("/x/tty7-server-someone-elses".to_string()),
|
||||
wanted_version: "9.9.9".to_string(),
|
||||
};
|
||||
remember(
|
||||
&distro,
|
||||
Proved {
|
||||
binary: "/x/tty7-server".to_string(),
|
||||
mismatch: Some(entry.clone()),
|
||||
},
|
||||
);
|
||||
|
||||
// A later pane is a fresh route with a fresh sink, and the client on
|
||||
// the other end of it has never been told.
|
||||
let sink = Arc::new(Mutex::new(Vec::new()));
|
||||
let answered =
|
||||
crate::daemon::install::with_mismatch_sink(sink.clone(), || ensure_wsl_server(&distro))
|
||||
.expect("the note is the answer");
|
||||
|
||||
assert_eq!(answered, "/x/tty7-server");
|
||||
assert_eq!(
|
||||
&*sink.lock().expect("the sink"),
|
||||
&[entry],
|
||||
"the warning stopped at the first pane"
|
||||
);
|
||||
|
||||
forget_wsl_server(&distro);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_refuses_an_unusable_distro_name_before_spawning_anything() {
|
||||
let err = ensure_wsl_server("--shutdown").expect_err("refused");
|
||||
|
||||
@@ -605,7 +605,25 @@ async fn drive(local: Stream, header: &RouteHeader) -> io::Result<()> {
|
||||
if !leftover.is_empty() {
|
||||
tokio::io::AsyncWriteExt::write_all(&mut *link, &leftover).await?;
|
||||
}
|
||||
let (to_remote, to_local) = tokio::io::copy_bidirectional(&mut local, &mut *link).await?;
|
||||
let copied = tokio::io::copy_bidirectional(&mut local, &mut *link).await;
|
||||
// A bridge that never sent a byte never ran. This is where a stale note is
|
||||
// actually found out: `wsl.exe` spawns quite happily with a server path
|
||||
// that no longer exists inside the distro — the distro was reinstalled, the
|
||||
// directory was cleaned out — and only fails once it is the shell trying to
|
||||
// exec it. Forget the distro, so the pane after this one proves it again
|
||||
// rather than repeating a failure that would otherwise outlive every window
|
||||
// and last until tty7 itself restarts.
|
||||
if let RouteTarget::Wsl { distro } = &header.target
|
||||
&& header.server_command.is_none()
|
||||
&& !copied
|
||||
.as_ref()
|
||||
.is_ok_and(|(_, from_remote)| *from_remote > 0)
|
||||
{
|
||||
log::info!("wsl:{distro}: the bridge closed without answering; proving it again next time");
|
||||
crate::daemon::install::wsl::forget_wsl_server(distro);
|
||||
}
|
||||
|
||||
let (to_remote, to_local) = copied?;
|
||||
log::debug!("routed connection closed after {to_remote} up / {to_local} down bytes");
|
||||
drop(conn);
|
||||
Ok(())
|
||||
@@ -742,6 +760,15 @@ async fn restart_server(
|
||||
}
|
||||
}
|
||||
|
||||
/// Prove (or recall) where this distro's server is, off the reactor — the probe
|
||||
/// is a chain of blocking `wsl.exe` calls the first time round.
|
||||
async fn ensure_wsl_server(distro: &str, setup: &RouteSetup) -> anyhow::Result<String> {
|
||||
let distro = distro.to_string();
|
||||
Ok(setup
|
||||
.blocking(move || crate::daemon::install::wsl::ensure_wsl_server(&distro))
|
||||
.await??)
|
||||
}
|
||||
|
||||
async fn open_link(
|
||||
header: &RouteHeader,
|
||||
setup: &RouteSetup,
|
||||
@@ -754,25 +781,30 @@ async fn open_link(
|
||||
Ok((link, Some(conn)))
|
||||
}
|
||||
RouteTarget::Wsl { distro } => {
|
||||
let resolved = match header.server_command {
|
||||
Some(_) => None,
|
||||
None => {
|
||||
let distro = distro.clone();
|
||||
Some(
|
||||
setup
|
||||
.blocking(move || {
|
||||
crate::daemon::install::wsl::ensure_wsl_server(&distro)
|
||||
})
|
||||
.await??,
|
||||
)
|
||||
if let Some(command) = header.server_command.as_deref() {
|
||||
let link = RemoteLink::wsl_shell(distro, command, setup.channel)?;
|
||||
return Ok((link, None));
|
||||
}
|
||||
|
||||
let from_memory = crate::daemon::install::wsl::remembered_wsl_server(distro).is_some();
|
||||
let binary = ensure_wsl_server(distro, setup).await?;
|
||||
match RemoteLink::wsl(distro, &binary, setup.channel) {
|
||||
Ok(link) => Ok((link, None)),
|
||||
// Only worth a second look when the path came from memory: one
|
||||
// proved a moment ago will prove the same, and re-proving it
|
||||
// just doubles the wait before the error reaches the user.
|
||||
Err(stale) if from_memory => {
|
||||
log::info!(
|
||||
"wsl:{distro}: the remembered server would not start ({stale}); \
|
||||
looking again"
|
||||
);
|
||||
crate::daemon::install::wsl::forget_wsl_server(distro);
|
||||
let binary = ensure_wsl_server(distro, setup).await?;
|
||||
let link = RemoteLink::wsl(distro, &binary, setup.channel)?;
|
||||
Ok((link, None))
|
||||
}
|
||||
};
|
||||
let link = match (header.server_command.as_deref(), resolved.as_deref()) {
|
||||
(Some(command), _) => RemoteLink::wsl_shell(distro, command, setup.channel)?,
|
||||
(None, Some(binary)) => RemoteLink::wsl(distro, binary, setup.channel)?,
|
||||
(None, None) => unreachable!("resolved is Some whenever there is no override"),
|
||||
};
|
||||
Ok((link, None))
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
RouteTarget::LocalStdio { program, args } => {
|
||||
let args: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
|
||||
+12
-5
@@ -2119,11 +2119,18 @@ fn connect_routed(route: &PaneRoute) -> anyhow::Result<Stream> {
|
||||
|
||||
tty7_core::host::guard_off_ui();
|
||||
|
||||
if let crate::daemon::router::RouteTarget::Wsl { distro } = &header.target {
|
||||
crate::daemon::install::wsl::ensure_wsl_server(distro)
|
||||
.map_err(|e| anyhow::anyhow!("prepare tty7-server in WSL `{distro}`: {e}"))?;
|
||||
}
|
||||
|
||||
// No `ensure_wsl_server` here on purpose. The daemon runs exactly the same
|
||||
// probe inside `router::open_link` before it opens the link, so asking from
|
||||
// this side too bought nothing and cost a second full round of `wsl.exe`
|
||||
// invocations — five of them, serially, on every single pane. On a machine
|
||||
// where a `wsl.exe` round trip is slow (issue #454 measured 3.3s) that
|
||||
// duplicate was half of the wait before a new tab could take a key.
|
||||
//
|
||||
// Nothing is lost by dropping it: the returned path was discarded, the
|
||||
// failure is reported just as well through the route ack below, and the
|
||||
// first-install consent question still reaches this process — the daemon
|
||||
// runs its probe under `RouteSetup::blocking`, which installs the relay
|
||||
// that turns the question into a frame on this very connection.
|
||||
let mut stream = connect()?;
|
||||
let ack = crate::daemon::router::negotiate(&mut stream, header)
|
||||
.map_err(|e| anyhow::anyhow!("route this pane to {}: {e}", header.describe()))?;
|
||||
|
||||
Reference in New Issue
Block a user