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
11 changes: 11 additions & 0 deletions Tools/windows/Tests/WindowsShell.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,17 @@ Assert-Contract ($codespaceDialogSource -match 'WM_CTLCOLORLISTBOX' -and $codesp
Assert-Contract ($codespaceDialogSource -match 'IsDialogMessageW') `
"the codespace sheet must remain keyboard navigable"

$zmxSessionSource = Get-Content -LiteralPath (Join-Path $shellRoot "src\ZmxSession.zig") -Raw
$terminalSurfaceSource = Get-Content -LiteralPath (Join-Path $shellRoot "src\TerminalSurface.zig") -Raw
$workspaceTeardownSource = Get-Content -LiteralPath (Join-Path $shellRoot "src\WorkspaceTeardown.zig") -Raw
Assert-Contract ($zmxSessionSource -match '(?s)pub fn child\(.*?\.create_no_window = true;') `
"zmx children must be created without a console window"
Assert-Contract ($terminalSurfaceSource -notmatch 'std\.process\.Child\.init\(' -and
$workspaceTeardownSource -notmatch 'std\.process\.Child\.init\(' -and
[regex]::Matches($terminalSurfaceSource, 'ZmxSession\.child\(').Count -eq 2 -and
[regex]::Matches($workspaceTeardownSource, 'ZmxSession\.child\(').Count -eq 1) `
"zmx attach, resize, and kill must spawn through ZmxSession.child so the GUI shell never opens a console window"

$zig = Resolve-TestZig
Invoke-Native "Accessibility contract executable tests" {
Push-Location $shellRoot
Expand Down
13 changes: 2 additions & 11 deletions graphcode-windows/src/TerminalSurface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1564,11 +1564,7 @@ pub const Workspace = struct {
&session_buffer,
)).len;
}
var child = std.process.Child.init(attach_args[0..attach_len], self.allocator);
child.cwd = self.cwd;
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Ignore;
var child = ZmxSession.child(self.allocator, attach_args[0..attach_len], self.cwd, .attach);
try child.spawn();
if (child.stdin) |stdin| {
var mode: c.DWORD = c.PIPE_NOWAIT;
Expand Down Expand Up @@ -1704,12 +1700,7 @@ pub const Workspace = struct {
self.setInputError("terminal PTY resize tracking allocation failed");
return;
};
var child = std.process.Child.init(command, self.allocator);
child.cwd = self.cwd;
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
child.create_no_window = true;
var child = ZmxSession.child(self.allocator, command, self.cwd, .control);
child.spawn() catch {
self.allocator.free(session);
slot.attempted_resize_size = size;
Expand Down
5 changes: 1 addition & 4 deletions graphcode-windows/src/WorkspaceTeardown.zig
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,7 @@ pub const Live = struct {
defer allocator.free(zmx);
const argv = killArgv(allocator, zmx, ids) catch return 0;
defer freeKillArgv(allocator, argv);
var child = std.process.Child.init(argv, allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
var child = ZmxSession.child(allocator, argv, null, .control);
_ = child.spawnAndWait() catch return 0;
return ids.len;
}
Expand Down
44 changes: 44 additions & 0 deletions graphcode-windows/src/ZmxSession.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,50 @@ pub fn allocName(allocator: std.mem.Allocator, session: []const u8) ![]u8 {
return std.fmt.allocPrint(allocator, prefix ++ "{s}", .{session});
}

pub const ChildStdio = enum {
/// `zmx attach`: the shell writes terminal input and reads terminal output.
attach,
/// One-shot control commands such as `resize` and `kill`.
control,
};

/// zmx.exe is a console-subsystem program. Spawned from the GUI shell without
/// CREATE_NO_WINDOW, Windows allocates it a new console, which the default
/// terminal host shows as a visible window that steals foreground.
pub fn child(
allocator: std.mem.Allocator,
argv: []const []const u8,
cwd: ?[]const u8,
stdio: ChildStdio,
) std.process.Child {
var result = std.process.Child.init(argv, allocator);
result.cwd = cwd;
result.stdin_behavior = if (stdio == .attach) .Pipe else .Ignore;
result.stdout_behavior = if (stdio == .attach) .Pipe else .Ignore;
result.stderr_behavior = .Ignore;
result.create_no_window = true;
return result;
}

test "zmx children keep their stdio contract and never open a console window" {
const attach_argv = [_][]const u8{ "zmx.exe", "attach", "graphcode-alpha", "--size", "80x24" };
const attach = child(std.testing.allocator, &attach_argv, "C:\\GraphCode\\bin", .attach);
try std.testing.expectEqual(std.process.Child.StdIo.Pipe, attach.stdin_behavior);
try std.testing.expectEqual(std.process.Child.StdIo.Pipe, attach.stdout_behavior);
try std.testing.expectEqual(std.process.Child.StdIo.Ignore, attach.stderr_behavior);
try std.testing.expectEqualStrings("C:\\GraphCode\\bin", attach.cwd.?);
try std.testing.expectEqual(attach_argv.len, attach.argv.len);
try std.testing.expectEqual(true, attach.create_no_window);

const kill_argv = [_][]const u8{ "zmx.exe", "kill", "graphcode-alpha" };
const control = child(std.testing.allocator, &kill_argv, null, .control);
try std.testing.expectEqual(std.process.Child.StdIo.Ignore, control.stdin_behavior);
try std.testing.expectEqual(std.process.Child.StdIo.Ignore, control.stdout_behavior);
try std.testing.expectEqual(std.process.Child.StdIo.Ignore, control.stderr_behavior);
try std.testing.expect(control.cwd == null);
try std.testing.expectEqual(true, control.create_no_window);
}

test "canonical zmx session names add the ownership prefix exactly once" {
var storage: [64]u8 = undefined;
try std.testing.expectEqualStrings("graphcode-alpha", try nameBuffer("alpha", &storage));
Expand Down
Loading