Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Tools/windows/Tests/ValidationRunner.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ try {
if ($runnerSource -notmatch '(?s)Pinned GraphCode Windows shell build and smoke.*?Native UI Automation live gate.*?uia-live-gate\.ps1') {
throw "RED: Windows shell validation does not execute the UI Automation live gate"
}
$shellTests = Get-Content (Join-Path $PSScriptRoot "WindowsShell.Tests.ps1") -Raw
if ($shellTests -notmatch '(?s)Windows update feed executable tests.*?zig test src\\WindowsUpdates\.zig.*?-lwinhttp') {
throw "RED: Windows shell validation does not run the native updater tests"
}
if ($runnerSource -notmatch '\$SkipWslRemoteE2E' -or
$runnerSource -notmatch '"--skip-local-wsl"') {
throw "RED: hosted validation cannot explicitly isolate unavailable local WSL fixtures"
Expand Down
17 changes: 17 additions & 0 deletions Tools/windows/Tests/WindowsShell.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ $mainWindowSource = Get-Content (Join-Path $shellRoot "src\MainWindow.zig") -Raw
$nativeFormsSource = Get-Content (Join-Path $shellRoot "src\NativeForms.zig") -Raw
$inputSource = Get-Content (Join-Path $shellRoot "src\InputRouter.zig") -Raw
$stubSource = Get-Content (Join-Path $repoRoot "Tools\windows\Stub-Daemon.ps1") -Raw
Assert-Contract ($appSource -match
'(?s)pub fn checkForUpdates.*?requestUpdateCheck\(true\)' -and
$appSource -match 'if \(!envFlag\("GRAPHCODE_UIA_UPDATE_AVAILABLE"\)\) self\.requestUpdateCheck\(false\)' -and
$appSource -match 'shouldPresentOffer\(self\.update_user_initiated\)') `
"explicit and background update checks must preserve their presentation intent"
Assert-Contract ($appSource -match
'(?s)app\.smoke_tick >= 16 and\s*app\.client\.connectionState\(\) == \.connected and\s*app\.currentProject\(\) != null and app\.model\.selected\(\) != null and\s*!app\.smoke_action_requested') `
"smoke graph command must wait for connection and selection instead of a single tick"
Expand Down Expand Up @@ -317,6 +322,18 @@ Invoke-Native "Product Settings executable tests" {
-lc -luser32 -lgdi32 "-I$include"
} finally { Pop-Location }
}
Invoke-Native "Windows update feed executable tests" {
$winghosttyRoot = $env:GRAPHCODE_WINGHOSTTY_ROOT
if (-not $winghosttyRoot) {
$depotRoot = Split-Path (Split-Path $repoRoot -Parent) -Parent
$winghosttyRoot = Join-Path $depotRoot "Winghostty-worktrees\host-integration"
}
$include = Join-Path $winghosttyRoot "include"
Push-Location $shellRoot
try {
& $zig test src\WindowsUpdates.zig -target x86_64-windows-msvc -lc -lwinhttp "-I$include"
} finally { Pop-Location }
}
Invoke-Native "Frame buffer executable tests" {
Push-Location $shellRoot
try { & $zig test src\FrameBuffer.zig } finally { Pop-Location }
Expand Down
1 change: 1 addition & 0 deletions Tools/windows/validation-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The Windows port must have runnable commands before implementation fleets begin.
| Deterministic release hardening fixtures | `pwsh Tools/windows/validate.ps1 -Task hardening` |
| Signed catalog integrity and publisher policy | `pwsh Tools\windows\Tests\Packaging.Signing.Tests.ps1` |
| Product/investigation provider pin consistency | `pwsh Tools\windows\Tests\ProviderPins.Tests.ps1` |
| Native release feed, URL handoff, and allocation safety | `pwsh Tools\windows\Tests\WindowsShell.Tests.ps1 -ZigExecutable $env:GRAPHCODE_ZIG0152` (includes updater tests) |
| Real-product packaging/install/upgrade/rollback | `pwsh Tools\windows\validate.ps1 -Task packaging` |
| Shared Swift package | `swift test --package-path <shared-package>` once extracted |
| macOS app/daemon/CLI | `make test` |
Expand Down
21 changes: 21 additions & 0 deletions graphcode-windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ bind the complete payload with a publisher-pinned Authenticode catalog; see
`Tools\windows\PACKAGING.md`. There is not yet a published signed installer or
an automatic install/relaunch path in the native updater.

## Update checks

The native client checks `scgopi/GraphCode`'s GitHub releases API, accepting
additive release/asset metadata while retaining type checks on consumed fields.
The greatest eligible version from the fetched 30-release page is selected;
stable checks exclude prereleases and both channels exclude drafts. Existing
version ordering and cancellation/generation behavior are unchanged. Startup
and settings-refresh checks update status/sidebar offers without opening a modal;
only an explicit Check for Updates action or banner click opens the offer.

An offer opens only the HTTPS release overview or a single-segment version-tag
page in that repository; encoded paths, dot segments, query/fragment additions,
and backslash separators are rejected. It is a project-release notification,
not proof of an installable Windows artifact: the dialog notes that assets may target other
platforms and that Windows installation/relaunch is not implemented.
No installer is downloaded or executed.

`Tools\windows\Tests\WindowsShell.Tests.ps1` runs the native updater contracts,
including realistic GitHub response fields, URL handoff validation, stable/beta
selection, malformed consumed fields, and allocation-failure cleanup.

## Tray lifecycle coverage

The shell registers a version-4 notification icon with a stable `HWND`/icon ID
Expand Down
57 changes: 33 additions & 24 deletions graphcode-windows/src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ pub const App = struct {
update_cancel: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
update_generation: u64 = 0,
update_pending: bool = false,
update_user_initiated: bool = false,
update_version: []u8 = &.{},
update_release_url: []u8 = &.{},
smoke_restart_index: ?usize = null,
Expand Down Expand Up @@ -411,7 +412,7 @@ pub const App = struct {
if (self.workspace) |workspace| try workspace.startInputWorker();
}
self.layoutWorkspace();
if (!envFlag("GRAPHCODE_UIA_UPDATE_AVAILABLE")) self.requestUpdateCheck();
if (!envFlag("GRAPHCODE_UIA_UPDATE_AVAILABLE")) self.requestUpdateCheck(false);
if (std.process.getEnvVarOwned(self.allocator, "GRAPHCODE_SHELL_REQUIRE_DAEMON")) |value| {
defer self.allocator.free(value);
self.require_smoke_contract = std.mem.eql(u8, value, "1");
Expand Down Expand Up @@ -1138,7 +1139,7 @@ pub const App = struct {
self.update_state = WindowsUpdates.CheckState.configure(draft.beta);
self.update_lock.unlock();
self.setStatus("Checking for updates…");
self.requestUpdateCheck();
self.requestUpdateCheck(false);
_ = c.InvalidateRect(self.window.hwnd, null, 0);
}

Expand Down Expand Up @@ -1216,13 +1217,14 @@ pub const App = struct {

pub fn checkForUpdates(self: *App) void {
self.setStatus("Checking for updates...");
self.requestUpdateCheck();
self.requestUpdateCheck(true);
self.updateNativeChrome();
}

fn requestUpdateCheck(self: *App) void {
fn requestUpdateCheck(self: *App, user_initiated: bool) void {
self.update_lock.lock();
self.update_generation += 1;
self.update_user_initiated = user_initiated;
self.update_pending = true;
if (self.update_thread != null) {
self.update_cancel.store(true, .release);
Expand Down Expand Up @@ -1263,30 +1265,24 @@ pub const App = struct {
};
defer self.allocator.free(version);
var client = WindowsUpdates.CheckClient{ .allocator = self.allocator };
const result = client.checkWithCancel(beta, version, &self.update_cancel) catch {
var result = client.checkWithCancel(beta, version, &self.update_cancel) catch {
self.update_lock.lock();
if (generation == self.update_generation and !self.update_cancel.load(.acquire))
self.update_state = .{ .channel = if (beta) .beta else .stable, .state = .failed };
self.update_done = true;
self.update_lock.unlock();
return;
};
defer {
var owned = result;
owned.deinit(self.allocator);
}
const version_copy = if (result.version) |value| self.allocator.dupe(u8, value) catch null else null;
const url_copy = if (result.release_url) |value| self.allocator.dupe(u8, value) catch null else null;
defer result.deinit(self.allocator);
self.update_lock.lock();
if (generation == self.update_generation and !self.update_cancel.load(.acquire)) {
self.update_state = .{ .channel = result.channel, .state = result.state };
if (self.update_version.len != 0) self.allocator.free(self.update_version);
if (self.update_release_url.len != 0) self.allocator.free(self.update_release_url);
self.update_version = version_copy orelse &.{};
self.update_release_url = url_copy orelse &.{};
} else {
if (version_copy) |value| self.allocator.free(value);
if (url_copy) |value| self.allocator.free(value);
self.update_version = result.version orelse &.{};
self.update_release_url = result.release_url orelse &.{};
result.version = null;
result.release_url = null;
}
self.update_done = true;
self.update_lock.unlock();
Expand All @@ -1304,35 +1300,44 @@ pub const App = struct {
const pending = self.update_pending;
self.update_pending = false;
const label = self.update_state.label();
const available = self.update_state.state == .available;
const present_offer = self.update_state.shouldPresentOffer(self.update_user_initiated);
const version = self.update_version;
const release_url = self.update_release_url;
self.update_lock.unlock();
if (pending) {
self.launchUpdateCheck();
} else {
self.setStatus(label);
if (available) self.showAvailableUpdate(version, release_url);
if (present_offer) self.showAvailableUpdate(version, release_url);
}
}
}

fn showAvailableUpdate(self: *App, version: []const u8, release_url: []const u8) void {
const url = WindowsUpdates.releasePageUrl(release_url) catch {
self.setStatus("Update release URL is not a trusted GraphCode release page");
return;
};
const message = std.fmt.allocPrint(
self.allocator,
"GraphCode {s} is available.\n\nOpen the verified GitHub release page to review release notes and download the Windows package?",
"GraphCode {s} is available.\n\nOpen the verified GitHub release page to review release notes?\n\nIn-app Windows installation and relaunch are not available. Release assets may target other platforms.",
.{if (version.len == 0) "update" else version},
) catch return;
) catch {
self.setStatus("Unable to prepare the update offer");
return;
};
defer self.allocator.free(message);
const message_wide = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, message) catch return;
const message_wide = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, message) catch {
self.setStatus("Unable to encode the update offer");
return;
};
defer self.allocator.free(message_wide);
if (c.MessageBoxW(
self.window.hwnd,
message_wide.ptr,
std.unicode.utf8ToUtf16LeStringLiteral("GraphCode Update Available").ptr,
c.MB_ICONINFORMATION | c.MB_YESNO | c.MB_DEFBUTTON1,
) != c.IDYES) return;
const url = if (release_url.len != 0) release_url else "https://github.com/GraphCode/GraphCode/releases";
const url_wide = std.unicode.utf8ToUtf16LeAllocZ(self.allocator, url) catch {
self.setStatus("Unable to encode the release URL");
return;
Expand All @@ -1358,7 +1363,11 @@ pub const App = struct {
defer if (version) |value| self.allocator.free(value);
defer if (release_url) |value| self.allocator.free(value);
if (!available) return;
self.showAvailableUpdate(version orelse "", release_url orelse "");
if (version == null or release_url == null) {
self.setStatus("Unable to prepare the update offer");
return;
}
self.showAvailableUpdate(version.?, release_url.?);
}

fn addRemoteRepository(self: *App) void {
Expand Down Expand Up @@ -2116,7 +2125,7 @@ pub const App = struct {
if (self.update_version.len != 0) self.allocator.free(self.update_version);
if (self.update_release_url.len != 0) self.allocator.free(self.update_release_url);
self.update_version = self.allocator.dupe(u8, "9.9.9-test") catch &.{};
self.update_release_url = self.allocator.dupe(u8, "https://github.com/GraphCode/GraphCode/releases/tag/v9.9.9-test") catch &.{};
self.update_release_url = self.allocator.dupe(u8, WindowsUpdates.releases_page_url ++ "/tag/v9.9.9-test") catch &.{};
self.update_lock.unlock();
}
if (std.process.getEnvVarOwned(self.allocator, "GRAPHCODE_UIA_INGRESS_ERROR")) |message| {
Expand Down
Loading
Loading