дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
* perf(vfs): list directories off the main thread and reuse the repository lookup Opening a folder on a network share (\\wsl.localhost\<distro>\...) is slow because every listing ran on the main thread and walked up to the repository root and ran a repository discovery again. - fs_read_dir runs on the blocking pool. - GitignoreStack::cached keeps the repository root and the repo-level rules for a few seconds. An edit of .git/info/exclude is still seen at once (its mtime and size are checked) and every .gitignore / .ignore is read fresh. Refs #916 * feat(remote): open WSL projects through a WSL transport A folder of a WSL distro (\\wsl.localhost\<distro>\... or \\wsl$\...) now opens as a remote project whose openidea-server runs inside the distro, so the terminal, SDK discovery, run, language servers and git are native Linux. - RemoteTarget (ssh | wsl) replaces the SSH configuration id in the remote launch, connection, session and recents; old recents are read as SSH and a WSL target never inherits the legacy trust migration. - remote_wsl: probe the distro, stream the server into ~/.openidea/server and start it with `wsl.exe -d <distro> --exec <bin> --stdio`. Distro names are validated and every command goes through --exec with positional arguments. - wsl_distros / wsl_resolve commands; File > Open, the CLI and recents route WSL folders to the distro and focus the window that already has them. - Remote Development dialog: SSH/WSL choice with the detected distros; the status bar shows `WSL: <distro>` / `SSH: <host>`. - ide-jsonrpc: skip the NUL left at the start of a line by UTF-16 output of wsl.exe, which made the first message of the server disappear. Not run on Windows yet: only a stand-in for wsl.exe is tested. Refs #925, #327, #721 * fix(remote): satisfy clippy in WSL output decoding * style(run): format run types * fix: regenerate bindings and drop untranslated WSL catalog entry * test(i18n): list WSL as an untranslated product name * fix(run): pick build wrappers and shell quoting by the execution host A remote window runs commands on its Linux or macOS server, so a Windows desktop no longer selects gradlew.bat, mvnw.cmd or Windows quoting for it. * fix(welcome): open legacy WSL UNC recents and sessions through the distro Recents and the restored session called the local openProject for a \\wsl.localhost folder saved before WSL projects, which fails with OS error 123. They now resolve it like the other entry points. * fix(windows): keep WSL files out of remote project launches path_launch tested the WSL share before is_dir, so a first-instance CLI launch with a WSL file opened a remote project instead of LightEdit. * fix(remote): run-command OS comes from the remote host, not an assumption The server's OS was dropped after connect, so a restored session had none and isWindows() assumed every remote host is Unix. Keep it on the connection and the session, and resolve it through hostOs(). --------- Co-authored-by: NguyenBang <[email protected]> Co-authored-by: NguyenBang <[email protected]> Co-authored-by: l0ng-ai <[email protected]>
138 рядки
5.1 KiB
Rust
138 рядки
5.1 KiB
Rust
//! Projects that live inside a WSL distro are reached from Windows through the
|
|
//! `\\wsl.localhost\<distro>` (or legacy `\\wsl$\<distro>`) share.
|
|
|
|
use std::path::Path;
|
|
|
|
/// The WSL launcher on Windows.
|
|
pub const EXE: &str = "wsl.exe";
|
|
|
|
/// A location inside a WSL distro.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct WslPath {
|
|
pub distro: String,
|
|
/// Absolute Linux path, e.g. `/home/me/app`.
|
|
pub path: String,
|
|
}
|
|
|
|
/// Splits a WSL share path into distro and Linux path. Pure string work, so it
|
|
/// accepts every spelling the UI and the OS produce: `\\wsl.localhost\X\a`,
|
|
/// `//wsl$/X/a` and the verbatim `\\?\UNC\wsl.localhost\X\a`.
|
|
pub fn parse(path: &Path) -> Option<WslPath> {
|
|
let s = path.to_string_lossy().replace('\\', "/");
|
|
let s = match s.strip_prefix("//?/UNC/") {
|
|
Some(rest) => format!("//{rest}"),
|
|
None => s,
|
|
};
|
|
let rest = s.strip_prefix("//")?;
|
|
let (host, rest) = rest.split_once('/')?;
|
|
if !host.eq_ignore_ascii_case("wsl.localhost") && !host.eq_ignore_ascii_case("wsl$") {
|
|
return None;
|
|
}
|
|
let (distro, tail) = rest.split_once('/').unwrap_or((rest, ""));
|
|
if distro.is_empty() {
|
|
return None;
|
|
}
|
|
Some(WslPath { distro: distro.to_owned(), path: format!("/{}", tail.trim_end_matches('/')) })
|
|
}
|
|
|
|
/// A distro name `wsl.exe -d` can be given safely: Windows creates them from letters, digits,
|
|
/// `.`, `_` and `-`. A leading `-` would be read as an option, so it is refused too.
|
|
pub fn is_valid_distro(name: &str) -> bool {
|
|
!name.is_empty()
|
|
&& name.len() <= 64
|
|
&& !name.starts_with('-')
|
|
&& name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
|
|
}
|
|
|
|
/// Text of `wsl.exe`'s own output: UTF-16LE (with or without BOM) for its management commands
|
|
/// and its error messages, UTF-8 for what a command inside the distro prints.
|
|
pub fn decode_output(raw: &[u8]) -> String {
|
|
if raw.starts_with(&[0xff, 0xfe]) || raw.get(1) == Some(&0) {
|
|
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
|
|
String::from_utf16_lossy(&units)
|
|
} else {
|
|
String::from_utf8_lossy(raw).into_owned()
|
|
}
|
|
}
|
|
|
|
/// Distro names from the output of `wsl.exe -l -q`. That output is UTF-16LE (UTF-8 when
|
|
/// `WSL_UTF8=1`), one name per line; names that fail [`is_valid_distro`] are dropped, which also
|
|
/// removes the localized "no installed distributions" message `wsl.exe` prints when there are none.
|
|
pub fn parse_distro_list(raw: &[u8]) -> Vec<String> {
|
|
let text = decode_output(raw);
|
|
let mut names: Vec<String> = Vec::new();
|
|
for line in text.lines() {
|
|
let name = line.trim_matches(|c: char| c.is_whitespace() || c == '\u{feff}');
|
|
if is_valid_distro(name) && !names.iter().any(|n| n == name) {
|
|
names.push(name.to_owned());
|
|
}
|
|
}
|
|
names
|
|
}
|
|
|
|
/// Installed WSL distros. Empty off Windows, when WSL is not installed or when `wsl.exe` fails.
|
|
pub fn list_distros() -> Vec<String> {
|
|
#[cfg(windows)]
|
|
{
|
|
let mut cmd = std::process::Command::new(EXE);
|
|
cmd.args(["-l", "-q"]);
|
|
crate::process::prepare(&mut cmd);
|
|
match cmd.output() {
|
|
Ok(out) if out.status.success() => parse_distro_list(&out.stdout),
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
#[cfg(not(windows))]
|
|
Vec::new()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn p(s: &str) -> Option<(String, String)> {
|
|
parse(Path::new(s)).map(|w| (w.distro, w.path))
|
|
}
|
|
|
|
#[test]
|
|
fn every_spelling_of_the_share_is_recognised() {
|
|
let want = Some(("Ubuntu-24.04".to_owned(), "/home/me/app".to_owned()));
|
|
assert_eq!(p(r"\\wsl.localhost\Ubuntu-24.04\home\me\app"), want);
|
|
assert_eq!(p("//wsl.localhost/Ubuntu-24.04/home/me/app"), want);
|
|
assert_eq!(p(r"\\wsl$\Ubuntu-24.04\home\me\app\"), want);
|
|
assert_eq!(p(r"\\?\UNC\WSL.LOCALHOST\Ubuntu-24.04\home\me\app"), want);
|
|
}
|
|
|
|
#[test]
|
|
fn distro_root_and_foreign_paths() {
|
|
assert_eq!(p(r"\\wsl.localhost\Debian"), Some(("Debian".to_owned(), "/".to_owned())));
|
|
assert_eq!(p(r"\\server\share\a"), None);
|
|
assert_eq!(p(r"\\wsl.localhost\"), None);
|
|
assert_eq!(p("C:/Users/me"), None);
|
|
assert_eq!(p("/home/me"), None);
|
|
}
|
|
|
|
fn utf16(s: &str) -> Vec<u8> {
|
|
std::iter::once(0xfeffu16).chain(s.encode_utf16()).flat_map(u16::to_le_bytes).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn distro_list_is_decoded_from_utf16_and_utf8() {
|
|
let want = vec!["Ubuntu-24.04".to_owned(), "Debian".to_owned()];
|
|
assert_eq!(parse_distro_list(&utf16("Ubuntu-24.04\r\nDebian\r\n\r\n")), want);
|
|
assert_eq!(parse_distro_list(b"Ubuntu-24.04\nDebian\nDebian\n"), want);
|
|
assert!(
|
|
parse_distro_list(&utf16("Windows Subsystem for Linux has no installed distributions.\r\n")).is_empty()
|
|
);
|
|
assert!(parse_distro_list(b"").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn distro_names_cannot_smuggle_options_or_paths() {
|
|
for bad in ["", "-d", "--exec", "a b", "a/b", r"a\b", "a;b", "a$(x)", "a\"b"] {
|
|
assert!(!is_valid_distro(bad), "{bad}");
|
|
}
|
|
assert!(is_valid_distro("Ubuntu-22.04") && is_valid_distro("docker_desktop"));
|
|
}
|
|
}
|