@zogien/native-sdk 0.11.0-fork.3 → 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.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +53 -34
  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/core/references/bridge-security-native-capabilities.md +4 -0
  9. package/skill-data/ts-core/SKILL.md +2 -0
  10. package/src/app_runner/app_assets.zig +85 -1
  11. package/src/app_runner/root.zig +63 -3
  12. package/src/app_runner/ts_core_main.zig +8 -0
  13. package/src/platform/linux/cef_host.cpp +22 -0
  14. package/src/platform/linux/gtk_host.c +317 -34
  15. package/src/platform/linux/gtk_host.h +37 -0
  16. package/src/platform/linux/root.zig +114 -20
  17. package/src/platform/macos/appkit_host.h +53 -0
  18. package/src/platform/macos/appkit_host.m +667 -67
  19. package/src/platform/macos/cef_host.mm +21 -0
  20. package/src/platform/macos/root.zig +146 -29
  21. package/src/platform/null_platform.zig +136 -0
  22. package/src/platform/root.zig +7 -0
  23. package/src/platform/types.zig +90 -0
  24. package/src/platform/windows/dialog_test.cpp +303 -0
  25. package/src/platform/windows/host_loop_test.cpp +169 -0
  26. package/src/platform/windows/root.zig +262 -39
  27. package/src/platform/windows/session_end_test.cpp +303 -0
  28. package/src/platform/windows/webview2_host.cpp +1435 -306
  29. package/src/primitives/app_manifest/root.zig +1 -0
  30. package/src/primitives/app_manifest/tests.zig +35 -0
  31. package/src/primitives/app_manifest/types.zig +12 -0
  32. package/src/primitives/app_manifest/validation.zig +44 -6
  33. package/src/root.zig +8 -0
  34. package/src/runtime/builtin_bridge.zig +108 -13
  35. package/src/runtime/channel_sources.zig +49 -1
  36. package/src/runtime/core.zig +24 -3
  37. package/src/runtime/dialog_tests.zig +367 -0
  38. package/src/runtime/dialogs.zig +385 -0
  39. package/src/runtime/effects.zig +83 -3
  40. package/src/runtime/effects_host_tests.zig +69 -1
  41. package/src/runtime/flow.zig +20 -6
  42. package/src/runtime/flow_reference.zig +20 -6
  43. package/src/runtime/platform_bridge_tests.zig +1 -0
  44. package/src/runtime/root.zig +9 -0
  45. package/src/runtime/single_instance.zig +893 -0
  46. package/src/runtime/system_services.zig +64 -82
  47. package/src/runtime/ui_app.zig +67 -0
  48. package/src/tooling/manifest.zig +111 -3
  49. package/src/tooling/package.zig +98 -8
  50. package/src/tooling/raw_manifest.zig +7 -0
  51. package/src/tooling/root.zig +2 -0
  52. package/src/tooling/templates.zig +44 -2
  53. package/src/tooling/update.zig +228 -10
  54. package/src/tooling/windows_installer.zig +245 -0
  55. package/src/tooling/zip_writer.zig +211 -0
  56. package/src/updater/engine.zig +936 -0
  57. package/src/updater/feed.zig +261 -12
  58. package/src/updater/macos.zig +187 -0
  59. package/src/updater/root.zig +41 -0
  60. package/src/updater/windows.zig +470 -0
  61. package/src/windows_install/config.zig +247 -0
  62. package/src/windows_install/main.zig +690 -0
  63. package/src/windows_install/payload.zig +146 -0
  64. package/src/windows_install/switch.zig +184 -0
  65. package/src/windows_install/tests.zig +11 -0
  66. package/src/windows_install/win32.zig +248 -0
  67. package/tools/native-sdk/main.zig +10 -9
