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:
1 parent
6d7f3c8480
commit
145b585c3f
2 files changed
+243
-17
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user