@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +3 -0
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/ts-core/SKILL.md +2 -0
  9. package/src/app_runner/app_assets.zig +85 -1
  10. package/src/app_runner/root.zig +63 -3
  11. package/src/app_runner/ts_core_main.zig +8 -0
  12. package/src/platform/macos/appkit_host.h +9 -0
  13. package/src/platform/macos/appkit_host.m +366 -0
  14. package/src/platform/macos/root.zig +29 -6
  15. package/src/platform/root.zig +1 -0
  16. package/src/platform/types.zig +9 -0
  17. package/src/platform/windows/root.zig +37 -0
  18. package/src/platform/windows/webview2_host.cpp +371 -26
  19. package/src/primitives/app_manifest/root.zig +1 -0
  20. package/src/primitives/app_manifest/tests.zig +35 -0
  21. package/src/primitives/app_manifest/types.zig +12 -0
  22. package/src/primitives/app_manifest/validation.zig +44 -6
  23. package/src/root.zig +1 -0
  24. package/src/runtime/channel_sources.zig +49 -1
  25. package/src/runtime/effects.zig +12 -1
  26. package/src/runtime/flow.zig +6 -0
  27. package/src/runtime/flow_reference.zig +6 -0
  28. package/src/runtime/root.zig +4 -0
  29. package/src/runtime/single_instance.zig +893 -0
  30. package/src/tooling/manifest.zig +111 -3
  31. package/src/tooling/package.zig +98 -8
  32. package/src/tooling/raw_manifest.zig +7 -0
  33. package/src/tooling/root.zig +2 -0
  34. package/src/tooling/templates.zig +44 -2
  35. package/src/tooling/update.zig +228 -10
  36. package/src/tooling/windows_installer.zig +245 -0
  37. package/src/tooling/zip_writer.zig +211 -0
  38. package/src/updater/engine.zig +936 -0
  39. package/src/updater/feed.zig +261 -12
  40. package/src/updater/macos.zig +187 -0
  41. package/src/updater/root.zig +41 -0
  42. package/src/updater/windows.zig +470 -0
  43. package/src/windows_install/config.zig +247 -0
  44. package/src/windows_install/main.zig +690 -0
  45. package/src/windows_install/payload.zig +146 -0
  46. package/src/windows_install/switch.zig +184 -0
  47. package/src/windows_install/tests.zig +11 -0
  48. package/src/windows_install/win32.zig +248 -0
  49. package/tools/native-sdk/main.zig +10 -9