@@ -0,0 +1,303 @@
1
+ /* Asynchronous dialogs against the production Windows host. Each phase
2
+ * opens real dialogs on their own threads and drives them with window
3
+ * messages only, no input injection:
4
+ *
5
+ * owned An open dialog owned by the app window: the request returns
6
+ * at once, the owner is disabled while it shows, app timers
7
+ * keep firing, and a cancel reports cancelled on a wake with
8
+ * the owner enabled again.
9
+ * early Dialogs cancelled in the turn that requested them, before
10
+ * their windows exist, report cancelled.
11
+ * button A task dialog's second button reports selected/secondary.
12
+ * save Accepting a save dialog reports the chosen path.
13
+ * close Destroying a dialog's window cancels the dialog.
14
+ * parentless A dialog without a window leaves the app window enabled.
15
+ * teardown Destroying the host with a dialog open returns at once, and
16
+ * the dialog closes on its own thread.
17
+ *
18
+ * Every dialog reports exactly once. A watchdog names the phase that
19
+ * stalled. */
20
+ #include "webview2_host.cpp"
21
+
22
+ namespace {
23
+
24
+ enum Phase { kPhaseOwned, kPhaseEarly, kPhaseButton, kPhaseSave, kPhaseClose, kPhaseParentless, kPhaseTeardown, kPhaseDone };
25
+ const char *const kPhaseNames[] = {
26
+ "owned: an owned open dialog shows, timers fire, and its cancel reports",
27
+ "early: dialogs cancelled before they show report cancelled",
28
+ "button: a task dialog button reports its answer",
29
+ "save: an accepted save dialog reports its path",
30
+ "close: a closing window cancels its dialog",
31
+ "parentless: a dialog without a window leaves the app window enabled",
32
+ "teardown: the host goes away with a dialog open",
33
+ "done",
34
+ };
35
+ constexpr uint64_t kAppTimerId = 91;
36
+
37
+ std::atomic<int> phase{kPhaseOwned};
38
+ Host *fixture = nullptr;
39
+ int timer_ticks = 0;
40
+ int ticks_when_shown = -1;
41
+ bool cancel_sent = false;
42
+ std::map<uint64_t, WindowsDialogResult> results;
43
+ std::map<uint64_t, std::string> result_paths;
44
+ std::string save_name;
45
+ DWORD teardown_thread = 0;
46
+
47
+ void require(bool condition, const char *message) {
48
+ if (condition) return;
49
+ fprintf(stderr, "FAIL (%s): %s\n", kPhaseNames[phase.load()], message);
50
+ ExitProcess(1);
51
+ }
52
+
53
+ HWND appWindow(uint64_t window_id) {
54
+ auto found = fixture->windows.find(window_id);
55
+ return found == fixture->windows.end() ? nullptr : found->second.hwnd;
56
+ }
57
+
58
+ DWORD dialogThread(uint64_t id) {
59
+ std::lock_guard<std::mutex> lock(fixture->dialog_queue->mutex);
60
+ auto found = fixture->dialogs.find(id);
61
+ return found == fixture->dialogs.end() ? 0 : found->second->thread_id;
62
+ }
63
+
64
+ BOOL CALLBACK findVisibleDialog(HWND hwnd, LPARAM lparam) {
65
+ wchar_t class_name[8] = {};
66
+ if (!IsWindowVisible(hwnd) || GetClassNameW(hwnd, class_name, 8) == 0 || wcscmp(class_name, L"#32770") != 0) return TRUE;
67
+ *reinterpret_cast<HWND *>(lparam) = hwnd;
68
+ return FALSE;
69
+ }
70
+
71
+ BOOL CALLBACK findAnyDialog(HWND hwnd, LPARAM lparam) {
72
+ wchar_t class_name[8] = {};
73
+ if (GetClassNameW(hwnd, class_name, 8) == 0 || wcscmp(class_name, L"#32770") != 0) return TRUE;
74
+ *reinterpret_cast<HWND *>(lparam) = hwnd;
75
+ return FALSE;
76
+ }
77
+
78
+ /* The dialog's window once it is on screen, else null. */
79
+ HWND shownDialog(uint64_t id) {
80
+ const DWORD thread_id = dialogThread(id);
81
+ if (thread_id == 0) return nullptr;
82
+ HWND found = nullptr;
83
+ EnumThreadWindows(thread_id, findVisibleDialog, reinterpret_cast<LPARAM>(&found));
84
+ return found;
85
+ }
86
+
87
+ void requestOpen(uint64_t id, uint64_t parent_window_id) {
88
+ WindowsOpenDialogOpts open = {};
89
+ open.title = "Dialog Test";
90
+ open.title_len = strlen(open.title);
91
+ WindowsDialogRequest request = {};
92
+ request.id = id;
93
+ request.parent_window_id = parent_window_id;
94
+ request.kind = kDialogOpen;
95
+ request.open_opts = &open;
96
+ require(native_sdk_windows_request_dialog(fixture, &request) == 1, "open dialog requested");
97
+ }
98
+
99
+ void requestMessage(uint64_t id, uint64_t parent_window_id) {
100
+ WindowsMessageDialogOpts message = {};
101
+ message.title = "Dialog Test";
102
+ message.title_len = strlen(message.title);
103
+ message.message = "Pick the second button";
104
+ message.message_len = strlen(message.message);
105
+ message.primary_button = "First";
106
+ message.primary_button_len = strlen(message.primary_button);
107
+ message.secondary_button = "Second";
108
+ message.secondary_button_len = strlen(message.secondary_button);
109
+ message.tertiary_button = "Third";
110
+ message.tertiary_button_len = strlen(message.tertiary_button);
111
+ WindowsDialogRequest request = {};
112
+ request.id = id;
113
+ request.parent_window_id = parent_window_id;
114
+ request.kind = kDialogMessage;
115
+ request.message_opts = &message;
116
+ require(native_sdk_windows_request_dialog(fixture, &request) == 1, "message dialog requested");
117
+ }
118
+
119
+ void requestSave(uint64_t id) {
120
+ wchar_t temp[MAX_PATH] = {};
121
+ require(GetTempPathW(MAX_PATH, temp) > 0, "temp directory");
122
+ std::string folder = narrow(std::wstring(temp));
123
+ save_name = "native-sdk-dialog-test-" + std::to_string(GetCurrentProcessId()) + ".txt";
124
+ WindowsSaveDialogOpts save = {};
125
+ save.title = "Dialog Test";
126
+ save.title_len = strlen(save.title);
127
+ save.default_path = folder.data();
128
+ save.default_path_len = folder.size();
129
+ save.default_name = save_name.data();
130
+ save.default_name_len = save_name.size();
131
+ WindowsDialogRequest request = {};
132
+ request.id = id;
133
+ request.parent_window_id = 1;
134
+ request.kind = kDialogSave;
135
+ request.save_opts = &save;
136
+ require(native_sdk_windows_request_dialog(fixture, &request) == 1, "save dialog requested");
137
+ }
138
+
139
+ bool finished(uint64_t id) { return results.count(id) != 0; }
140
+
141
+ void requireResult(uint64_t id, int outcome, const char *message) {
142
+ require(finished(id), message);
143
+ require(results[id].outcome == outcome, message);
144
+ }
145
+
146
+ /* One step of the current phase, every 20 ms. */
147
+ void CALLBACK advance(HWND, UINT, UINT_PTR, DWORD) {
148
+ switch (phase.load()) {
149
+ case kPhaseOwned: {
150
+ if (finished(1)) {
151
+ requireResult(1, kDialogCancelled, "the cancelled dialog reports cancelled");
152
+ require(IsWindowEnabled(appWindow(1)), "the owner is enabled again once the dialog is gone");
153
+ phase.store(kPhaseEarly);
154
+ requestMessage(2, 1);
155
+ native_sdk_windows_cancel_dialog(fixture, 2);
156
+ requestOpen(3, 0);
157
+ native_sdk_windows_cancel_dialog(fixture, 3);
158
+ return;
159
+ }
160
+ if (cancel_sent || !shownDialog(1)) return;
161
+ if (ticks_when_shown < 0) {
162
+ ticks_when_shown = timer_ticks;
163
+ require(!IsWindowEnabled(appWindow(1)), "the owner is disabled while its dialog shows");
164
+ return;
165
+ }
166
+ if (timer_ticks < ticks_when_shown + 3) return;
167
+ cancel_sent = true;
168
+ native_sdk_windows_cancel_dialog(fixture, 1);
169
+ return;
170
+ }
171
+ case kPhaseEarly:
172
+ if (!finished(2) || !finished(3)) return;
173
+ requireResult(2, kDialogCancelled, "a message dialog cancelled before it showed reports cancelled");
174
+ requireResult(3, kDialogCancelled, "an open dialog cancelled before it showed reports cancelled");
175
+ phase.store(kPhaseButton);
176
+ requestMessage(4, 1);
177
+ return;
178
+ case kPhaseButton: {
179
+ if (finished(4)) {
180
+ requireResult(4, kDialogSelected, "a pressed button reports selected");
181
+ require(results[4].button == 1, "the second button");
182
+ phase.store(kPhaseSave);
183
+ requestSave(5);
184
+ return;
185
+ }
186
+ HWND dialog = shownDialog(4);
187
+ if (dialog) PostMessageW(dialog, TDM_CLICK_BUTTON, 101, 0);
188
+ return;
189
+ }
190
+ case kPhaseSave: {
191
+ if (finished(5)) {
192
+ requireResult(5, kDialogSelected, "an accepted save dialog reports selected");
193
+ require(results[5].count == 1, "one path");
194
+ const std::string &path = result_paths[5];
195
+ require(path.size() > save_name.size() && path.compare(path.size() - save_name.size(), save_name.size(), save_name) == 0, "the default name was chosen");
196
+ phase.store(kPhaseClose);
197
+ require(native_sdk_windows_create_window(fixture, 2, "Second", 6, "second", 6, 0, 0, 200, 150, 0, 0, 0, 1, 0, 0, 0, 2, 0) == 1, "second window");
198
+ requestOpen(6, 2);
199
+ return;
200
+ }
201
+ HWND dialog = shownDialog(5);
202
+ if (dialog) PostMessageW(dialog, WM_COMMAND, IDOK, 0);
203
+ return;
204
+ }
205
+ case kPhaseClose: {
206
+ if (finished(6)) {
207
+ requireResult(6, kDialogCancelled, "a closed window's dialog reports cancelled");
208
+ phase.store(kPhaseParentless);
209
+ cancel_sent = false;
210
+ requestOpen(7, 0);
211
+ return;
212
+ }
213
+ HWND second = appWindow(2);
214
+ if (second && shownDialog(6)) DestroyWindow(second);
215
+ return;
216
+ }
217
+ case kPhaseParentless:
218
+ if (finished(7)) {
219
+ requireResult(7, kDialogCancelled, "the parentless dialog reports cancelled");
220
+ phase.store(kPhaseTeardown);
221
+ requestOpen(8, 1);
222
+ return;
223
+ }
224
+ if (cancel_sent || !shownDialog(7)) return;
225
+ require(IsWindowEnabled(appWindow(1)), "a parentless dialog leaves the app window enabled");
226
+ cancel_sent = true;
227
+ native_sdk_windows_cancel_dialog(fixture, 7);
228
+ return;
229
+ case kPhaseTeardown:
230
+ if (teardown_thread != 0 || !shownDialog(8)) return;
231
+ teardown_thread = dialogThread(8);
232
+ native_sdk_windows_stop(fixture);
233
+ return;
234
+ default:
235
+ return;
236
+ }
237
+ }
238
+
239
+ void event(void *, const WindowsEvent *incoming) {
240
+ switch (incoming->kind) {
241
+ case kStart:
242
+ KillTimer(appWindow(1), kFrameTimerId);
243
+ native_sdk_windows_start_timer(fixture, kAppTimerId, 50ull * 1000000ull, 1);
244
+ require(SetTimer(nullptr, 0, 20, advance) != 0, "phase timer");
245
+ requestOpen(1, 1);
246
+ break;
247
+ case kTimer:
248
+ if (incoming->timer_id == kAppTimerId) timer_ticks += 1;
249
+ break;
250
+ case kWake: {
251
+ char buffer[4096];
252
+ WindowsDialogResult result = {};
253
+ while (native_sdk_windows_poll_dialog_result(fixture, buffer, sizeof(buffer), &result) == 1) {
254
+ require(results.count(result.id) == 0, "each dialog reports once");
255
+ results[result.id] = result;
256
+ result_paths[result.id] = std::string(buffer, result.bytes_written);
257
+ }
258
+ break;
259
+ }
260
+ default:
261
+ break;
262
+ }
263
+ }
264
+
265
+ } // namespace
266
+
267
+ int main() {
268
+ HANDLE done = CreateEventW(nullptr, TRUE, FALSE, nullptr);
269
+ require(done != nullptr, "watchdog event");
270
+ std::thread watchdog([done] {
271
+ if (WaitForSingleObject(done, 60000) != WAIT_OBJECT_0) {
272
+ fprintf(stderr, "FAIL: stalled in %s\n", kPhaseNames[phase.load()]);
273
+ TerminateProcess(GetCurrentProcess(), 1);
274
+ }
275
+ });
276
+ fixture = native_sdk_windows_create(
277
+ "dialog-test", 11, "Dialog Test", 11,
278
+ "dev.native-sdk.dialog-test", sizeof("dev.native-sdk.dialog-test") - 1,
279
+ nullptr, 0, "main", 4, 0, 0, 320, 240,
280
+ 0, 0, 0, 1, 0, 0, 0, 2, 0);
281
+ require(fixture != nullptr, "create host");
282
+ native_sdk_windows_run(fixture, event, nullptr);
283
+ require(phase.load() == kPhaseTeardown && teardown_thread != 0, "the loop reached teardown with a dialog open");
284
+
285
+ const ULONGLONG destroy_started = GetTickCount64();
286
+ native_sdk_windows_destroy(fixture);
287
+ require(GetTickCount64() - destroy_started < 1000, "destroying the host does not wait for its dialogs");
288
+ HWND left = nullptr;
289
+ for (int attempt = 0; attempt < 250; ++attempt) {
290
+ left = nullptr;
291
+ EnumThreadWindows(teardown_thread, findAnyDialog, reinterpret_cast<LPARAM>(&left));
292
+ if (!left) break;
293
+ Sleep(20);
294
+ }
295
+ require(left == nullptr, "the open dialog closed after the host went away");
296
+ phase.store(kPhaseDone);
297
+
298
+ SetEvent(done);
299
+ watchdog.join();
300
+ CloseHandle(done);
301
+ puts("PASS: Windows asynchronous dialogs");
302
+ return 0;
303
+ }
@@ -0,0 +1,169 @@
1
+ /* The Windows host's loop model, against a real (hidden) WebView. The
2
+ * production host is compiled in, so this drives the same code apps run:
3
+ *
4
+ * 1. A Bridge command reaches the app outside every WebView2 callback
5
+ * and can join a worker that wakes the loop and requests frames; the
6
+ * wake and the frame arrive once the command returns, and two frame
7
+ * requests made while one is queued produce one frame.
8
+ * 2. After the first app window closes, a wake posted just before it
9
+ * closed still arrives, and an app timer keeps firing.
10
+ *
11
+ * No app callback may run while a WebView2 callback is on the stack. A
12
+ * watchdog names the phase that stalled. */
13
+ #include "webview2_host.cpp"
14
+
15
+ namespace {
16
+
17
+ enum Phase { kPhaseWebView, kPhaseCommand, kPhaseAfterCommand, kPhaseSecondWindow, kPhaseDone };
18
+ const char *const kPhaseNames[] = {
19
+ "waiting for the WebView to load",
20
+ "waiting for the Bridge command",
21
+ "waiting for the wake and frame the command's worker requested",
22
+ "waiting for a wake and app timer after the first window closed",
23
+ "done",
24
+ };
25
+ constexpr uint64_t kAppTimerId = 77;
26
+
27
+ std::atomic<int> phase{kPhaseWebView};
28
+ Host *fixture = nullptr;
29
+ UINT_PTR poll_timer = 0;
30
+ int wakes = 0;
31
+ int frames = 0;
32
+ int timer_ticks = 0;
33
+ int wakes_before_close = 0;
34
+
35
+ void require(bool condition, const char *message) {
36
+ if (condition) return;
37
+ fprintf(stderr, "FAIL (%s): %s\n", kPhaseNames[phase.load()], message);
38
+ ExitProcess(1);
39
+ }
40
+
41
+ void requireOutsideWebViewCallbacks() {
42
+ require(fixture->webview_callback_depth == 0, "an app callback ran inside a WebView2 callback");
43
+ }
44
+
45
+ /* The per-window 16 ms frame pump would bury the frames the test counts,
46
+ * so the test windows run without it. */
47
+ void stopFramePump(uint64_t window_id) {
48
+ auto found = fixture->windows.find(window_id);
49
+ require(found != fixture->windows.end() && found->second.hwnd, "window exists");
50
+ KillTimer(found->second.hwnd, kFrameTimerId);
51
+ }
52
+
53
+ void wakeFromWorker(int expected_frames_requested) {
54
+ std::thread worker([expected_frames_requested] {
55
+ require(native_sdk_windows_wake(fixture) == 1, "wake posted");
56
+ for (int index = 0; index < expected_frames_requested; ++index) {
57
+ require(native_sdk_windows_request_frame(fixture) == 1, "frame request accepted");
58
+ }
59
+ });
60
+ worker.join();
61
+ }
62
+
63
+ void CALLBACK closeFirstWindow(HWND, UINT, UINT_PTR timer, DWORD) {
64
+ KillTimer(nullptr, timer);
65
+ require(native_sdk_windows_create_window(fixture, 2, "Second", 6, "second", 6, 0, 0, 200, 150, 0, 0, 0, 1, 0, 0, 0, 2, 0) == 1, "second window");
66
+ stopFramePump(2);
67
+ native_sdk_windows_start_timer(fixture, kAppTimerId, 50ull * 1000000ull, 1);
68
+ wakes_before_close = wakes;
69
+ phase.store(kPhaseSecondWindow);
70
+ /* Posted while window 1 is still the first app window, then window 1
71
+ * goes away before the loop takes the wake. */
72
+ wakeFromWorker(0);
73
+ auto first = fixture->windows.find(1);
74
+ require(first != fixture->windows.end() && first->second.hwnd, "first window open");
75
+ DestroyWindow(first->second.hwnd);
76
+ }
77
+
78
+ void CALLBACK checkCommandResults(HWND, UINT, UINT_PTR timer, DWORD) {
79
+ if (wakes < 1 || frames < 1) return;
80
+ KillTimer(nullptr, timer);
81
+ require(wakes == 1, "exactly one wake after the command");
82
+ require(frames == 1, "the two frame requests coalesced into one frame");
83
+ SetTimer(nullptr, 0, 10, closeFirstWindow);
84
+ }
85
+
86
+ void CALLBACK postCommand(HWND, UINT, UINT_PTR timer, DWORD) {
87
+ auto found = fixture->webviews.find(webViewKey(1, "main"));
88
+ if (found == fixture->webviews.end() || !found->second.webview) return;
89
+ KillTimer(nullptr, timer);
90
+ phase.store(kPhaseCommand);
91
+ require(SUCCEEDED(found->second.webview->ExecuteScript(
92
+ L"chrome.webview.postMessage('join-worker')", nullptr)), "post command");
93
+ }
94
+
95
+ void bridge(void *, uint64_t window_id,
96
+ const char *label, size_t label_len,
97
+ const char *message, size_t message_len,
98
+ const char *origin, size_t origin_len) {
99
+ requireOutsideWebViewCallbacks();
100
+ require(phase.load() == kPhaseCommand, "one Bridge command");
101
+ require(window_id == 1, "window identity");
102
+ require(std::string(label, label_len) == "main", "view identity");
103
+ require(std::string(message, message_len) == "join-worker", "message");
104
+ require(std::string(origin, origin_len) == "zero://inline", "origin");
105
+ wakeFromWorker(2);
106
+ require(wakes == 0 && frames == 0, "wake and frame wait for the command to return");
107
+ phase.store(kPhaseAfterCommand);
108
+ SetTimer(nullptr, 0, 20, checkCommandResults);
109
+ }
110
+
111
+ void event(void *, const WindowsEvent *incoming) {
112
+ requireOutsideWebViewCallbacks();
113
+ switch (incoming->kind) {
114
+ case kStart: {
115
+ stopFramePump(1);
116
+ const char html[] = "<!doctype html><title>Host Loop Test</title>";
117
+ require(native_sdk_windows_load_window_webview(
118
+ fixture, 1, html, sizeof(html) - 1, 0,
119
+ nullptr, 0, nullptr, 0, nullptr, 0, 0) == 1, "load WebView");
120
+ poll_timer = SetTimer(nullptr, 0, 50, postCommand);
121
+ require(poll_timer != 0, "command timer");
122
+ break;
123
+ }
124
+ case kWake:
125
+ wakes += 1;
126
+ break;
127
+ case kFrame:
128
+ frames += 1;
129
+ break;
130
+ case kTimer:
131
+ if (incoming->timer_id == kAppTimerId) timer_ticks += 1;
132
+ break;
133
+ default:
134
+ break;
135
+ }
136
+ if (phase.load() == kPhaseSecondWindow && wakes > wakes_before_close && timer_ticks >= 2) {
137
+ require(fixture->windows.find(1)->second.hwnd == nullptr, "first window closed");
138
+ phase.store(kPhaseDone);
139
+ native_sdk_windows_stop(fixture);
140
+ }
141
+ }
142
+
143
+ } // namespace
144
+
145
+ int main() {
146
+ HANDLE finished = CreateEventW(nullptr, TRUE, FALSE, nullptr);
147
+ require(finished != nullptr, "watchdog event");
148
+ std::thread watchdog([finished] {
149
+ if (WaitForSingleObject(finished, 30000) != WAIT_OBJECT_0) {
150
+ fprintf(stderr, "FAIL: stalled %s\n", kPhaseNames[phase.load()]);
151
+ TerminateProcess(GetCurrentProcess(), 1);
152
+ }
153
+ });
154
+ fixture = native_sdk_windows_create(
155
+ "host-loop-test", 14, "Host Loop Test", 14,
156
+ "dev.native-sdk.host-loop-test", sizeof("dev.native-sdk.host-loop-test") - 1,
157
+ nullptr, 0, "main", 4, 0, 0, 320, 240,
158
+ 0, 0, 0, 1, 0, 0, 0, 2, 0);
159
+ require(fixture != nullptr, "create host");
160
+ native_sdk_windows_set_bridge_callback(fixture, bridge, nullptr);
161
+ native_sdk_windows_run(fixture, event, nullptr);
162
+ require(phase.load() == kPhaseDone, "the loop finished every phase");
163
+ native_sdk_windows_destroy(fixture);
164
+ SetEvent(finished);
165
+ watchdog.join();
166
+ CloseHandle(finished);
167
+ puts("PASS: Windows host loop model");
168
+ return 0;
169
+ }