fix(agents): detect ZCode in custom Windows install locations

- Discover installation paths from registry entries and shortcuts
- Handle installer records without InstallLocation
- Avoid launching the desktop app when version metadata is unavailable
- Add regression tests for path resolution and version detection
This commit is contained in:
lzt404 committed 2026-09-11 16:25:14 +08:00
1 parent 6d7f3c8480
commit 145b585c3f
2 files changed
+243 -17

No files matched your search

+129 -17
View File
@@ -938,7 +938,7 @@ pub(crate) fn inspect_agent_config(
.as_deref()
.and_then(read_opencode_desktop_version),
AgentClient::DeepSeekHarness => read_deepseek_harness_profile_version(home),
AgentClient::ZCode => read_zcode_app_version(home),
AgentClient::ZCode => executable.as_deref().and_then(read_zcode_app_version),
_ => None,
};
let version = cli_version.clone().or_else(|| app_version.clone());
@@ -2017,30 +2017,22 @@ pub(crate) fn read_codex_app_installation_version(
}
}
pub(crate) fn read_zcode_app_version(home: &Path) -> Option<String> {
pub(crate) fn read_zcode_app_version(executable: &Path) -> Option<String> {
#[cfg(target_os = "windows")]
{
find_zcode_desktop_executable(home).and_then(|path| {
read_windows_executable_version(&path).or_else(|| read_agent_version(&path, home))
})
// Missing version metadata must not cause the desktop app to be launched.
read_windows_executable_version(executable)
}
#[cfg(target_os = "macos")]
{
[
PathBuf::from("/Applications/ZCode.app"),
home.join("Applications/ZCode.app"),
]
.into_iter()
.find(|path| path.is_dir())
.and_then(|path| read_macos_app_version(&path))
.or_else(|| {
find_named_agent_executable(home, &["zcode"])
.and_then(|path| read_agent_version(&path, home))
})
let application = executable.ancestors().find(|path| {
path.extension().and_then(|value| value.to_str()) == Some("app")
})?;
read_macos_app_version(application)
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let _ = home;
let _ = executable;
None
}
}
@@ -2050,6 +2042,7 @@ pub(crate) fn find_zcode_desktop_executable(home: &Path) -> Option<PathBuf> {
{
let local = env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| home.join("AppData/Local"));
let mut candidates = vec![
local.join("Programs/ZCode/ZCode.exe"),
@@ -2067,6 +2060,7 @@ pub(crate) fn find_zcode_desktop_executable(home: &Path) -> Option<PathBuf> {
.into_iter()
.find(|path| path.is_file())
.or_else(|| find_named_agent_executable(home, &["zcode"]))
.or_else(find_windows_registered_zcode_executable)
}
#[cfg(target_os = "macos")]
{
@@ -2097,6 +2091,124 @@ pub(crate) fn find_zcode_desktop_executable(home: &Path) -> Option<PathBuf> {
}
}
#[cfg(target_os = "windows")]
pub(crate) fn find_windows_registered_zcode_executable() -> Option<PathBuf> {
// Electron/NSIS permits arbitrary installation directories. Its uninstall
// record may omit InstallLocation and point DisplayIcon at an .ico file.
const DISCOVERY_SCRIPT: &str = r#"
$ErrorActionPreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
$registryRoots = @(
'HKCU:\Software\Microsoft\Windows\CurrentVersion',
'HKCU:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion',
'HKLM:\Software\Microsoft\Windows\CurrentVersion',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion'
)
$candidates = @(
foreach ($root in $registryRoots) {
$appPath = Get-Item -LiteralPath "$root\App Paths\ZCode.exe"
if ($appPath) {
$value = $appPath.GetValue('')
if ($value) { @{ kind = 'executable'; value = [string]$value } }
}
foreach ($key in (Get-ChildItem -LiteralPath "$root\Uninstall")) {
$entry = Get-ItemProperty -LiteralPath $key.PSPath
if ($entry.DisplayName -notmatch '^ZCode(?:$|[\s(])') { continue }
if ($entry.InstallLocation) {
@{ kind = 'directory'; value = [string]$entry.InstallLocation }
}
if ($entry.DisplayIcon) {
@{ kind = 'icon'; value = [string]$entry.DisplayIcon }
}
if ($entry.UninstallString) {
@{ kind = 'uninstaller'; value = [string]$entry.UninstallString }
}
}
}
$shortcutRoots = @(
[Environment]::GetFolderPath('Programs'),
[Environment]::GetFolderPath('CommonPrograms'),
[Environment]::GetFolderPath('DesktopDirectory'),
[Environment]::GetFolderPath('CommonDesktopDirectory')
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) }
$shell = New-Object -ComObject WScript.Shell
foreach ($root in $shortcutRoots) {
foreach ($file in (Get-ChildItem -LiteralPath $root -Filter '*ZCode*.lnk' -Recurse)) {
$target = $shell.CreateShortcut($file.FullName).TargetPath
if ($target -and [System.IO.Path]::GetFileName($target) -ieq 'ZCode.exe') {
@{ kind = 'executable'; value = [string]$target }
}
}
}
)
ConvertTo-Json -InputObject $candidates -Compress
"#;
let encoded_command = windows_powershell_encoded_command(DISCOVERY_SCRIPT);
let mut command = Command::new(windows_powershell_executable());
command.args([
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
&encoded_command,
]);
configure_background_command(&mut command);
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
}
parse_windows_zcode_discovery_output(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(target_os = "windows")]
pub(crate) fn parse_windows_zcode_discovery_output(output: &str) -> Option<PathBuf> {
let candidates: Vec<serde_json::Value> = serde_json::from_str(output.trim()).ok()?;
candidates.into_iter().find_map(|candidate| {
let kind = candidate.get("kind")?.as_str()?;
let value = candidate.get("value")?.as_str()?.trim();
let value = if let Some(quoted) = value.strip_prefix('"') {
quoted.split_once('"')?.0
} else if kind == "uninstaller" {
// Never execute UninstallString; only use its executable's directory.
let end = value.to_ascii_lowercase().match_indices(".exe").find_map(
|(index, extension)| {
let end = index + extension.len();
(value[end..].is_empty() || value[end..].starts_with(char::is_whitespace))
.then_some(end)
},
)?;
&value[..end]
} else if kind == "icon" {
value
.rsplit_once(',')
.filter(|(_, index)| index.trim().parse::<i32>().is_ok())
.map(|(path, _)| path.trim())
.unwrap_or(value)
} else {
value
};
let path = PathBuf::from(value);
if !path.is_absolute() {
return None;
}
let executable = match kind {
"executable" => path,
"directory" => path.join("ZCode.exe"),
"icon" | "uninstaller" => path.parent()?.join("ZCode.exe"),
_ => return None,
};
(executable
.file_name()?
.to_str()?
.eq_ignore_ascii_case("ZCode.exe")
&& executable.is_file())
.then_some(executable)
})
}
#[cfg(target_os = "macos")]
pub(crate) fn read_macos_app_version(application: &Path) -> Option<String> {
let info_plist = application.join("Contents/Info.plist");
+114
View File
@@ -58,3 +58,117 @@ fn configuration_paths_ignore_inherited_environment() {
}
fs::remove_dir_all(home).unwrap();
}
#[test]
fn zcode_installation_does_not_require_version_metadata() {
assert!(agent_installation_detected(
AgentClient::ZCode,
None,
true,
false,
));
assert!(!agent_installation_detected(
AgentClient::ZCode,
None,
false,
false,
));
let targets = agent_launch_targets(
AgentClient::ZCode,
Some(Path::new("ZCode.exe")),
None,
false,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].id, "app");
}
#[cfg(target_os = "windows")]
#[test]
fn zcode_windows_finds_custom_installations_from_registered_paths() {
let home = agent_test_home("zcode-custom-installation");
let directory = home.join("自定义应用 [桌面]/ZCode's directory");
let executable = directory.join("ZCode.exe");
fs::create_dir_all(&directory).unwrap();
fs::write(&executable, []).unwrap();
let icon = directory.join("uninstallerIcon.ico");
let uninstaller = directory.join("Uninstall ZCode.exe");
for (kind, value) in [
("executable", path_to_string(&executable)),
("executable", format!("\"{}\"", executable.display())),
("directory", path_to_string(&directory)),
("icon", path_to_string(&icon)),
("icon", format!("{},0", executable.display())),
("icon", format!("\"{}\",-123", executable.display())),
(
"uninstaller",
format!("\"{}\" /currentuser", uninstaller.display()),
),
(
"uninstaller",
format!("{} /allusers", uninstaller.display()),
),
] {
let output = serde_json::json!([{ "kind": kind, "value": value }]).to_string();
assert_eq!(
parse_windows_zcode_discovery_output(&output),
Some(executable.clone()),
"{output}",
);
}
fs::remove_dir_all(home).unwrap();
}
#[cfg(target_os = "windows")]
#[test]
fn zcode_windows_skips_stale_and_unrelated_registration_entries() {
let home = agent_test_home("zcode-stale-registration");
let unrelated = home.join("Uninstall ZCode.exe");
fs::write(&unrelated, []).unwrap();
let executable = home.join("valid/ZCode.exe");
fs::create_dir_all(executable.parent().unwrap()).unwrap();
fs::write(&executable, []).unwrap();
let mut candidates = vec![
serde_json::json!({ "kind": "directory", "value": home }),
serde_json::json!({ "kind": "executable", "value": unrelated }),
serde_json::json!({ "kind": "executable", "value": home.join("missing/ZCode.exe") }),
serde_json::json!({ "kind": "executable", "value": "ZCode.exe" }),
serde_json::json!({ "kind": "executable", "value": "\"unterminated" }),
serde_json::json!({ "kind": "unknown", "value": executable }),
serde_json::json!({ "kind": "executable", "value": null }),
];
assert_eq!(
parse_windows_zcode_discovery_output(&serde_json::to_string(&candidates).unwrap()),
None,
);
candidates.push(serde_json::json!({ "kind": "executable", "value": executable }));
assert_eq!(
parse_windows_zcode_discovery_output(&serde_json::to_string(&candidates).unwrap()),
Some(executable),
);
assert_eq!(parse_windows_zcode_discovery_output("[]"), None);
assert_eq!(parse_windows_zcode_discovery_output("invalid JSON"), None);
fs::remove_dir_all(home).unwrap();
}
#[cfg(target_os = "windows")]
#[test]
fn zcode_windows_version_discovery_does_not_launch_the_application() {
let home = agent_test_home("zcode-version-no-launch");
let executable = home.join("zcode.cmd");
let marker = home.join("launched.txt");
fs::write(
&executable,
format!(
"@echo off\r\necho launched > \"{}\"\r\necho 1.0.0\r\n",
marker.display()
),
)
.unwrap();
assert_eq!(read_zcode_app_version(&executable), None);
assert!(
!marker.exists(),
"version discovery launched the application"
);
fs::remove_dir_all(home).unwrap();
}