@@ -0,0 +1,893 @@
1
+ //! Single-instance ownership (manifest `single_instance`): one process per
2
+ //! bundle id, user, and login session owns the app. The first launch to
3
+ //! claim ownership becomes the primary. A later launch hands its working
4
+ //! directory and arguments to the primary, reports the delivery through
5
+ //! its exit code, and exits before any window exists. The primary's core
6
+ //! receives each launch on the `native-sdk.app.launches` channel source
7
+ //! (`attach`) as one `"data"` event and decides what to do with it.
8
+ //!
9
+ //! Windows is the implemented host: a message-mode named pipe created with
10
+ //! FILE_FLAG_FIRST_PIPE_INSTANCE is both the ownership lock and the
11
+ //! notification endpoint. The kernel retires it with the process, so a
12
+ //! normal exit or a crash leaves no stale ownership. Other hosts answer
13
+ //! `.standalone` and run without ownership.
14
+ const std = @import("std");
15
+ const builtin = @import("builtin");
16
+ const effects = @import("effects.zig");
17
+ const clock = @import("clock.zig");
18
+
19
+ const is_windows = builtin.os.tag == .windows;
20
+
21
+ pub const source_name = "native-sdk.app.launches";
22
+
23
+ /// One launch notification is the later process's working directory,
24
+ /// then each of its arguments (argv[0] excluded), every field preceded by
25
+ /// a NUL except the first. Paths and arguments never contain NUL, so the
26
+ /// split is unambiguous; a launch without arguments is the bare working
27
+ /// directory. The bound is the channel's post bound.
28
+ pub const max_launch_bytes = effects.max_effect_channel_bytes;
29
+
30
+ /// How long the primary holds a launch while its core has not opened the
31
+ /// source yet (during startup, or in a core that never opens it).
32
+ pub const hold_ms: u64 = 5_000;
33
+
34
+ /// A later launch's whole budget: waiting for a free endpoint, the
35
+ /// primary's hold, and the reply.
36
+ pub const notify_timeout_ms: u64 = 10_000;
37
+
38
+ /// What a later launch learned. Each member has its own exit code so a
39
+ /// launching shell or script can tell the outcomes apart.
40
+ pub const NotifyResult = enum {
41
+ /// The primary accepted the launch into its core's event queue.
42
+ delivered,
43
+ /// No reply arrived within `notify_timeout_ms`.
44
+ not_responding,
45
+ /// The primary is running, but its core did not open the
46
+ /// `native-sdk.app.launches` source within `hold_ms`.
47
+ not_listening,
48
+ /// The source's event queue was full.
49
+ queue_full,
50
+ /// The launch exceeds `max_launch_bytes`; nothing was sent.
51
+ too_large,
52
+ /// The primary's endpoint could not be reached, or it is not owned
53
+ /// by this user in this login session.
54
+ ipc_failed,
55
+
56
+ pub fn exitCode(self: NotifyResult) u8 {
57
+ return switch (self) {
58
+ .delivered => 0,
59
+ .not_responding => 3,
60
+ .not_listening => 4,
61
+ .queue_full => 5,
62
+ .too_large => 6,
63
+ .ipc_failed => 7,
64
+ };
65
+ }
66
+
67
+ pub fn describe(self: NotifyResult) []const u8 {
68
+ return switch (self) {
69
+ .delivered => "launch delivered to the running instance",
70
+ .not_responding => "the running instance did not respond",
71
+ .not_listening => "the running instance is not listening for launches (its core never opened " ++ source_name ++ ")",
72
+ .queue_full => "the running instance's launch queue is full",
73
+ .too_large => std.fmt.comptimePrint("the launch arguments exceed the {d}-byte notification limit", .{max_launch_bytes}),
74
+ .ipc_failed => "the running instance's endpoint could not be reached or verified",
75
+ };
76
+ }
77
+ };
78
+
79
+ pub const Claim = union(enum) {
80
+ /// This process owns the app and serves later launches.
81
+ primary,
82
+ /// Another process owns the app; this launch was handed over (or
83
+ /// not), and the process should exit with `result.exitCode()`.
84
+ secondary: NotifyResult,
85
+ /// Ownership is unavailable; the process runs as an ordinary
86
+ /// instance. The reason is static text.
87
+ standalone: []const u8,
88
+ };
89
+
90
+ /// Claims ownership for `bundle_id`. Only the first call in a process
91
+ /// claims; every later call answers null, so a process that fell back to
92
+ /// `.standalone` never re-attempts after its startup work has begun.
93
+ pub fn claim(io: std.Io, gpa: std.mem.Allocator, args: std.process.Args, bundle_id: []const u8) ?Claim {
94
+ if (!beginClaim()) return null;
95
+ if (comptime !is_windows) return .{ .standalone = "single_instance is not implemented on this platform yet" };
96
+ return claimWindows(io, gpa, args, bundle_id);
97
+ }
98
+
99
+ /// True exactly once per process: for the first claim attempt.
100
+ fn beginClaim() bool {
101
+ owner.lock();
102
+ defer owner.unlock();
103
+ if (owner.claim_started) return false;
104
+ owner.claim_started = true;
105
+ return true;
106
+ }
107
+
108
+ /// Whether this process owns the app.
109
+ pub fn owned() bool {
110
+ owner.lock();
111
+ defer owner.unlock();
112
+ return owner.owned;
113
+ }
114
+
115
+ /// The `native-sdk.app.launches` channel source: the core's open hands
116
+ /// the posting handle to the listener. A reopen replaces the previous
117
+ /// handle; launches held while no handle was live deliver to the new one.
118
+ pub fn attach(payload: []const u8, handle: effects.ChannelHandle) !void {
119
+ if (payload.len != 0) return error.InvalidChannelSourceArguments;
120
+ if (comptime !is_windows) return error.UnsupportedChannelSource;
121
+ owner.lock();
122
+ defer owner.unlock();
123
+ if (!owner.owned) return error.SingleInstanceNotOwned;
124
+ owner.handle = handle;
125
+ }
126
+
127
+ /// Encodes one launch into a caller-owned buffer (see `max_launch_bytes`).
128
+ pub const LaunchWriter = struct {
129
+ buffer: []u8,
130
+ len: usize = 0,
131
+ overflow: bool = false,
132
+
133
+ pub fn init(buffer: []u8, cwd: []const u8) LaunchWriter {
134
+ var writer: LaunchWriter = .{ .buffer = buffer };
135
+ writer.append(cwd);
136
+ return writer;
137
+ }
138
+
139
+ pub fn addArgument(self: *LaunchWriter, argument: []const u8) void {
140
+ self.append("\x00");
141
+ self.append(argument);
142
+ }
143
+
144
+ /// The encoded launch, or null when it did not fit.
145
+ pub fn bytes(self: *const LaunchWriter) ?[]const u8 {
146
+ return if (self.overflow) null else self.buffer[0..self.len];
147
+ }
148
+
149
+ fn append(self: *LaunchWriter, chunk: []const u8) void {
150
+ if (self.overflow) return;
151
+ if (chunk.len > self.buffer.len - self.len) {
152
+ self.overflow = true;
153
+ return;
154
+ }
155
+ @memcpy(self.buffer[self.len..][0..chunk.len], chunk);
156
+ self.len += chunk.len;
157
+ }
158
+ };
159
+
160
+ /// The primary's process-wide state. Guarded sections are a flag or one
161
+ /// handle copy, so a spin lock suffices (0.16 has no blocking thread
162
+ /// mutex outside `Io`).
163
+ const Owner = struct {
164
+ mutex: std.atomic.Mutex = .unlocked,
165
+ claim_started: bool = false,
166
+ owned: bool = false,
167
+ handle: ?effects.ChannelHandle = null,
168
+
169
+ fn lock(self: *Owner) void {
170
+ while (!self.mutex.tryLock()) std.atomic.spinLoopHint();
171
+ }
172
+
173
+ fn unlock(self: *Owner) void {
174
+ self.mutex.unlock();
175
+ }
176
+ };
177
+
178
+ var owner: Owner = .{};
179
+
180
+ /// The primary's answer to one launch, as it crosses the endpoint.
181
+ const Reply = enum(u8) {
182
+ accepted = 0,
183
+ not_listening = 1,
184
+ queue_full = 2,
185
+ too_large = 3,
186
+ bad_request = 4,
187
+ _,
188
+
189
+ fn result(self: Reply) NotifyResult {
190
+ return switch (self) {
191
+ .accepted => .delivered,
192
+ .not_listening => .not_listening,
193
+ .queue_full => .queue_full,
194
+ .too_large => .too_large,
195
+ .bad_request, _ => .ipc_failed,
196
+ };
197
+ }
198
+ };
199
+
200
+ /// Where the endpoint hands a received launch: the core's channel in the
201
+ /// primary, a recorder in tests.
202
+ const Sink = struct {
203
+ context: *anyopaque,
204
+ deliver_fn: *const fn (context: *anyopaque, launch: []const u8) Reply,
205
+
206
+ fn deliver(self: Sink, launch: []const u8) Reply {
207
+ return self.deliver_fn(self.context, launch);
208
+ }
209
+ };
210
+
211
+ /// Posts a launch to the core's channel, holding it up to `hold` while no
212
+ /// live handle is attached.
213
+ fn deliverToCore(io: std.Io, launch: []const u8, hold: u64) Reply {
214
+ if (launch.len > max_launch_bytes) return .too_large;
215
+ const deadline = clock.monotonicMs() + hold;
216
+ while (true) {
217
+ if (currentHandle()) |handle| switch (handle.post(launch)) {
218
+ .accepted => return .accepted,
219
+ .dropped_full => return .queue_full,
220
+ .dropped_oversized => return .too_large,
221
+ // The core closed the channel (or tore down): wait for a
222
+ // reopen within the same hold.
223
+ .closed => forgetHandle(handle),
224
+ };
225
+ if (clock.monotonicMs() >= deadline) return .not_listening;
226
+ std.Io.sleep(io, std.Io.Duration.fromMilliseconds(25), .awake) catch return .not_listening;
227
+ }
228
+ }
229
+
230
+ fn currentHandle() ?effects.ChannelHandle {
231
+ owner.lock();
232
+ defer owner.unlock();
233
+ return owner.handle;
234
+ }
235
+
236
+ fn forgetHandle(handle: effects.ChannelHandle) void {
237
+ owner.lock();
238
+ defer owner.unlock();
239
+ const current = owner.handle orelse return;
240
+ if (current.shared == handle.shared and current.generation == handle.generation) owner.handle = null;
241
+ }
242
+
243
+ const CoreSink = struct {
244
+ io: std.Io,
245
+
246
+ fn sink(self: *CoreSink) Sink {
247
+ return .{ .context = self, .deliver_fn = deliver };
248
+ }
249
+
250
+ fn deliver(context: *anyopaque, launch: []const u8) Reply {
251
+ const self: *CoreSink = @ptrCast(@alignCast(context));
252
+ return deliverToCore(self.io, launch, hold_ms);
253
+ }
254
+ };
255
+
256
+ /// Leads every request so a stray client cannot pass for a launch.
257
+ const request_magic = "NSL1";
258
+ const request_capacity = request_magic.len + max_launch_bytes;
259
+ const reply_capacity = 64;
260
+
261
+ fn remainingMs(deadline: u64) ?u64 {
262
+ const now = clock.monotonicMs();
263
+ return if (now >= deadline) null else deadline - now;
264
+ }
265
+
266
+ fn claimWindows(io: std.Io, gpa: std.mem.Allocator, args: std.process.Args, bundle_id: []const u8) Claim {
267
+ if (comptime !is_windows) unreachable;
268
+ var identity: Identity = undefined;
269
+ if (!identity.loadCurrent()) return .{ .standalone = "the current user's identity is unavailable" };
270
+ var name_buffer: [pipe_name_capacity:0]u16 = undefined;
271
+ const name = pipeName(&name_buffer, identity.session, identity.sidText(), bundle_id);
272
+ var security: Security = undefined;
273
+ if (!security.init(identity.sidText())) return .{ .standalone = "the instance endpoint's security descriptor could not be built" };
274
+ defer security.deinit();
275
+
276
+ var launch_buffer: [max_launch_bytes]u8 = undefined;
277
+ var launch: ?[]const u8 = null;
278
+ const deadline = clock.monotonicMs() + notify_timeout_ms;
279
+ while (true) {
280
+ switch (createEndpoint(name, &security.attributes)) {
281
+ .owned => |pipe| {
282
+ owner.lock();
283
+ owner.owned = true;
284
+ owner.unlock();
285
+ const listener = std.Thread.spawn(.{}, listenerMain, .{pipe}) catch {
286
+ owner.lock();
287
+ owner.owned = false;
288
+ owner.unlock();
289
+ _ = win.CloseHandle(pipe);
290
+ return .{ .standalone = "the launch listener thread could not start" };
291
+ };
292
+ listener.detach();
293
+ return .primary;
294
+ },
295
+ .taken => {},
296
+ .failed => return .{ .standalone = "the instance endpoint could not be created" },
297
+ }
298
+ const bytes = launch orelse encoded: {
299
+ const encoded = encodeCurrentLaunch(io, gpa, args, &launch_buffer) orelse return .{ .secondary = .too_large };
300
+ launch = encoded;
301
+ break :encoded encoded;
302
+ };
303
+ switch (notify(name, bytes, deadline)) {
304
+ .result => |result| return .{ .secondary = result },
305
+ // The owner exited between our create and connect: try to
306
+ // own the app ourselves.
307
+ .vanished => if (remainingMs(deadline) == null) return .{ .secondary = .not_responding },
308
+ }
309
+ }
310
+ }
311
+
312
+ fn encodeCurrentLaunch(io: std.Io, gpa: std.mem.Allocator, args: std.process.Args, buffer: []u8) ?[]const u8 {
313
+ var cwd_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
314
+ const cwd_len = std.Io.Dir.cwd().realPathFile(io, ".", &cwd_buffer) catch 0;
315
+ var writer: LaunchWriter = .init(buffer, cwd_buffer[0..cwd_len]);
316
+ var iterator = std.process.Args.Iterator.initAllocator(args, gpa) catch return null;
317
+ defer iterator.deinit();
318
+ _ = iterator.next();
319
+ while (iterator.next()) |argument| writer.addArgument(argument);
320
+ return writer.bytes();
321
+ }
322
+
323
+ fn listenerMain(pipe: win.HANDLE) void {
324
+ var threaded: std.Io.Threaded = .init(std.heap.page_allocator, .{});
325
+ defer threaded.deinit();
326
+ var core_sink: CoreSink = .{ .io = threaded.io() };
327
+ const sink = core_sink.sink();
328
+ while (true) serveNext(pipe, sink);
329
+ }
330
+
331
+ const CreateResult = union(enum) {
332
+ owned: win.HANDLE,
333
+ /// Another process holds the name.
334
+ taken,
335
+ failed: u32,
336
+ };
337
+
338
+ /// One pipe instance, so the name exists exactly while the owner lives.
339
+ fn createEndpoint(name: [*:0]const u16, attributes: ?*win.SECURITY_ATTRIBUTES) CreateResult {
340
+ if (comptime !is_windows) unreachable;
341
+ const pipe = win.CreateNamedPipeW(
342
+ name,
343
+ win.PIPE_ACCESS_DUPLEX | win.FILE_FLAG_FIRST_PIPE_INSTANCE,
344
+ win.PIPE_TYPE_MESSAGE | win.PIPE_READMODE_MESSAGE | win.PIPE_WAIT | win.PIPE_REJECT_REMOTE_CLIENTS,
345
+ 1,
346
+ reply_capacity,
347
+ request_capacity,
348
+ 0,
349
+ attributes,
350
+ );
351
+ if (pipe != win.INVALID_HANDLE_VALUE) return .{ .owned = pipe };
352
+ const err = lastError();
353
+ if (err == win.ERROR_ACCESS_DENIED or err == win.ERROR_PIPE_BUSY) return .taken;
354
+ return .{ .failed = err };
355
+ }
356
+
357
+ /// Serves one client on the owner's pipe: read one launch, hand it to the
358
+ /// sink, reply with the sink's answer.
359
+ fn serveNext(pipe: win.HANDLE, sink: Sink) void {
360
+ if (comptime !is_windows) unreachable;
361
+ if (win.ConnectNamedPipe(pipe, null) == 0) {
362
+ const err = lastError();
363
+ if (err != win.ERROR_PIPE_CONNECTED) {
364
+ _ = win.DisconnectNamedPipe(pipe);
365
+ // A client that left before we connected is routine; any
366
+ // other failure backs off so the loop cannot spin.
367
+ if (err != win.ERROR_NO_DATA) win.Sleep(50);
368
+ return;
369
+ }
370
+ }
371
+ defer _ = win.DisconnectNamedPipe(pipe);
372
+ var request: [request_capacity]u8 = undefined;
373
+ var read: win.DWORD = 0;
374
+ const reply: Reply = if (win.ReadFile(pipe, &request, request.len, &read, null) != 0)
375
+ answer(request[0..read], sink)
376
+ else if (lastError() == win.ERROR_MORE_DATA) oversized: {
377
+ drainMessage(pipe);
378
+ break :oversized .too_large;
379
+ } else return;
380
+ const out = [1]u8{@intFromEnum(reply)};
381
+ var written: win.DWORD = 0;
382
+ if (win.WriteFile(pipe, &out, out.len, &written, null) == 0) return;
383
+ // Disconnecting discards unread data: wait until the client has the
384
+ // reply.
385
+ _ = win.FlushFileBuffers(pipe);
386
+ }
387
+
388
+ fn answer(request: []const u8, sink: Sink) Reply {
389
+ if (!std.mem.startsWith(u8, request, request_magic)) return .bad_request;
390
+ const launch = request[request_magic.len..];
391
+ if (launch.len > max_launch_bytes) return .too_large;
392
+ return sink.deliver(launch);
393
+ }
394
+
395
+ fn drainMessage(pipe: win.HANDLE) void {
396
+ var scratch: [512]u8 = undefined;
397
+ while (true) {
398
+ var read: win.DWORD = 0;
399
+ if (win.ReadFile(pipe, &scratch, scratch.len, &read, null) != 0) return;
400
+ if (lastError() != win.ERROR_MORE_DATA) return;
401
+ }
402
+ }
403
+
404
+ const NotifyOutcome = union(enum) {
405
+ result: NotifyResult,
406
+ /// No endpoint exists under the name.
407
+ vanished,
408
+ };
409
+
410
+ /// Hands one launch to the endpoint's owner and waits for its reply.
411
+ fn notify(name: [*:0]const u16, launch: []const u8, deadline: u64) NotifyOutcome {
412
+ if (comptime !is_windows) unreachable;
413
+ const pipe = connect: while (true) {
414
+ const candidate = win.CreateFileW(name, win.GENERIC_READ | win.GENERIC_WRITE, 0, null, win.OPEN_EXISTING, win.FILE_FLAG_OVERLAPPED, null);
415
+ if (candidate != win.INVALID_HANDLE_VALUE) break :connect candidate;
416
+ switch (lastError()) {
417
+ win.ERROR_FILE_NOT_FOUND => return .vanished,
418
+ win.ERROR_PIPE_BUSY => {
419
+ // The owner is serving another launch.
420
+ const remaining = remainingMs(deadline) orelse return .{ .result = .not_responding };
421
+ _ = win.WaitNamedPipeW(name, waitMs(remaining));
422
+ },
423
+ else => return .{ .result = .ipc_failed },
424
+ }
425
+ };
426
+ defer _ = win.CloseHandle(pipe);
427
+ return .{ .result = transact(pipe, launch, deadline) };
428
+ }
429
+
430
+ fn transact(pipe: win.HANDLE, launch: []const u8, deadline: u64) NotifyResult {
431
+ // A name squatted by another user, or by another session, gets nothing.
432
+ if (!trustedEndpoint(pipe)) return .ipc_failed;
433
+ var server_pid: win.DWORD = 0;
434
+ if (win.GetNamedPipeServerProcessId(pipe, &server_pid) == 0) return .ipc_failed;
435
+ // Lets the primary's window come to the front when its core shows
436
+ // or focuses it; without this Windows only flashes the taskbar.
437
+ _ = win.AllowSetForegroundWindow(server_pid);
438
+ var mode: win.DWORD = win.PIPE_READMODE_MESSAGE;
439
+ if (win.SetNamedPipeHandleState(pipe, &mode, null, null) == 0) return .ipc_failed;
440
+
441
+ var request: [request_capacity]u8 = undefined;
442
+ @memcpy(request[0..request_magic.len], request_magic);
443
+ @memcpy(request[request_magic.len..][0..launch.len], launch);
444
+ const event = win.CreateEventW(null, 1, 0, null) orelse return .ipc_failed;
445
+ defer _ = win.CloseHandle(event);
446
+ _ = transfer(pipe, event, .write, request[0 .. request_magic.len + launch.len], deadline) catch |err| return ioFailure(err);
447
+ var reply: [1]u8 = undefined;
448
+ const read = transfer(pipe, event, .read, &reply, deadline) catch |err| return ioFailure(err);
449
+ if (read != reply.len) return .ipc_failed;
450
+ return @as(Reply, @enumFromInt(reply[0])).result();
451
+ }
452
+
453
+ fn ioFailure(err: TransferError) NotifyResult {
454
+ return switch (err) {
455
+ error.Timeout => .not_responding,
456
+ error.Failed => .ipc_failed,
457
+ };
458
+ }
459
+
460
+ const TransferError = error{ Timeout, Failed };
461
+
462
+ /// One overlapped read or write, bounded by `deadline`.
463
+ fn transfer(pipe: win.HANDLE, event: win.HANDLE, comptime direction: enum { read, write }, buffer: []u8, deadline: u64) TransferError!win.DWORD {
464
+ var overlapped: win.OVERLAPPED = .{ .hEvent = event };
465
+ const length: win.DWORD = @intCast(buffer.len);
466
+ const started = switch (direction) {
467
+ .read => win.ReadFile(pipe, buffer.ptr, length, null, &overlapped),
468
+ .write => win.WriteFile(pipe, buffer.ptr, length, null, &overlapped),
469
+ };
470
+ if (started == 0 and lastError() != win.ERROR_IO_PENDING) return error.Failed;
471
+ const waited = win.WaitForSingleObject(event, waitMs(remainingMs(deadline) orelse 0));
472
+ if (waited != win.WAIT_OBJECT_0) {
473
+ _ = win.CancelIo(pipe);
474
+ var ignored: win.DWORD = 0;
475
+ _ = win.GetOverlappedResult(pipe, &overlapped, &ignored, 1);
476
+ return if (waited == win.WAIT_TIMEOUT) error.Timeout else error.Failed;
477
+ }
478
+ var transferred: win.DWORD = 0;
479
+ if (win.GetOverlappedResult(pipe, &overlapped, &transferred, 0) == 0) return error.Failed;
480
+ return transferred;
481
+ }
482
+
483
+ /// A finite Win32 wait: never 0 for WaitNamedPipe's "default" or
484
+ /// INFINITE's all-ones.
485
+ fn waitMs(remaining: u64) win.DWORD {
486
+ return @intCast(std.math.clamp(remaining, 1, std.math.maxInt(win.DWORD) - 1));
487
+ }
488
+
489
+ /// `\\.\pipe\native-sdk-instance-<session>-<hash>`: pipe names are
490
+ /// machine-wide, so the session and a hash of the bundle id and user SID
491
+ /// keep each user's sessions apart.
492
+ const pipe_name_capacity = 96;
493
+
494
+ fn pipeName(buffer: *[pipe_name_capacity:0]u16, session: u32, sid_text: []const u16, bundle_id: []const u8) [:0]const u16 {
495
+ var hasher = std.hash.Wyhash.init(0);
496
+ hasher.update(bundle_id);
497
+ hasher.update("\x00");
498
+ hasher.update(std.mem.sliceAsBytes(sid_text));
499
+ var ascii: [pipe_name_capacity]u8 = undefined;
500
+ const text = std.fmt.bufPrint(&ascii, "\\\\.\\pipe\\native-sdk-instance-{d}-{x:0>16}", .{ session, hasher.final() }) catch unreachable;
501
+ for (text, 0..) |byte, index| buffer[index] = byte;
502
+ buffer[text.len] = 0;
503
+ return buffer[0..text.len :0];
504
+ }
505
+
506
+ /// The current user's SID (as SDDL text) and login session.
507
+ const Identity = struct {
508
+ session: u32,
509
+ sid_text: [sid_text_capacity]u16,
510
+ sid_text_len: usize,
511
+
512
+ const sid_text_capacity = 192;
513
+
514
+ fn loadCurrent(self: *Identity) bool {
515
+ if (comptime !is_windows) return false;
516
+ if (win.ProcessIdToSessionId(win.GetCurrentProcessId(), &self.session) == 0) return false;
517
+ var token_buffer: [token_user_capacity]u8 align(@alignOf(usize)) = undefined;
518
+ const sid = tokenUserSid(win.GetCurrentProcess(), &token_buffer) orelse return false;
519
+ var text: ?[*:0]u16 = null;
520
+ if (win.ConvertSidToStringSidW(sid, &text) == 0) return false;
521
+ const owned_text = text orelse return false;
522
+ defer _ = win.LocalFree(@ptrCast(owned_text));
523
+ const span = std.mem.span(owned_text);
524
+ if (span.len > self.sid_text.len) return false;
525
+ @memcpy(self.sid_text[0..span.len], span);
526
+ self.sid_text_len = span.len;
527
+ return true;
528
+ }
529
+
530
+ fn sidText(self: *const Identity) []const u16 {
531
+ return self.sid_text[0..self.sid_text_len];
532
+ }
533
+ };
534
+
535
+ /// The endpoint's descriptor: owned by the current user (an elevated
536
+ /// token would otherwise make Administrators the owner, which the owner
537
+ /// check below would refuse), a protected DACL granting that user alone,
538
+ /// and a medium mandatory label so a non-elevated later launch can reach
539
+ /// an elevated owner.
540
+ const Security = struct {
541
+ descriptor: ?*anyopaque = null,
542
+ attributes: win.SECURITY_ATTRIBUTES = undefined,
543
+
544
+ fn init(self: *Security, sid_text: []const u16) bool {
545
+ if (comptime !is_windows) return false;
546
+ const owner_part = std.unicode.utf8ToUtf16LeStringLiteral("O:");
547
+ const dacl_part = std.unicode.utf8ToUtf16LeStringLiteral("D:P(A;;GA;;;");
548
+ const label_part = std.unicode.utf8ToUtf16LeStringLiteral(")S:(ML;;NW;;;ME)");
549
+ var sddl: [owner_part.len + dacl_part.len + label_part.len + 2 * Identity.sid_text_capacity:0]u16 = undefined;
550
+ var len: usize = 0;
551
+ for ([_][]const u16{ owner_part, sid_text, dacl_part, sid_text, label_part }) |part| {
552
+ @memcpy(sddl[len..][0..part.len], part);
553
+ len += part.len;
554
+ }
555
+ sddl[len] = 0;
556
+ self.descriptor = null;
557
+ if (win.ConvertStringSecurityDescriptorToSecurityDescriptorW(&sddl, win.SDDL_REVISION_1, &self.descriptor, null) == 0) return false;
558
+ self.attributes = .{
559
+ .nLength = @sizeOf(win.SECURITY_ATTRIBUTES),
560
+ .lpSecurityDescriptor = self.descriptor,
561
+ .bInheritHandle = .FALSE,
562
+ };
563
+ return true;
564
+ }
565
+
566
+ fn deinit(self: *Security) void {
567
+ if (self.descriptor) |descriptor| _ = win.LocalFree(descriptor);
568
+ self.descriptor = null;
569
+ }
570
+ };
571
+
572
+ const token_user_capacity = 256;
573
+
574
+ /// The token user's SID, pointing into `buffer`.
575
+ fn tokenUserSid(process: win.HANDLE, buffer: *align(@alignOf(usize)) [token_user_capacity]u8) ?*anyopaque {
576
+ var token: win.HANDLE = undefined;
577
+ if (win.OpenProcessToken(process, win.TOKEN_QUERY, &token) == 0) return null;
578
+ defer _ = win.CloseHandle(token);
579
+ var needed: win.DWORD = 0;
580
+ if (win.GetTokenInformation(token, win.TokenUser, buffer, buffer.len, &needed) == 0) return null;
581
+ const user: *const win.TOKEN_USER = @ptrCast(buffer);
582
+ return user.User.Sid;
583
+ }
584
+
585
+ /// Whether a connected endpoint belongs to this user in this login
586
+ /// session. Reads only the pipe handle: querying the owner's process
587
+ /// would need PROCESS_QUERY_INFORMATION, which a non-elevated launch
588
+ /// never holds on an elevated owner.
589
+ fn trustedEndpoint(pipe: win.HANDLE) bool {
590
+ var own_session: win.DWORD = 0;
591
+ var server_session: win.DWORD = 0;
592
+ if (win.ProcessIdToSessionId(win.GetCurrentProcessId(), &own_session) == 0) return false;
593
+ if (win.GetNamedPipeServerSessionId(pipe, &server_session) == 0) return false;
594
+ if (own_session != server_session) return false;
595
+ var endpoint_owner: ?*anyopaque = null;
596
+ var descriptor: ?*anyopaque = null;
597
+ if (win.GetSecurityInfo(pipe, win.SE_FILE_OBJECT, win.OWNER_SECURITY_INFORMATION, &endpoint_owner, null, null, null, &descriptor) != 0) return false;
598
+ defer _ = win.LocalFree(descriptor);
599
+ var own_buffer: [token_user_capacity]u8 align(@alignOf(usize)) = undefined;
600
+ const own = tokenUserSid(win.GetCurrentProcess(), &own_buffer) orelse return false;
601
+ const found = endpoint_owner orelse return false;
602
+ return win.EqualSid(own, found) != 0;
603
+ }
604
+
605
+ fn lastError() u32 {
606
+ return @intFromEnum(std.os.windows.GetLastError());
607
+ }
608
+
609
+ /// Self-contained Win32 surface, declared here like `pty_windows.zig`
610
+ /// does so the runtime keeps no dependence on the platform layer.
611
+ const win = struct {
612
+ const HANDLE = std.os.windows.HANDLE;
613
+ const DWORD = u32;
614
+ const BOOL = i32;
615
+ const SECURITY_ATTRIBUTES = std.os.windows.SECURITY_ATTRIBUTES;
616
+ const OVERLAPPED = extern struct {
617
+ Internal: usize = 0,
618
+ InternalHigh: usize = 0,
619
+ Offset: DWORD = 0,
620
+ OffsetHigh: DWORD = 0,
621
+ hEvent: ?HANDLE = null,
622
+ };
623
+ const SID_AND_ATTRIBUTES = extern struct {
624
+ Sid: *anyopaque,
625
+ Attributes: DWORD,
626
+ };
627
+ const TOKEN_USER = extern struct {
628
+ User: SID_AND_ATTRIBUTES,
629
+ };
630
+
631
+ const INVALID_HANDLE_VALUE = std.os.windows.INVALID_HANDLE_VALUE;
632
+ const GENERIC_READ: DWORD = 0x8000_0000;
633
+ const GENERIC_WRITE: DWORD = 0x4000_0000;
634
+ const OPEN_EXISTING: DWORD = 3;
635
+ const PIPE_ACCESS_DUPLEX: DWORD = 0x3;
636
+ const FILE_FLAG_FIRST_PIPE_INSTANCE: DWORD = 0x0008_0000;
637
+ const FILE_FLAG_OVERLAPPED: DWORD = 0x4000_0000;
638
+ const PIPE_TYPE_MESSAGE: DWORD = 0x4;
639
+ const PIPE_READMODE_MESSAGE: DWORD = 0x2;
640
+ const PIPE_WAIT: DWORD = 0x0;
641
+ const PIPE_REJECT_REMOTE_CLIENTS: DWORD = 0x8;
642
+ const TOKEN_QUERY: DWORD = 0x0008;
643
+ const TokenUser: u32 = 1;
644
+ const SDDL_REVISION_1: DWORD = 1;
645
+ const SE_FILE_OBJECT: u32 = 1;
646
+ const OWNER_SECURITY_INFORMATION: DWORD = 0x1;
647
+ const WAIT_OBJECT_0: DWORD = 0;
648
+ const WAIT_TIMEOUT: DWORD = 0x102;
649
+ const ERROR_FILE_NOT_FOUND: u32 = 2;
650
+ const ERROR_ACCESS_DENIED: u32 = 5;
651
+ const ERROR_PIPE_BUSY: u32 = 231;
652
+ const ERROR_NO_DATA: u32 = 232;
653
+ const ERROR_MORE_DATA: u32 = 234;
654
+ const ERROR_PIPE_CONNECTED: u32 = 535;
655
+ const ERROR_IO_PENDING: u32 = 997;
656
+
657
+ extern "kernel32" fn CreateNamedPipeW(
658
+ lpName: [*:0]const u16,
659
+ dwOpenMode: DWORD,
660
+ dwPipeMode: DWORD,
661
+ nMaxInstances: DWORD,
662
+ nOutBufferSize: DWORD,
663
+ nInBufferSize: DWORD,
664
+ nDefaultTimeOut: DWORD,
665
+ lpSecurityAttributes: ?*SECURITY_ATTRIBUTES,
666
+ ) callconv(.winapi) HANDLE;
667
+ extern "kernel32" fn ConnectNamedPipe(hNamedPipe: HANDLE, lpOverlapped: ?*OVERLAPPED) callconv(.winapi) BOOL;
668
+ extern "kernel32" fn DisconnectNamedPipe(hNamedPipe: HANDLE) callconv(.winapi) BOOL;
669
+ extern "kernel32" fn WaitNamedPipeW(lpNamedPipeName: [*:0]const u16, nTimeOut: DWORD) callconv(.winapi) BOOL;
670
+ extern "kernel32" fn SetNamedPipeHandleState(hNamedPipe: HANDLE, lpMode: ?*DWORD, lpMaxCollectionCount: ?*DWORD, lpCollectDataTimeout: ?*DWORD) callconv(.winapi) BOOL;
671
+ extern "kernel32" fn GetNamedPipeServerProcessId(Pipe: HANDLE, ServerProcessId: *DWORD) callconv(.winapi) BOOL;
672
+ extern "kernel32" fn GetNamedPipeServerSessionId(Pipe: HANDLE, ServerSessionId: *DWORD) callconv(.winapi) BOOL;
673
+ extern "kernel32" fn CreateFileW(
674
+ lpFileName: [*:0]const u16,
675
+ dwDesiredAccess: DWORD,
676
+ dwShareMode: DWORD,
677
+ lpSecurityAttributes: ?*SECURITY_ATTRIBUTES,
678
+ dwCreationDisposition: DWORD,
679
+ dwFlagsAndAttributes: DWORD,
680
+ hTemplateFile: ?HANDLE,
681
+ ) callconv(.winapi) HANDLE;
682
+ extern "kernel32" fn ReadFile(hFile: HANDLE, lpBuffer: [*]u8, nNumberOfBytesToRead: DWORD, lpNumberOfBytesRead: ?*DWORD, lpOverlapped: ?*OVERLAPPED) callconv(.winapi) BOOL;
683
+ extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*OVERLAPPED) callconv(.winapi) BOOL;
684
+ extern "kernel32" fn FlushFileBuffers(hFile: HANDLE) callconv(.winapi) BOOL;
685
+ extern "kernel32" fn CancelIo(hFile: HANDLE) callconv(.winapi) BOOL;
686
+ extern "kernel32" fn GetOverlappedResult(hFile: HANDLE, lpOverlapped: *OVERLAPPED, lpNumberOfBytesTransferred: *DWORD, bWait: BOOL) callconv(.winapi) BOOL;
687
+ extern "kernel32" fn CreateEventW(lpEventAttributes: ?*SECURITY_ATTRIBUTES, bManualReset: BOOL, bInitialState: BOOL, lpName: ?[*:0]const u16) callconv(.winapi) ?HANDLE;
688
+ extern "kernel32" fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) callconv(.winapi) DWORD;
689
+ extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(.winapi) BOOL;
690
+ extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.winapi) void;
691
+ extern "kernel32" fn GetCurrentProcess() callconv(.winapi) HANDLE;
692
+ extern "kernel32" fn GetCurrentProcessId() callconv(.winapi) DWORD;
693
+ extern "kernel32" fn ProcessIdToSessionId(dwProcessId: DWORD, pSessionId: *DWORD) callconv(.winapi) BOOL;
694
+ extern "kernel32" fn LocalFree(hMem: ?*anyopaque) callconv(.winapi) ?*anyopaque;
695
+ extern "advapi32" fn OpenProcessToken(ProcessHandle: HANDLE, DesiredAccess: DWORD, TokenHandle: *HANDLE) callconv(.winapi) BOOL;
696
+ extern "advapi32" fn GetTokenInformation(TokenHandle: HANDLE, TokenInformationClass: u32, TokenInformation: ?*anyopaque, TokenInformationLength: DWORD, ReturnLength: *DWORD) callconv(.winapi) BOOL;
697
+ extern "advapi32" fn ConvertSidToStringSidW(Sid: *anyopaque, StringSid: *?[*:0]u16) callconv(.winapi) BOOL;
698
+ extern "advapi32" fn ConvertStringSecurityDescriptorToSecurityDescriptorW(StringSecurityDescriptor: [*:0]const u16, StringSDRevision: DWORD, SecurityDescriptor: *?*anyopaque, SecurityDescriptorSize: ?*u32) callconv(.winapi) BOOL;
699
+ extern "advapi32" fn EqualSid(pSid1: *anyopaque, pSid2: *anyopaque) callconv(.winapi) BOOL;
700
+ extern "advapi32" fn GetSecurityInfo(
701
+ handle: HANDLE,
702
+ ObjectType: u32,
703
+ SecurityInfo: DWORD,
704
+ ppsidOwner: ?*?*anyopaque,
705
+ ppsidGroup: ?*?*anyopaque,
706
+ ppDacl: ?*?*anyopaque,
707
+ ppSacl: ?*?*anyopaque,
708
+ ppSecurityDescriptor: ?*?*anyopaque,
709
+ ) callconv(.winapi) DWORD;
710
+ extern "user32" fn AllowSetForegroundWindow(dwProcessId: DWORD) callconv(.winapi) BOOL;
711
+ };
712
+
713
+ const testing = std.testing;
714
+
715
+ test "a launch is the working directory then each argument, NUL-separated" {
716
+ var buffer: [64]u8 = undefined;
717
+ var writer: LaunchWriter = .init(&buffer, "C:\\work");
718
+ writer.addArgument("--open");
719
+ writer.addArgument("");
720
+ try testing.expectEqualStrings("C:\\work\x00--open\x00", writer.bytes().?);
721
+
722
+ var bare_buffer: [16]u8 = undefined;
723
+ const bare: LaunchWriter = .init(&bare_buffer, "/tmp");
724
+ try testing.expectEqualStrings("/tmp", bare.bytes().?);
725
+ }
726
+
727
+ test "a launch that does not fit encodes to nothing" {
728
+ var buffer: [8]u8 = undefined;
729
+ var writer: LaunchWriter = .init(&buffer, "C:\\work");
730
+ writer.addArgument("x");
731
+ try testing.expect(writer.bytes() == null);
732
+ writer.addArgument("");
733
+ try testing.expect(writer.bytes() == null);
734
+ }
735
+
736
+ test "every notification result has its own exit code and only delivery exits zero" {
737
+ const results = std.enums.values(NotifyResult);
738
+ for (results, 0..) |result, index| {
739
+ try testing.expectEqual(result == .delivered, result.exitCode() == 0);
740
+ for (results[index + 1 ..]) |other| try testing.expect(result.exitCode() != other.exitCode());
741
+ }
742
+ }
743
+
744
+ test "only the first claim in a process claims; later calls are no-ops" {
745
+ owner.lock();
746
+ const saved = owner.claim_started;
747
+ owner.claim_started = false;
748
+ owner.unlock();
749
+ defer {
750
+ owner.lock();
751
+ owner.claim_started = saved;
752
+ owner.unlock();
753
+ }
754
+ try testing.expect(beginClaim());
755
+ // A later claim answers null before touching its arguments, so a
756
+ // standalone fallback is never retried mid-startup.
757
+ try testing.expect(claim(testing.io, testing.allocator, undefined, "test.native_sdk.single_instance") == null);
758
+ try testing.expect(!owned());
759
+ }
760
+
761
+ test "the source refuses arguments, and refuses a process that does not own the app" {
762
+ try testing.expectError(error.InvalidChannelSourceArguments, attach("x", .{}));
763
+ const expected = if (is_windows) error.SingleInstanceNotOwned else error.UnsupportedChannelSource;
764
+ try testing.expectError(expected, attach("", .{}));
765
+ }
766
+
767
+ test "a launch held with no live channel reports the core is not listening" {
768
+ owner.lock();
769
+ owner.handle = .{};
770
+ owner.unlock();
771
+ try testing.expectEqual(Reply.not_listening, deliverToCore(testing.io, "C:\\work", 0));
772
+ // The dead handle answered `.closed` and was forgotten.
773
+ try testing.expect(currentHandle() == null);
774
+ try testing.expectEqual(Reply.too_large, deliverToCore(testing.io, &([_]u8{'a'} ** (max_launch_bytes + 1)), 0));
775
+ }
776
+
777
+ test "the endpoint name separates bundle ids, users, and sessions" {
778
+ const sid_a = std.unicode.utf8ToUtf16LeStringLiteral("S-1-5-21-1-1001");
779
+ const sid_b = std.unicode.utf8ToUtf16LeStringLiteral("S-1-5-21-1-1002");
780
+ var first: [pipe_name_capacity:0]u16 = undefined;
781
+ var second: [pipe_name_capacity:0]u16 = undefined;
782
+ const base = pipeName(&first, 1, sid_a, "dev.example.app");
783
+ try testing.expect(std.mem.startsWith(u16, base, std.unicode.utf8ToUtf16LeStringLiteral("\\\\.\\pipe\\native-sdk-instance-1-")));
784
+ try testing.expectEqualSlices(u16, base, pipeName(&second, 1, sid_a, "dev.example.app"));
785
+ try testing.expect(!std.mem.eql(u16, base, pipeName(&second, 1, sid_a, "dev.example.other")));
786
+ try testing.expect(!std.mem.eql(u16, base, pipeName(&second, 1, sid_b, "dev.example.app")));
787
+ try testing.expect(!std.mem.eql(u16, base, pipeName(&second, 2, sid_a, "dev.example.app")));
788
+ }
789
+
790
+ /// A per-test endpoint name, so concurrent test runs never collide.
791
+ fn testPipeName(buffer: *[pipe_name_capacity:0]u16, label: []const u8) [:0]const u16 {
792
+ var bundle: [96]u8 = undefined;
793
+ const id = std.fmt.bufPrint(&bundle, "test.native_sdk.single_instance.{s}.{d}", .{ label, win.GetCurrentProcessId() }) catch unreachable;
794
+ return pipeName(buffer, 0, std.unicode.utf8ToUtf16LeStringLiteral("test"), id);
795
+ }
796
+
797
+ const Recorder = struct {
798
+ answer: Reply,
799
+ launch: [max_launch_bytes]u8 = undefined,
800
+ launch_len: usize = 0,
801
+
802
+ fn sink(self: *Recorder) Sink {
803
+ return .{ .context = self, .deliver_fn = deliver };
804
+ }
805
+
806
+ fn deliver(context: *anyopaque, launch: []const u8) Reply {
807
+ const self: *Recorder = @ptrCast(@alignCast(context));
808
+ @memcpy(self.launch[0..launch.len], launch);
809
+ self.launch_len = launch.len;
810
+ return self.answer;
811
+ }
812
+ };
813
+
814
+ test "exactly one process owns a name, and ownership ends with the owner" {
815
+ if (comptime !is_windows) return error.SkipZigTest;
816
+ var name_buffer: [pipe_name_capacity:0]u16 = undefined;
817
+ const name = testPipeName(&name_buffer, "ownership");
818
+ var security: Security = undefined;
819
+ try initTestSecurity(&security);
820
+ defer security.deinit();
821
+ const first = createEndpoint(name, &security.attributes);
822
+ try testing.expect(first == .owned);
823
+ try testing.expect(createEndpoint(name, &security.attributes) == .taken);
824
+ _ = win.CloseHandle(first.owned);
825
+ const next = createEndpoint(name, &security.attributes);
826
+ try testing.expect(next == .owned);
827
+ _ = win.CloseHandle(next.owned);
828
+ }
829
+
830
+ test "a later launch reaches the owner and carries the owner's answer back" {
831
+ if (comptime !is_windows) return error.SkipZigTest;
832
+ var name_buffer: [pipe_name_capacity:0]u16 = undefined;
833
+ const name = testPipeName(&name_buffer, "delivery");
834
+ var security: Security = undefined;
835
+ try initTestSecurity(&security);
836
+ defer security.deinit();
837
+ const created = createEndpoint(name, &security.attributes);
838
+ try testing.expect(created == .owned);
839
+ defer _ = win.CloseHandle(created.owned);
840
+
841
+ const answers = [_]struct { Reply, NotifyResult }{
842
+ .{ .accepted, .delivered },
843
+ .{ .not_listening, .not_listening },
844
+ .{ .queue_full, .queue_full },
845
+ };
846
+ for (answers) |pair| {
847
+ var recorder: Recorder = .{ .answer = pair[0] };
848
+ const server = try std.Thread.spawn(.{}, serveNext, .{ created.owned, recorder.sink() });
849
+ const outcome = notify(name, "C:\\work\x00--open\x00notes.txt", clock.monotonicMs() + 5_000);
850
+ server.join();
851
+ try testing.expectEqual(NotifyOutcome{ .result = pair[1] }, outcome);
852
+ try testing.expectEqualStrings("C:\\work\x00--open\x00notes.txt", recorder.launch[0..recorder.launch_len]);
853
+ }
854
+ }
855
+
856
+ test "a launch with no owner finds no endpoint, and a silent owner times out" {
857
+ if (comptime !is_windows) return error.SkipZigTest;
858
+ var name_buffer: [pipe_name_capacity:0]u16 = undefined;
859
+ const name = testPipeName(&name_buffer, "silent");
860
+ try testing.expectEqual(NotifyOutcome.vanished, notify(name, "C:\\work", clock.monotonicMs() + 1_000));
861
+
862
+ var security: Security = undefined;
863
+ try initTestSecurity(&security);
864
+ defer security.deinit();
865
+ const created = createEndpoint(name, &security.attributes);
866
+ try testing.expect(created == .owned);
867
+ defer _ = win.CloseHandle(created.owned);
868
+ try testing.expectEqual(NotifyOutcome{ .result = .not_responding }, notify(name, "C:\\work", clock.monotonicMs() + 200));
869
+ }
870
+
871
+ test "an endpoint with the launcher's descriptor passes the handle-only owner check" {
872
+ if (comptime !is_windows) return error.SkipZigTest;
873
+ var name_buffer: [pipe_name_capacity:0]u16 = undefined;
874
+ const name = testPipeName(&name_buffer, "trust");
875
+ var security: Security = undefined;
876
+ try initTestSecurity(&security);
877
+ defer security.deinit();
878
+ const created = createEndpoint(name, &security.attributes);
879
+ try testing.expect(created == .owned);
880
+ defer _ = win.CloseHandle(created.owned);
881
+ const client = win.CreateFileW(name, win.GENERIC_READ | win.GENERIC_WRITE, 0, null, win.OPEN_EXISTING, win.FILE_FLAG_OVERLAPPED, null);
882
+ try testing.expect(client != win.INVALID_HANDLE_VALUE);
883
+ defer _ = win.CloseHandle(client);
884
+ try testing.expect(trustedEndpoint(client));
885
+ }
886
+
887
+ /// The launcher's own descriptor for the current user, so the endpoint
888
+ /// tests exercise the owner, DACL, and label production creates.
889
+ fn initTestSecurity(security: *Security) !void {
890
+ var identity: Identity = undefined;
891
+ try testing.expect(identity.loadCurrent());
892
+ try testing.expect(security.init(identity.sidText()));
893
+ }