From 5ea8dfc2dd8a7d925963d1f0c39dbb26cd8432b7 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Mon, 5 Oct 2026 22:58:05 -0700 Subject: [PATCH] Find the packaged zmx beside the Windows daemon The Windows package installs zmx.exe beside graphcoded.exe and never copies it into the support directory, so ZmxLocator (support bin only) reported zmx as missing. The packaged daemon then could not launch, message or kill any loop session, and Stop logged a kill that never happened (beta9 Dev Box). On Windows, prefer zmx.exe in the running executable's directory, then fall back to the support bin. macOS keeps resolving the support bin unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- .../Sources/Sessions/ZmxLocator.swift | 23 ++++++++++++++- graphcode/Tests/SupportDirectoryTests.swift | 20 +++++++++++++ windows-tests/WindowsDaemonTests.swift | 28 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/GraphcodeKit/Sources/Sessions/ZmxLocator.swift b/GraphcodeKit/Sources/Sessions/ZmxLocator.swift index 7df5906e..af8d6427 100644 --- a/GraphcodeKit/Sources/Sessions/ZmxLocator.swift +++ b/GraphcodeKit/Sources/Sessions/ZmxLocator.swift @@ -7,7 +7,12 @@ import Foundation public enum ZmxLocator { public static var binaryURL: URL { #if os(Windows) - SupportDirectory.binDirectory.appendingPathComponent("zmx.exe") + resolve( + candidates( + executableDirectory: Bundle.main.executableURL?.deletingLastPathComponent(), + supportBinDirectory: SupportDirectory.binDirectory, + executableName: "zmx.exe"), + isExecutable: FileManager.default.isExecutableFile(atPath:)) #else SupportDirectory.binDirectory.appendingPathComponent("zmx") #endif @@ -16,4 +21,20 @@ public enum ZmxLocator { public static var isInstalled: Bool { FileManager.default.isExecutableFile(atPath: binaryURL.path) } + + /// The places `zmx` may live, most preferred first; the last is the support directory's + /// `bin`, which is also what is reported when none of them exists. The Windows package + /// installs `zmx.exe` beside `graphcoded.exe` and never copies it into the support + /// directory, so the running executable's own directory comes first. + static func candidates( + executableDirectory: URL?, supportBinDirectory: URL, executableName: String + ) -> [URL] { + let supportBinary = supportBinDirectory.appendingPathComponent(executableName) + guard let executableDirectory else { return [supportBinary] } + return [executableDirectory.appendingPathComponent(executableName), supportBinary] + } + + static func resolve(_ candidates: [URL], isExecutable: (String) -> Bool) -> URL { + candidates.first { isExecutable($0.path) } ?? candidates[candidates.count - 1] + } } diff --git a/graphcode/Tests/SupportDirectoryTests.swift b/graphcode/Tests/SupportDirectoryTests.swift index e0134ec5..c504c49f 100644 --- a/graphcode/Tests/SupportDirectoryTests.swift +++ b/graphcode/Tests/SupportDirectoryTests.swift @@ -26,6 +26,26 @@ struct SupportDirectoryTests { #expect(SupportDirectory.binDirectory.deletingLastPathComponent() == root) } + @Test + func zmxResolutionPrefersAnExecutableSiblingThenTheSupportBin() { + // Windows packages zmx.exe beside graphcoded; macOS keeps using the support bin, and + // `binaryURL` there is unchanged (asserted above). + let install = URL(fileURLWithPath: "/opt/graphcode/bin", isDirectory: true) + let support = SupportDirectory.binDirectory + let candidates = ZmxLocator.candidates( + executableDirectory: install, supportBinDirectory: support, executableName: "zmx") + let sibling = install.appendingPathComponent("zmx") + let supportZmx = support.appendingPathComponent("zmx") + + #expect(candidates == [sibling, supportZmx]) + #expect(ZmxLocator.resolve(candidates) { $0 == sibling.path } == sibling) + #expect(ZmxLocator.resolve(candidates) { $0 == supportZmx.path } == supportZmx) + #expect(ZmxLocator.resolve(candidates) { _ in false } == supportZmx) + #if !os(Windows) + #expect(ZmxLocator.binaryURL == supportZmx) + #endif + } + @Test func aBackslashOverrideRemainsRelativeOnDarwin() { let home = URL(fileURLWithPath: "/Users/test-user", isDirectory: true) diff --git a/windows-tests/WindowsDaemonTests.swift b/windows-tests/WindowsDaemonTests.swift index 7f60ff1f..572a9ca9 100644 --- a/windows-tests/WindowsDaemonTests.swift +++ b/windows-tests/WindowsDaemonTests.swift @@ -41,6 +41,34 @@ final class WindowsDaemonTests: XCTestCase { XCTAssertEqual(ZmxLocator.binaryURL.lastPathComponent, "zmx.exe") } + // The packaged daemon ships zmx.exe beside itself under %LOCALAPPDATA%\GraphCode; + // nothing copies it into the support directory, so the support bin alone left the + // daemon unable to launch, message or kill any loop session (beta9 Dev Box). + func testZmxLocatorPrefersThePackagedSiblingOfTheRunningExecutable() { + let install = URL(fileURLWithPath: #"C:\Users\u\AppData\Local\GraphCode\current\bin"#) + let support = URL(fileURLWithPath: #"C:\Users\u\.graphcode\bin"#) + let candidates = ZmxLocator.candidates( + executableDirectory: install, supportBinDirectory: support, executableName: "zmx.exe") + let sibling = install.appendingPathComponent("zmx.exe") + let supportZmx = support.appendingPathComponent("zmx.exe") + + XCTAssertEqual(candidates, [sibling, supportZmx]) + XCTAssertEqual(ZmxLocator.resolve(candidates) { $0 == sibling.path }, sibling) + XCTAssertEqual( + ZmxLocator.resolve(candidates) { [sibling.path, supportZmx.path].contains($0) }, sibling) + XCTAssertEqual(ZmxLocator.resolve(candidates) { $0 == supportZmx.path }, supportZmx) + // Neither present: report the support bin, so isInstalled stays false. + XCTAssertEqual(ZmxLocator.resolve(candidates) { _ in false }, supportZmx) + } + + func testZmxLocatorWithoutAnExecutableDirectoryUsesTheSupportBin() { + let support = URL(fileURLWithPath: #"C:\Users\u\.graphcode\bin"#) + XCTAssertEqual( + ZmxLocator.candidates( + executableDirectory: nil, supportBinDirectory: support, executableName: "zmx.exe"), + [support.appendingPathComponent("zmx.exe")]) + } + func testWindowsProviderProbeUsesWhere() { let invocation = ProviderPath.probeInvocation(for: "copilot") XCTAssertEqual(URL(fileURLWithPath: invocation[0]).lastPathComponent, "where.exe")