@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.
- package/README.md +1 -1
- package/build/app.zig +53 -34
- package/build/cli.zig +22 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -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 +53 -0
- package/src/platform/macos/appkit_host.m +667 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +146 -29
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +7 -0
- package/src/platform/types.zig +90 -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 +262 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1435 -306
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +8 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/channel_sources.zig +49 -1
- 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 +83 -3
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +20 -6
- package/src/runtime/flow_reference.zig +20 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +9 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +44 -2
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
|
@@ -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
|
+
}
|