@zogien/native-sdk 0.11.0-fork.4 → 0.11.0-fork.5
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.
- package/build/app.zig +50 -34
- package/package.json +9 -9
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/src/platform/linux/cef_host.cpp +22 -0
- package/src/platform/linux/gtk_host.c +317 -34
- package/src/platform/linux/gtk_host.h +37 -0
- package/src/platform/linux/root.zig +114 -20
- package/src/platform/macos/appkit_host.h +44 -0
- package/src/platform/macos/appkit_host.m +301 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +117 -23
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +6 -0
- package/src/platform/types.zig +81 -0
- package/src/platform/windows/dialog_test.cpp +303 -0
- package/src/platform/windows/host_loop_test.cpp +169 -0
- package/src/platform/windows/root.zig +225 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1064 -280
- package/src/root.zig +7 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/core.zig +24 -3
- package/src/runtime/dialog_tests.zig +367 -0
- package/src/runtime/dialogs.zig +385 -0
- package/src/runtime/effects.zig +71 -2
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +14 -6
- package/src/runtime/flow_reference.zig +14 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +5 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/tools/native-sdk/main.zig +1 -1
|
@@ -143,8 +143,8 @@ extern fn native_sdk_windows_create(app_name: [*]const u8, app_name_len: usize,
|
|
|
143
143
|
extern fn native_sdk_windows_destroy(host: *WindowsHost) void;
|
|
144
144
|
extern fn native_sdk_windows_run(host: *WindowsHost, callback: WindowsCallback, context: ?*anyopaque) void;
|
|
145
145
|
extern fn native_sdk_windows_stop(host: *WindowsHost) void;
|
|
146
|
-
extern fn native_sdk_windows_wake(host: *WindowsHost)
|
|
147
|
-
extern fn native_sdk_windows_request_frame(host: *WindowsHost)
|
|
146
|
+
extern fn native_sdk_windows_wake(host: *WindowsHost) c_int;
|
|
147
|
+
extern fn native_sdk_windows_request_frame(host: *WindowsHost) c_int;
|
|
148
148
|
extern fn native_sdk_windows_decode_image(bytes: [*]const u8, bytes_len: usize, pixels: [*]u8, pixels_len: usize, max_pixels: usize, out_width: *usize, out_height: *usize) c_int;
|
|
149
149
|
extern fn native_sdk_windows_load_webview(host: *WindowsHost, source: [*]const u8, source_len: usize, source_kind: c_int, asset_root: [*]const u8, asset_root_len: usize, asset_entry: [*]const u8, asset_entry_len: usize, asset_origin: [*]const u8, asset_origin_len: usize, spa_fallback: c_int) void;
|
|
150
150
|
extern fn native_sdk_windows_load_window_webview(host: *WindowsHost, window_id: u64, source: [*]const u8, source_len: usize, source_kind: c_int, asset_root: [*]const u8, asset_root_len: usize, asset_entry: [*]const u8, asset_entry_len: usize, asset_origin: [*]const u8, asset_origin_len: usize, spa_fallback: c_int) c_int;
|
|
@@ -197,6 +197,9 @@ extern fn native_sdk_windows_reveal_path(host: *WindowsHost, path: [*]const u8,
|
|
|
197
197
|
extern fn native_sdk_windows_show_open_dialog(host: *WindowsHost, opts: *const WindowsOpenDialogOpts, buffer: [*]u8, buffer_len: usize) WindowsOpenDialogResult;
|
|
198
198
|
extern fn native_sdk_windows_show_save_dialog(host: *WindowsHost, opts: *const WindowsSaveDialogOpts, buffer: [*]u8, buffer_len: usize) usize;
|
|
199
199
|
extern fn native_sdk_windows_show_message_dialog(host: *WindowsHost, opts: *const WindowsMessageDialogOpts) c_int;
|
|
200
|
+
extern fn native_sdk_windows_request_dialog(host: *WindowsHost, request: *const WindowsDialogRequest) c_int;
|
|
201
|
+
extern fn native_sdk_windows_cancel_dialog(host: *WindowsHost, id: u64) void;
|
|
202
|
+
extern fn native_sdk_windows_poll_dialog_result(host: *WindowsHost, buffer: [*]u8, buffer_len: usize, result: *WindowsDialogResult) c_int;
|
|
200
203
|
extern fn native_sdk_windows_show_notification(host: *WindowsHost, title: [*]const u8, title_len: usize, subtitle: [*]const u8, subtitle_len: usize, body: [*]const u8, body_len: usize, notification_id: [*]const u8, notification_id_len: usize, action_label: [*]const u8, action_label_len: usize, action_command: [*]const u8, action_command_len: usize) c_int;
|
|
201
204
|
extern fn native_sdk_windows_create_tray(host: *WindowsHost, icon_path: [*]const u8, icon_path_len: usize, tooltip: [*]const u8, tooltip_len: usize, activation_command: [*]const u8, activation_command_len: usize, alternate_activation_command: [*]const u8, alternate_activation_command_len: usize, open_command: [*]const u8, open_command_len: usize) c_int;
|
|
202
205
|
extern fn native_sdk_windows_update_tray_menu(host: *WindowsHost, item_ids: [*]const u32, labels: [*]const [*]const u8, label_lens: [*]const usize, separators: [*]const c_int, enabled_flags: [*]const c_int, details: [*]const [*]const u8, detail_lens: [*]const usize, roles: [*]const c_int, keys: [*]const [*]const u8, key_lens: [*]const usize, modifiers: [*]const u32, count: usize) c_int;
|
|
@@ -277,6 +280,23 @@ const WindowsMessageDialogOpts = extern struct {
|
|
|
277
280
|
tertiary_button_len: usize,
|
|
278
281
|
};
|
|
279
282
|
|
|
283
|
+
const WindowsDialogRequest = extern struct {
|
|
284
|
+
id: u64,
|
|
285
|
+
parent_window_id: u64,
|
|
286
|
+
kind: c_int,
|
|
287
|
+
open_opts: ?*const WindowsOpenDialogOpts,
|
|
288
|
+
save_opts: ?*const WindowsSaveDialogOpts,
|
|
289
|
+
message_opts: ?*const WindowsMessageDialogOpts,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const WindowsDialogResult = extern struct {
|
|
293
|
+
id: u64,
|
|
294
|
+
outcome: c_int,
|
|
295
|
+
button: c_int,
|
|
296
|
+
count: usize,
|
|
297
|
+
bytes_written: usize,
|
|
298
|
+
};
|
|
299
|
+
|
|
280
300
|
/// The startup-window twin of the runtime's create-time `.hide` gate:
|
|
281
301
|
/// on windows, hide-on-close is honest only when the app declares a
|
|
282
302
|
/// tray (status item) — SW_HIDE removes the taskbar entry and windows
|
|
@@ -477,6 +497,9 @@ pub const WindowsPlatform = struct {
|
|
|
477
497
|
.show_open_dialog_fn = showOpenDialog,
|
|
478
498
|
.show_save_dialog_fn = showSaveDialog,
|
|
479
499
|
.show_message_dialog_fn = showMessageDialog,
|
|
500
|
+
.request_dialog_fn = if (self.web_engine == .system) requestDialog else null,
|
|
501
|
+
.cancel_dialog_fn = if (self.web_engine == .system) cancelDialog else null,
|
|
502
|
+
.poll_dialog_result_fn = if (self.web_engine == .system) pollDialogResult else null,
|
|
480
503
|
.show_notification_fn = showNotification,
|
|
481
504
|
.create_tray_fn = createTray,
|
|
482
505
|
.update_tray_menu_fn = updateTrayMenu,
|
|
@@ -901,13 +924,14 @@ fn emitWindowEvent(context: ?*anyopaque, window_id: platform_mod.WindowId, name:
|
|
|
901
924
|
native_sdk_windows_emit_window_event(self.host, window_id, name.ptr, name.len, detail_json.ptr, detail_json.len);
|
|
902
925
|
}
|
|
903
926
|
|
|
904
|
-
/// Thread-safe: `PostMessageW`
|
|
905
|
-
/// enqueue-only shape the wake contract requires — never the
|
|
927
|
+
/// Thread-safe: `PostMessageW` to the host's loop window (the
|
|
928
|
+
/// enqueue-only shape the wake contract requires — no lock, never the
|
|
906
929
|
/// synchronous `SendMessage`), whose window procedure emits `.wake` on
|
|
907
|
-
/// the loop thread.
|
|
930
|
+
/// the loop thread. A wake that could not be posted is an error, so the
|
|
931
|
+
/// caller re-arms instead of assuming the nudge happened.
|
|
908
932
|
fn wake(context: ?*anyopaque) anyerror!void {
|
|
909
933
|
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
910
|
-
native_sdk_windows_wake(self.host);
|
|
934
|
+
if (native_sdk_windows_wake(self.host) == 0) return error.WakeNotPosted;
|
|
911
935
|
}
|
|
912
936
|
|
|
913
937
|
/// Teardown abandoned an in-flight channel wake call: latch the flag
|
|
@@ -918,13 +942,14 @@ fn noteChannelWakeAbandoned(context: ?*anyopaque) void {
|
|
|
918
942
|
self.channel_wake_abandoned.store(true, .seq_cst);
|
|
919
943
|
}
|
|
920
944
|
|
|
921
|
-
/// Thread-safe like `wake`: `PostMessage`
|
|
922
|
-
/// window procedure emits one `.frame` event on the loop thread
|
|
923
|
-
///
|
|
924
|
-
///
|
|
945
|
+
/// Thread-safe like `wake`: `PostMessage` to the loop window, whose
|
|
946
|
+
/// window procedure emits one `.frame` event on the loop thread;
|
|
947
|
+
/// requests coalesce while one is queued. The automation arrival
|
|
948
|
+
/// watcher calls this when a command lands so consumption never depends
|
|
949
|
+
/// on the host's own frame pump.
|
|
925
950
|
fn requestFrame(context: ?*anyopaque) anyerror!void {
|
|
926
951
|
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
927
|
-
native_sdk_windows_request_frame(self.host);
|
|
952
|
+
if (native_sdk_windows_request_frame(self.host) == 0) return error.FrameRequestNotPosted;
|
|
928
953
|
}
|
|
929
954
|
|
|
930
955
|
/// Headless image codec for session replay: the SAME WIC decode a live
|
|
@@ -1481,12 +1506,9 @@ fn closeWebView(context: ?*anyopaque, window_id: platform_mod.WindowId, label: [
|
|
|
1481
1506
|
if (native_sdk_windows_close_webview(self.host, window_id, label.ptr, label.len) == 0) return error.WebViewNotFound;
|
|
1482
1507
|
}
|
|
1483
1508
|
|
|
1484
|
-
fn
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
var ext_buf: [1024]u8 = undefined;
|
|
1488
|
-
const ext_str = flattenFilters(options.filters, &ext_buf);
|
|
1489
|
-
const opts = WindowsOpenDialogOpts{
|
|
1509
|
+
fn windowsOpenDialogOpts(options: platform_mod.OpenDialogOptions, ext_buf: []u8) WindowsOpenDialogOpts {
|
|
1510
|
+
const ext_str = flattenFilters(options.filters, ext_buf);
|
|
1511
|
+
return .{
|
|
1490
1512
|
.title = options.title.ptr,
|
|
1491
1513
|
.title_len = options.title.len,
|
|
1492
1514
|
.default_path = options.default_path.ptr,
|
|
@@ -1496,17 +1518,11 @@ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions,
|
|
|
1496
1518
|
.allow_directories = if (options.allow_directories) 1 else 0,
|
|
1497
1519
|
.allow_multiple = if (options.allow_multiple) 1 else 0,
|
|
1498
1520
|
};
|
|
1499
|
-
const result = native_sdk_windows_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
|
|
1500
|
-
if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
|
|
1501
|
-
return .{ .count = result.count, .paths = buffer[0..result.bytes_written] };
|
|
1502
1521
|
}
|
|
1503
1522
|
|
|
1504
|
-
fn
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
var ext_buf: [1024]u8 = undefined;
|
|
1508
|
-
const ext_str = flattenFilters(options.filters, &ext_buf);
|
|
1509
|
-
const opts = WindowsSaveDialogOpts{
|
|
1523
|
+
fn windowsSaveDialogOpts(options: platform_mod.SaveDialogOptions, ext_buf: []u8) WindowsSaveDialogOpts {
|
|
1524
|
+
const ext_str = flattenFilters(options.filters, ext_buf);
|
|
1525
|
+
return .{
|
|
1510
1526
|
.title = options.title.ptr,
|
|
1511
1527
|
.title_len = options.title.len,
|
|
1512
1528
|
.default_path = options.default_path.ptr,
|
|
@@ -1516,16 +1532,10 @@ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions,
|
|
|
1516
1532
|
.extensions = ext_str.ptr,
|
|
1517
1533
|
.extensions_len = ext_str.len,
|
|
1518
1534
|
};
|
|
1519
|
-
const written = native_sdk_windows_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
|
|
1520
|
-
if (written > buffer.len) return error.NoSpaceLeft;
|
|
1521
|
-
if (written == 0) return null;
|
|
1522
|
-
return buffer[0..written];
|
|
1523
1535
|
}
|
|
1524
1536
|
|
|
1525
|
-
fn
|
|
1526
|
-
|
|
1527
|
-
if (self.web_engine != .system) return error.UnsupportedService;
|
|
1528
|
-
const opts = WindowsMessageDialogOpts{
|
|
1537
|
+
fn windowsMessageDialogOpts(options: platform_mod.MessageDialogOptions) WindowsMessageDialogOpts {
|
|
1538
|
+
return .{
|
|
1529
1539
|
.style = @intFromEnum(options.style),
|
|
1530
1540
|
.title = options.title.ptr,
|
|
1531
1541
|
.title_len = options.title.len,
|
|
@@ -1540,7 +1550,97 @@ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOp
|
|
|
1540
1550
|
.tertiary_button = options.tertiary_button.ptr,
|
|
1541
1551
|
.tertiary_button_len = options.tertiary_button.len,
|
|
1542
1552
|
};
|
|
1543
|
-
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
|
|
1556
|
+
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
1557
|
+
if (self.web_engine != .system) return error.UnsupportedService;
|
|
1558
|
+
var ext_buf: [1024]u8 = undefined;
|
|
1559
|
+
const opts = windowsOpenDialogOpts(options, &ext_buf);
|
|
1560
|
+
const result = native_sdk_windows_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
|
|
1561
|
+
if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
|
|
1562
|
+
return .{ .count = result.count, .paths = buffer[0..result.bytes_written] };
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
|
|
1566
|
+
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
1567
|
+
if (self.web_engine != .system) return error.UnsupportedService;
|
|
1568
|
+
var ext_buf: [1024]u8 = undefined;
|
|
1569
|
+
const opts = windowsSaveDialogOpts(options, &ext_buf);
|
|
1570
|
+
const written = native_sdk_windows_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
|
|
1571
|
+
if (written > buffer.len) return error.NoSpaceLeft;
|
|
1572
|
+
if (written == 0) return null;
|
|
1573
|
+
return buffer[0..written];
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
|
|
1577
|
+
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
1578
|
+
if (self.web_engine != .system) return error.UnsupportedService;
|
|
1579
|
+
const opts = windowsMessageDialogOpts(options);
|
|
1580
|
+
// Negative: a session end left the dialog unanswered (the host
|
|
1581
|
+
// cancelled it to unwind this dispatch); no button stands in for it.
|
|
1582
|
+
const pressed = native_sdk_windows_show_message_dialog(self.host, &opts);
|
|
1583
|
+
if (pressed < 0) return error.SessionEnding;
|
|
1584
|
+
return @enumFromInt(pressed);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// The host copies every option before returning, so they may live on this
|
|
1588
|
+
// stack; the result comes back through pollDialogResult on a later wake.
|
|
1589
|
+
fn requestDialog(context: ?*anyopaque, request: platform_mod.DialogRequest) anyerror!void {
|
|
1590
|
+
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
1591
|
+
var ext_buf: [1024]u8 = undefined;
|
|
1592
|
+
var open_opts: WindowsOpenDialogOpts = undefined;
|
|
1593
|
+
var save_opts: WindowsSaveDialogOpts = undefined;
|
|
1594
|
+
var message_opts: WindowsMessageDialogOpts = undefined;
|
|
1595
|
+
var native_request = WindowsDialogRequest{
|
|
1596
|
+
.id = request.id,
|
|
1597
|
+
.parent_window_id = request.parent_window_id orelse 0,
|
|
1598
|
+
.kind = @intFromEnum(std.meta.activeTag(request.options)),
|
|
1599
|
+
.open_opts = null,
|
|
1600
|
+
.save_opts = null,
|
|
1601
|
+
.message_opts = null,
|
|
1602
|
+
};
|
|
1603
|
+
switch (request.options) {
|
|
1604
|
+
.open => |options| {
|
|
1605
|
+
open_opts = windowsOpenDialogOpts(options, &ext_buf);
|
|
1606
|
+
native_request.open_opts = &open_opts;
|
|
1607
|
+
},
|
|
1608
|
+
.save => |options| {
|
|
1609
|
+
save_opts = windowsSaveDialogOpts(options, &ext_buf);
|
|
1610
|
+
native_request.save_opts = &save_opts;
|
|
1611
|
+
},
|
|
1612
|
+
.message => |options| {
|
|
1613
|
+
message_opts = windowsMessageDialogOpts(options);
|
|
1614
|
+
native_request.message_opts = &message_opts;
|
|
1615
|
+
},
|
|
1616
|
+
}
|
|
1617
|
+
if (native_sdk_windows_request_dialog(self.host, &native_request) == 0) return error.DialogUnavailable;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
fn cancelDialog(context: ?*anyopaque, id: platform_mod.DialogRequestId) void {
|
|
1621
|
+
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
1622
|
+
native_sdk_windows_cancel_dialog(self.host, id);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?platform_mod.DialogResult {
|
|
1626
|
+
const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
|
|
1627
|
+
var result: WindowsDialogResult = undefined;
|
|
1628
|
+
if (native_sdk_windows_poll_dialog_result(self.host, buffer.ptr, buffer.len, &result) == 0) return null;
|
|
1629
|
+
return .{
|
|
1630
|
+
.id = result.id,
|
|
1631
|
+
.outcome = switch (result.outcome) {
|
|
1632
|
+
0 => .selected,
|
|
1633
|
+
1 => .cancelled,
|
|
1634
|
+
else => .failed,
|
|
1635
|
+
},
|
|
1636
|
+
.paths = buffer[0..@min(result.bytes_written, buffer.len)],
|
|
1637
|
+
.count = result.count,
|
|
1638
|
+
.button = switch (result.button) {
|
|
1639
|
+
1 => .secondary,
|
|
1640
|
+
2 => .tertiary,
|
|
1641
|
+
else => .primary,
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1544
1644
|
}
|
|
1545
1645
|
|
|
1546
1646
|
fn showNotification(context: ?*anyopaque, options: platform_mod.NotificationOptions) anyerror!void {
|
|
@@ -2274,7 +2374,9 @@ test "windows tray carries lifecycle commands rich rows and key equivalents into
|
|
|
2274
2374
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "display_label += shortcutSuffix(item.key, item.modifiers);") != null);
|
|
2275
2375
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "const std::vector<TrayItem> displayed_items = host->tray_items;") != null);
|
|
2276
2376
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "emitTrayActionForCommandId(host, displayed_items, command_id);") != null);
|
|
2277
|
-
try std.testing.expect(std.mem.indexOf(u8, host_source, "
|
|
2377
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "out->kind = kTrayAction;") != null);
|
|
2378
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "out->tray_item_id = item.id;") != null);
|
|
2379
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "event.tray_item_id = match.tray_item_id;") != null);
|
|
2278
2380
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "tray_event == NIN_SELECT || tray_event == NIN_KEYSELECT || tray_event == WM_LBUTTONUP") != null);
|
|
2279
2381
|
}
|
|
2280
2382
|
|
|
@@ -2336,6 +2438,87 @@ test "windows WM_CLOSE hide consults the live tray state and downgrades to a rea
|
|
|
2336
2438
|
try std.testing.expect(std.mem.indexOf(u8, close_hook, "downgraded to a real close") != null);
|
|
2337
2439
|
}
|
|
2338
2440
|
|
|
2441
|
+
test "windows session end runs the stop hook once and bypasses the close policy" {
|
|
2442
|
+
// C++ this suite cannot execute, pinned textually like the WM_CLOSE
|
|
2443
|
+
// hook above. A confirmed WM_ENDSESSION (logoff, system shutdown, a
|
|
2444
|
+
// Restart Manager close) never goes through the close-policy hide;
|
|
2445
|
+
// the query only says yes; the run loop's exit and the session end
|
|
2446
|
+
// share one exactly-once delivery.
|
|
2447
|
+
const host_source = @embedFile("webview2_host.cpp");
|
|
2448
|
+
const query_at = std.mem.indexOf(u8, host_source, "case WM_QUERYENDSESSION:") orelse return error.TestExpectedEqual;
|
|
2449
|
+
const end_at = std.mem.indexOfPos(u8, host_source, query_at, "case WM_ENDSESSION:") orelse return error.TestExpectedEqual;
|
|
2450
|
+
const close_at = std.mem.indexOfPos(u8, host_source, end_at, "case WM_CLOSE:") orelse return error.TestExpectedEqual;
|
|
2451
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source[query_at..end_at], "return TRUE;") != null);
|
|
2452
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source[end_at..close_at], "if (host && wparam) endSession(host);") != null);
|
|
2453
|
+
// While a session end unwinds a dispatch, closes are ignored before
|
|
2454
|
+
// the close policy is consulted.
|
|
2455
|
+
const ending_close_at = std.mem.indexOfPos(u8, host_source, close_at, "if (host && host->session_ending) return 0;") orelse return error.TestExpectedEqual;
|
|
2456
|
+
const hide_at = std.mem.indexOfPos(u8, host_source, close_at, "ShowWindow(hwnd, SW_HIDE);") orelse return error.TestExpectedEqual;
|
|
2457
|
+
try std.testing.expect(ending_close_at < hide_at);
|
|
2458
|
+
|
|
2459
|
+
// Outside a runtime dispatch the shutdown runs synchronously, then
|
|
2460
|
+
// the tray icon goes and the process ends. Inside one, the runtime
|
|
2461
|
+
// queues the shutdown behind it, so the host marks the session
|
|
2462
|
+
// ending, hands the shutdown over, stops the loop, and cancels the
|
|
2463
|
+
// dialogs holding the dispatch, without ending the process.
|
|
2464
|
+
const session_at = std.mem.indexOf(u8, host_source, "static void endSession(Host *host) {") orelse return error.TestExpectedEqual;
|
|
2465
|
+
const session_end = std.mem.indexOfPos(u8, host_source, session_at, "\n}\n") orelse return error.TestExpectedEqual;
|
|
2466
|
+
const session_fn = host_source[session_at..session_end];
|
|
2467
|
+
const order = [_][]const u8{
|
|
2468
|
+
"if (host->shutdown_delivered) return;",
|
|
2469
|
+
"if (host->runtime_dispatch_depth == 0) {",
|
|
2470
|
+
"deliverShutdown(host);",
|
|
2471
|
+
"removeNotificationIcon(host);",
|
|
2472
|
+
"TerminateProcess(GetCurrentProcess(), 0);",
|
|
2473
|
+
"host->session_ending = true;",
|
|
2474
|
+
"deliverShutdown(host);",
|
|
2475
|
+
"host->running = false;",
|
|
2476
|
+
"EnumThreadWindows(GetCurrentThreadId(), closeOwnedDialog,",
|
|
2477
|
+
};
|
|
2478
|
+
var cursor: usize = 0;
|
|
2479
|
+
for (order) |needle| cursor = (std.mem.indexOfPos(u8, session_fn, cursor, needle) orelse return error.TestExpectedEqual) + needle.len;
|
|
2480
|
+
try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, session_fn, "TerminateProcess("));
|
|
2481
|
+
|
|
2482
|
+
const deliver_fn_at = std.mem.indexOf(u8, host_source, "static void deliverShutdown(Host *host) {") orelse return error.TestExpectedEqual;
|
|
2483
|
+
const flag_at = std.mem.indexOfPos(u8, host_source, deliver_fn_at, "host->shutdown_delivered = true;") orelse return error.TestExpectedEqual;
|
|
2484
|
+
const callback_at = std.mem.indexOfPos(u8, host_source, deliver_fn_at, "host->callback(host->callback_context, &shutdown);") orelse return error.TestExpectedEqual;
|
|
2485
|
+
// The flag latches before the callback: a session end arriving while
|
|
2486
|
+
// the stop hook runs (a modal loop inside it) must not deliver again.
|
|
2487
|
+
try std.testing.expect(flag_at < callback_at);
|
|
2488
|
+
try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, host_source, "kind = kShutdown;"));
|
|
2489
|
+
|
|
2490
|
+
// The dispatch depth sees every call into the runtime: events through
|
|
2491
|
+
// the counting trampoline the run loop installs, Bridge messages
|
|
2492
|
+
// through their one delivery site.
|
|
2493
|
+
const run_at = std.mem.indexOf(u8, host_source, "void native_sdk_windows_run(") orelse return error.TestExpectedEqual;
|
|
2494
|
+
const run_end = std.mem.indexOfPos(u8, host_source, run_at, "\n}\n") orelse return error.TestExpectedEqual;
|
|
2495
|
+
const run_fn = host_source[run_at..run_end];
|
|
2496
|
+
try std.testing.expect(std.mem.indexOf(u8, run_fn, "host->callback = dispatchEventToRuntime;") != null);
|
|
2497
|
+
try std.testing.expect(std.mem.indexOf(u8, run_fn, "deliverShutdown(host);") != null);
|
|
2498
|
+
const trampoline_at = std.mem.indexOf(u8, host_source, "static void dispatchEventToRuntime(") orelse return error.TestExpectedEqual;
|
|
2499
|
+
const scope_at = std.mem.indexOfPos(u8, host_source, trampoline_at, "RuntimeDispatchScope scope(host);") orelse return error.TestExpectedEqual;
|
|
2500
|
+
const forward_at = std.mem.indexOfPos(u8, host_source, trampoline_at, "host->runtime_callback(host->runtime_context, event);") orelse return error.TestExpectedEqual;
|
|
2501
|
+
try std.testing.expect(scope_at < forward_at);
|
|
2502
|
+
// Nothing enters the runtime after the shutdown was handed over, on
|
|
2503
|
+
// every path: the stop hook's own events and a closing dialog's.
|
|
2504
|
+
const gate_at = std.mem.indexOfPos(u8, host_source, trampoline_at, "if (host->shutdown_delivered && event->kind != kShutdown) return;") orelse return error.TestExpectedEqual;
|
|
2505
|
+
try std.testing.expect(gate_at < forward_at);
|
|
2506
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "if (!host->bridge_callback || host->shutdown_delivered) return;") != null);
|
|
2507
|
+
try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, host_source, "host->bridge_callback("));
|
|
2508
|
+
const bridge_at = std.mem.indexOf(u8, host_source, "host->bridge_callback(") orelse return error.TestExpectedEqual;
|
|
2509
|
+
const bridge_scope_at = std.mem.lastIndexOf(u8, host_source[0..bridge_at], "RuntimeDispatchScope scope(host);") orelse return error.TestExpectedEqual;
|
|
2510
|
+
try std.testing.expect(bridge_at - bridge_scope_at < 128);
|
|
2511
|
+
|
|
2512
|
+
// Dialogs the unwinding cancels, through any chain of owners, report
|
|
2513
|
+
// no answer rather than a button, and new ones are not shown;
|
|
2514
|
+
// session_end_test.cpp drives this on a Windows host.
|
|
2515
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "for (HWND owner = GetWindow(hwnd, GW_OWNER); owner; owner = GetWindow(owner, GW_OWNER)) {") != null);
|
|
2516
|
+
const message_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_show_message_dialog(Host *host, const WindowsMessageDialogOpts *opts) {") orelse return error.TestExpectedEqual;
|
|
2517
|
+
const message_end = std.mem.indexOfPos(u8, host_source, message_at, "\n}\n") orelse return error.TestExpectedEqual;
|
|
2518
|
+
try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, host_source[message_at..message_end], "if (host->session_ending) return -1;"));
|
|
2519
|
+
try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, host_source, "|| buffer_len == 0 || host->session_ending) return"));
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2339
2522
|
test "windows passive show never foregrounds or focuses the window" {
|
|
2340
2523
|
const host_source = @embedFile("webview2_host.cpp");
|
|
2341
2524
|
const show_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_show_window(") orelse return error.TestExpectedEqual;
|
|
@@ -2720,11 +2903,14 @@ test "windows window focus includes focused native child views" {
|
|
|
2720
2903
|
test "windows webview focus reports the focused child label" {
|
|
2721
2904
|
// WebView2 owns the page HWND and its input; GotFocus is the
|
|
2722
2905
|
// controller-level edge that mirrors keyboard ownership back into
|
|
2723
|
-
// the runtime's per-view register.
|
|
2906
|
+
// the runtime's per-view register. The callback queues the focused
|
|
2907
|
+
// view, and the loop window reports it once the callback returns.
|
|
2724
2908
|
const host_source = @embedFile("webview2_host.cpp");
|
|
2725
2909
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "add_GotFocus(Callback<ICoreWebView2FocusChangedEventHandler>") != null);
|
|
2726
|
-
try std.testing.expect(std.mem.indexOf(u8, host_source, "event.kind =
|
|
2727
|
-
try std.testing.expect(std.mem.indexOf(u8, host_source, "event.
|
|
2910
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "event.kind = DeferredWebViewEvent::Kind::Focus;") != null);
|
|
2911
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "event.label = focused->second.label;") != null);
|
|
2912
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "focused.kind = kViewFocused;") != null);
|
|
2913
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "focused.view_label = event.label.c_str();") != null);
|
|
2728
2914
|
}
|
|
2729
2915
|
|
|
2730
2916
|
test "windows webview2 keeps its user data with the app's per-user data" {
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/* Session end against the production Windows host, across processes:
|
|
2
|
+
* the test runs itself again as the app (--child <scenario>) and plays
|
|
3
|
+
* the sender, the way logoff and Restart Manager reach a GUI process:
|
|
4
|
+
* WM_QUERYENDSESSION, then WM_ENDSESSION, sent to its top-level window.
|
|
5
|
+
* The app's only window is hidden, like one behind a tray icon.
|
|
6
|
+
*
|
|
7
|
+
* flat No app dispatch is running. A cancelled end (wParam FALSE)
|
|
8
|
+
* changes nothing; a confirmed one delivers one shutdown and
|
|
9
|
+
* ends the process inside the message.
|
|
10
|
+
* nested The app is inside a dispatch, in a modal dialog. The dialog
|
|
11
|
+
* is cancelled and reports no answer instead of a button,
|
|
12
|
+
* later dialogs are refused, no event follows the shutdown, a
|
|
13
|
+
* follow-up WM_CLOSE (Restart Manager sends one) is ignored,
|
|
14
|
+
* and the loop returns into the regular teardown.
|
|
15
|
+
* quit No session end: the app quits on its own, through the loop's
|
|
16
|
+
* exit, which shares the exactly-once shutdown.
|
|
17
|
+
*
|
|
18
|
+
* In every scenario the stop hook raises an event of its own while it
|
|
19
|
+
* runs, and no event may reach the app after its shutdown. The app
|
|
20
|
+
* prints one line per observation and the sender checks them. A
|
|
21
|
+
* watchdog bounds every wait. */
|
|
22
|
+
#include "webview2_host.cpp"
|
|
23
|
+
|
|
24
|
+
#include <cstdarg>
|
|
25
|
+
#include <cstring>
|
|
26
|
+
|
|
27
|
+
namespace {
|
|
28
|
+
|
|
29
|
+
void say(const char *format, ...) {
|
|
30
|
+
va_list args;
|
|
31
|
+
va_start(args, format);
|
|
32
|
+
vprintf(format, args);
|
|
33
|
+
va_end(args);
|
|
34
|
+
putchar('\n');
|
|
35
|
+
fflush(stdout);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ------------------------------------------------------------ the app */
|
|
39
|
+
|
|
40
|
+
Host *app = nullptr;
|
|
41
|
+
bool nested = false;
|
|
42
|
+
bool quitting = false;
|
|
43
|
+
bool shut_down = false;
|
|
44
|
+
|
|
45
|
+
void appEvent(void *, const WindowsEvent *incoming) {
|
|
46
|
+
if (shut_down) {
|
|
47
|
+
say("FAIL event %d reached the app after its shutdown", static_cast<int>(incoming->kind));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
switch (incoming->kind) {
|
|
51
|
+
case kStart: {
|
|
52
|
+
auto window = app->windows.find(1);
|
|
53
|
+
say("ready %llu", static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(window->second.hwnd)));
|
|
54
|
+
if (quitting) native_sdk_windows_stop(app);
|
|
55
|
+
if (!nested) break;
|
|
56
|
+
/* A confirmation whose primary button is the destructive
|
|
57
|
+
* one: cancelling it must not read as that button. */
|
|
58
|
+
WindowsMessageDialogOpts confirm = {};
|
|
59
|
+
confirm.title = "Session End Test";
|
|
60
|
+
confirm.title_len = strlen(confirm.title);
|
|
61
|
+
confirm.message = "Delete everything?";
|
|
62
|
+
confirm.message_len = strlen(confirm.message);
|
|
63
|
+
confirm.primary_button = "Delete";
|
|
64
|
+
confirm.primary_button_len = strlen(confirm.primary_button);
|
|
65
|
+
confirm.secondary_button = "Keep";
|
|
66
|
+
confirm.secondary_button_len = strlen(confirm.secondary_button);
|
|
67
|
+
say("dialog %d", native_sdk_windows_show_message_dialog(app, &confirm));
|
|
68
|
+
say("second dialog %d", native_sdk_windows_show_message_dialog(app, &confirm));
|
|
69
|
+
char path[MAX_PATH] = {};
|
|
70
|
+
WindowsOpenDialogOpts open = {};
|
|
71
|
+
say("open dialog %zu", native_sdk_windows_show_open_dialog(app, &open, path, sizeof(path)).count);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case kShutdown:
|
|
75
|
+
shut_down = true;
|
|
76
|
+
say("shutdown");
|
|
77
|
+
/* Work in the stop hook can raise an event (here a wake the
|
|
78
|
+
* loop window takes synchronously); it must not reach the
|
|
79
|
+
* app, whose shutdown already ran. */
|
|
80
|
+
SendMessageW(app->loop_window.load(), kWakeMessage, 0, 0);
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
int runApp(const char *scenario) {
|
|
88
|
+
nested = strcmp(scenario, "nested") == 0;
|
|
89
|
+
quitting = strcmp(scenario, "quit") == 0;
|
|
90
|
+
app = native_sdk_windows_create(
|
|
91
|
+
"session-end-test", 16, "Session End Test", 16,
|
|
92
|
+
"dev.native-sdk.session-end-test", sizeof("dev.native-sdk.session-end-test") - 1,
|
|
93
|
+
nullptr, 0, "main", 4, 0, 0, 320, 240,
|
|
94
|
+
0, 0, 0, 1, 0, 0, 0, 2, 0);
|
|
95
|
+
if (!app) {
|
|
96
|
+
say("FAIL create host");
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
native_sdk_windows_run(app, appEvent, nullptr);
|
|
100
|
+
say("run returned");
|
|
101
|
+
native_sdk_windows_destroy(app);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* --------------------------------------------------------- the sender */
|
|
106
|
+
|
|
107
|
+
const char *stage = "starting";
|
|
108
|
+
HANDLE child_process = nullptr;
|
|
109
|
+
|
|
110
|
+
void require(bool condition, const char *message) {
|
|
111
|
+
if (condition) return;
|
|
112
|
+
fprintf(stderr, "FAIL (%s): %s\n", stage, message);
|
|
113
|
+
if (child_process) TerminateProcess(child_process, 1);
|
|
114
|
+
ExitProcess(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
struct Child {
|
|
118
|
+
PROCESS_INFORMATION process = {};
|
|
119
|
+
HANDLE output = nullptr;
|
|
120
|
+
std::string lines;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
Child spawn(const char *scenario) {
|
|
124
|
+
SECURITY_ATTRIBUTES inherit = { sizeof(inherit), nullptr, TRUE };
|
|
125
|
+
HANDLE read_end = nullptr;
|
|
126
|
+
HANDLE write_end = nullptr;
|
|
127
|
+
require(CreatePipe(&read_end, &write_end, &inherit, 0) != 0, "output pipe");
|
|
128
|
+
SetHandleInformation(read_end, HANDLE_FLAG_INHERIT, 0);
|
|
129
|
+
wchar_t exe[MAX_PATH] = {};
|
|
130
|
+
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
|
131
|
+
std::wstring command = L"\"" + std::wstring(exe) + L"\" --child " + widen(scenario);
|
|
132
|
+
STARTUPINFOW startup = {};
|
|
133
|
+
startup.cb = sizeof(startup);
|
|
134
|
+
startup.dwFlags = STARTF_USESTDHANDLES;
|
|
135
|
+
startup.hStdOutput = write_end;
|
|
136
|
+
startup.hStdError = write_end;
|
|
137
|
+
Child child;
|
|
138
|
+
require(CreateProcessW(exe, command.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &child.process) != 0, "start the app");
|
|
139
|
+
CloseHandle(write_end);
|
|
140
|
+
child.output = read_end;
|
|
141
|
+
child_process = child.process.hProcess;
|
|
142
|
+
return child;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* The app's stdout is in text mode, so lines arrive ending in CRLF. */
|
|
146
|
+
void appendOutput(Child &child, const char *bytes, DWORD count) {
|
|
147
|
+
for (DWORD index = 0; index < count; ++index) {
|
|
148
|
+
if (bytes[index] != '\r') child.lines.push_back(bytes[index]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Reads until a line starting with `prefix` arrives; returns its rest. */
|
|
153
|
+
std::string waitForLine(Child &child, const char *prefix) {
|
|
154
|
+
const std::string wanted = std::string("\n") + prefix;
|
|
155
|
+
for (;;) {
|
|
156
|
+
const std::string text = "\n" + child.lines;
|
|
157
|
+
const size_t at = text.find(wanted);
|
|
158
|
+
if (at != std::string::npos) {
|
|
159
|
+
const size_t end = text.find('\n', at + 1);
|
|
160
|
+
if (end != std::string::npos) return text.substr(at + wanted.size(), end - at - wanted.size());
|
|
161
|
+
}
|
|
162
|
+
char buffer[512];
|
|
163
|
+
DWORD read = 0;
|
|
164
|
+
require(ReadFile(child.output, buffer, sizeof(buffer), &read, nullptr) && read > 0, "the app's output ended early");
|
|
165
|
+
appendOutput(child, buffer, read);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Waits for the app to exit and collects the rest of its output. */
|
|
170
|
+
DWORD finish(Child &child, DWORD timeout_ms) {
|
|
171
|
+
require(WaitForSingleObject(child.process.hProcess, timeout_ms) == WAIT_OBJECT_0, "the app exited");
|
|
172
|
+
char buffer[512];
|
|
173
|
+
DWORD read = 0;
|
|
174
|
+
while (ReadFile(child.output, buffer, sizeof(buffer), &read, nullptr) && read > 0) appendOutput(child, buffer, read);
|
|
175
|
+
DWORD code = 1;
|
|
176
|
+
GetExitCodeProcess(child.process.hProcess, &code);
|
|
177
|
+
CloseHandle(child.output);
|
|
178
|
+
CloseHandle(child.process.hThread);
|
|
179
|
+
CloseHandle(child.process.hProcess);
|
|
180
|
+
child_process = nullptr;
|
|
181
|
+
return code;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
size_t countLines(const Child &child, const char *line) {
|
|
185
|
+
const std::string text = "\n" + child.lines;
|
|
186
|
+
const std::string wanted = std::string("\n") + line + "\n";
|
|
187
|
+
size_t count = 0;
|
|
188
|
+
for (size_t at = text.find(wanted); at != std::string::npos; at = text.find(wanted, at + 1)) count += 1;
|
|
189
|
+
return count;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
void requireLine(const Child &child, const char *line) {
|
|
193
|
+
if (countLines(child, line) == 1) return;
|
|
194
|
+
fprintf(stderr, "app output:\n%s\n", child.lines.c_str());
|
|
195
|
+
require(false, line);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void requireClean(const Child &child) {
|
|
199
|
+
if (child.lines.find("FAIL") == std::string::npos) return;
|
|
200
|
+
fprintf(stderr, "app output:\n%s\n", child.lines.c_str());
|
|
201
|
+
require(false, "the app reported a failure");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
LRESULT sendToApp(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
205
|
+
DWORD_PTR answer = 0;
|
|
206
|
+
SendMessageTimeoutW(hwnd, message, wparam, lparam, SMTO_ABORTIFHUNG, 10000, &answer);
|
|
207
|
+
return static_cast<LRESULT>(answer);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
HWND appWindow(Child &child) {
|
|
211
|
+
const std::string handle = waitForLine(child, "ready ");
|
|
212
|
+
HWND hwnd = reinterpret_cast<HWND>(static_cast<uintptr_t>(std::stoull(handle)));
|
|
213
|
+
require(IsWindow(hwnd) && !IsWindowVisible(hwnd), "the app's window exists, hidden");
|
|
214
|
+
return hwnd;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
void runFlat() {
|
|
218
|
+
stage = "flat";
|
|
219
|
+
Child child = spawn("flat");
|
|
220
|
+
HWND hwnd = appWindow(child);
|
|
221
|
+
require(sendToApp(hwnd, WM_QUERYENDSESSION, 0, ENDSESSION_LOGOFF) == TRUE, "the query is answered yes");
|
|
222
|
+
sendToApp(hwnd, WM_ENDSESSION, FALSE, ENDSESSION_LOGOFF);
|
|
223
|
+
require(WaitForSingleObject(child.process.hProcess, 300) == WAIT_TIMEOUT, "a cancelled session end leaves the app running");
|
|
224
|
+
require(sendToApp(hwnd, WM_QUERYENDSESSION, 0, ENDSESSION_LOGOFF) == TRUE, "the query is answered yes");
|
|
225
|
+
sendToApp(hwnd, WM_ENDSESSION, TRUE, ENDSESSION_LOGOFF);
|
|
226
|
+
require(finish(child, 2000) == 0, "the app ended with exit code 0");
|
|
227
|
+
requireClean(child);
|
|
228
|
+
requireLine(child, "shutdown");
|
|
229
|
+
require(countLines(child, "run returned") == 0, "the process ended inside WM_ENDSESSION, not through the loop");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
struct DialogSearch {
|
|
233
|
+
DWORD process_id;
|
|
234
|
+
HWND found;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
BOOL CALLBACK findDialog(HWND hwnd, LPARAM lparam) {
|
|
238
|
+
auto *search = reinterpret_cast<DialogSearch *>(lparam);
|
|
239
|
+
DWORD process_id = 0;
|
|
240
|
+
GetWindowThreadProcessId(hwnd, &process_id);
|
|
241
|
+
wchar_t class_name[16] = {};
|
|
242
|
+
GetClassNameW(hwnd, class_name, 16);
|
|
243
|
+
if (process_id != search->process_id || !IsWindowVisible(hwnd) || wcscmp(class_name, L"#32770") != 0) return TRUE;
|
|
244
|
+
search->found = hwnd;
|
|
245
|
+
return FALSE;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
void runNested() {
|
|
249
|
+
stage = "nested";
|
|
250
|
+
Child child = spawn("nested");
|
|
251
|
+
HWND hwnd = appWindow(child);
|
|
252
|
+
DialogSearch search = { child.process.dwProcessId, nullptr };
|
|
253
|
+
for (int attempt = 0; attempt < 200 && !search.found; ++attempt) {
|
|
254
|
+
Sleep(50);
|
|
255
|
+
EnumWindows(findDialog, reinterpret_cast<LPARAM>(&search));
|
|
256
|
+
}
|
|
257
|
+
require(search.found != nullptr, "the app's dialog is open");
|
|
258
|
+
require(sendToApp(hwnd, WM_QUERYENDSESSION, 0, ENDSESSION_CLOSEAPP) == TRUE, "the query is answered yes");
|
|
259
|
+
sendToApp(hwnd, WM_ENDSESSION, TRUE, ENDSESSION_CLOSEAPP);
|
|
260
|
+
sendToApp(hwnd, WM_CLOSE, 0, 0);
|
|
261
|
+
require(finish(child, 10000) == 0, "the app exited with exit code 0");
|
|
262
|
+
requireClean(child);
|
|
263
|
+
requireLine(child, "shutdown");
|
|
264
|
+
requireLine(child, "dialog -1");
|
|
265
|
+
requireLine(child, "second dialog -1");
|
|
266
|
+
requireLine(child, "open dialog 0");
|
|
267
|
+
requireLine(child, "run returned");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void runQuit() {
|
|
271
|
+
stage = "quit";
|
|
272
|
+
Child child = spawn("quit");
|
|
273
|
+
appWindow(child);
|
|
274
|
+
require(finish(child, 10000) == 0, "the app exited with exit code 0");
|
|
275
|
+
requireClean(child);
|
|
276
|
+
requireLine(child, "shutdown");
|
|
277
|
+
requireLine(child, "run returned");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
} // namespace
|
|
281
|
+
|
|
282
|
+
int main(int argc, char **argv) {
|
|
283
|
+
if (argc == 3 && strcmp(argv[1], "--child") == 0) {
|
|
284
|
+
setvbuf(stdout, nullptr, _IONBF, 0);
|
|
285
|
+
return runApp(argv[2]);
|
|
286
|
+
}
|
|
287
|
+
HANDLE finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
|
288
|
+
require(finished != nullptr, "watchdog event");
|
|
289
|
+
std::thread watchdog([finished] {
|
|
290
|
+
if (WaitForSingleObject(finished, 60000) == WAIT_OBJECT_0) return;
|
|
291
|
+
fprintf(stderr, "FAIL: stalled in %s\n", stage);
|
|
292
|
+
if (child_process) TerminateProcess(child_process, 1);
|
|
293
|
+
TerminateProcess(GetCurrentProcess(), 1);
|
|
294
|
+
});
|
|
295
|
+
runFlat();
|
|
296
|
+
runNested();
|
|
297
|
+
runQuit();
|
|
298
|
+
SetEvent(finished);
|
|
299
|
+
watchdog.join();
|
|
300
|
+
CloseHandle(finished);
|
|
301
|
+
puts("PASS: Windows session end");
|
|
302
|
+
return 0;
|
|
303
|
+
}
|