@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
|
@@ -83,6 +83,7 @@ pub const FileAssociation = types.FileAssociation;
|
|
|
83
83
|
pub const UrlScheme = types.UrlScheme;
|
|
84
84
|
pub const PackageMetadata = types.PackageMetadata;
|
|
85
85
|
pub const UpdateConfig = types.UpdateConfig;
|
|
86
|
+
pub const UpdateMode = types.UpdateMode;
|
|
86
87
|
pub const Manifest = types.Manifest;
|
|
87
88
|
pub const ImagesConfig = types.ImagesConfig;
|
|
88
89
|
pub const validateManifest = validation.validateManifest;
|
|
@@ -125,6 +125,41 @@ test "updates require paired HTTPS feed and Ed25519 public key" {
|
|
|
125
125
|
}));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
test "app-driven updates need a feed to drive" {
|
|
129
|
+
try validateUpdates(.{
|
|
130
|
+
.feed_url = "https://example.com/native-update.json",
|
|
131
|
+
.public_key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
|
|
132
|
+
.mode = .app,
|
|
133
|
+
});
|
|
134
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .mode = .app }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
test "update feed URLs may name the target and installed version" {
|
|
138
|
+
const key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
|
|
139
|
+
try validateUpdates(.{ .feed_url = "https://example.com/releases/{target}/native-update.json", .public_key = key });
|
|
140
|
+
try validateUpdates(.{ .feed_url = "https://example.com/feed?target={target}&from={current_version}", .public_key = key });
|
|
141
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/{arch}/native-update.json", .public_key = key }));
|
|
142
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/{target/native-update.json", .public_key = key }));
|
|
143
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/target}/native-update.json", .public_key = key }));
|
|
144
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://{target}.example.com/native-update.json", .public_key = key }));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test "update public_keys add distinct trusted keys beside public_key" {
|
|
148
|
+
const key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
|
|
149
|
+
const other = "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=";
|
|
150
|
+
const rotated = [_][]const u8{other};
|
|
151
|
+
try validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &rotated });
|
|
152
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .public_keys = &rotated }));
|
|
153
|
+
const repeated = [_][]const u8{key};
|
|
154
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &repeated }));
|
|
155
|
+
const duplicated = [_][]const u8{ other, other };
|
|
156
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &duplicated }));
|
|
157
|
+
const malformed = [_][]const u8{"not-a-public-key"};
|
|
158
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &malformed }));
|
|
159
|
+
const too_many = [_][]const u8{ other, "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=", "+AzM3OSuHAeuIIoq35mjEK5CB+Awb6AjYRCwaCe7uNA=", "n0lDnp2wlbxBEe0l01eV2DG8VaBH9LHX9q7jd3u0EiA=" };
|
|
160
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &too_many }));
|
|
161
|
+
}
|
|
162
|
+
|
|
128
163
|
test "accessory startup requires the tray capability" {
|
|
129
164
|
const tray_capabilities = [_]Capability{.tray};
|
|
130
165
|
try validateManifest(.{
|
|
@@ -647,9 +647,21 @@ pub const PackageMetadata = struct {
|
|
|
647
647
|
pub const UpdateConfig = struct {
|
|
648
648
|
feed_url: ?[]const u8 = null,
|
|
649
649
|
public_key: ?[]const u8 = null,
|
|
650
|
+
/// Further trusted keys beside `public_key`, so a release signed with
|
|
651
|
+
/// a key being rotated out still verifies on installs that know it.
|
|
652
|
+
public_keys: []const []const u8 = &.{},
|
|
650
653
|
check_on_start: bool = false,
|
|
654
|
+
/// Who drives updates on macOS: the host's own prompts, or the app
|
|
655
|
+
/// through the `native-sdk.updates` requests and channel. Windows apps
|
|
656
|
+
/// always drive updates themselves.
|
|
657
|
+
mode: UpdateMode = .host,
|
|
651
658
|
};
|
|
652
659
|
|
|
660
|
+
pub const UpdateMode = enum { host, app };
|
|
661
|
+
|
|
662
|
+
/// `public_key` plus at most this many `public_keys`.
|
|
663
|
+
pub const max_update_public_keys: usize = 3;
|
|
664
|
+
|
|
653
665
|
pub const PersistRestore = struct {
|
|
654
666
|
ok: []const u8,
|
|
655
667
|
none: []const u8,
|
|
@@ -657,17 +657,55 @@ pub fn validateSecurity(security: SecurityConfig) ValidationError!void {
|
|
|
657
657
|
pub fn validateUpdates(updates: UpdateConfig) ValidationError!void {
|
|
658
658
|
if ((updates.feed_url == null) != (updates.public_key == null)) return error.MissingRequiredField;
|
|
659
659
|
if (updates.check_on_start and updates.feed_url == null) return error.MissingRequiredField;
|
|
660
|
+
if (updates.mode == .app and updates.feed_url == null) return error.MissingRequiredField;
|
|
661
|
+
if (updates.public_keys.len > 0 and updates.public_key == null) return error.MissingRequiredField;
|
|
662
|
+
if (updates.public_keys.len > types.max_update_public_keys) return error.MissingRequiredField;
|
|
660
663
|
if (updates.feed_url) |url| {
|
|
661
664
|
if (url.len > 4096) return error.InvalidUrl;
|
|
662
665
|
if (!std.mem.startsWith(u8, url, "https://")) return error.InvalidUrl;
|
|
663
666
|
try validateUrl(url);
|
|
667
|
+
try validateFeedUrlPlaceholders(url);
|
|
668
|
+
}
|
|
669
|
+
if (updates.public_key) |key| try validateUpdatePublicKey(key);
|
|
670
|
+
for (updates.public_keys, 0..) |key, index| {
|
|
671
|
+
try validateUpdatePublicKey(key);
|
|
672
|
+
if (std.mem.eql(u8, key, updates.public_key.?)) return error.MissingRequiredField;
|
|
673
|
+
for (updates.public_keys[0..index]) |previous| {
|
|
674
|
+
if (std.mem.eql(u8, key, previous)) return error.MissingRequiredField;
|
|
675
|
+
}
|
|
664
676
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
fn validateUpdatePublicKey(key: []const u8) ValidationError!void {
|
|
680
|
+
var decoded: [std.crypto.sign.Ed25519.PublicKey.encoded_length]u8 = undefined;
|
|
681
|
+
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(key) catch return error.MissingRequiredField;
|
|
682
|
+
if (decoded_len != decoded.len) return error.MissingRequiredField;
|
|
683
|
+
std.base64.standard.Decoder.decode(&decoded, key) catch return error.MissingRequiredField;
|
|
684
|
+
_ = std.crypto.sign.Ed25519.PublicKey.fromBytes(decoded) catch return error.MissingRequiredField;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// A feed_url may name `{target}` and `{current_version}`, which each
|
|
688
|
+
/// installed app expands for itself, in its path or query only.
|
|
689
|
+
fn validateFeedUrlPlaceholders(url: []const u8) ValidationError!void {
|
|
690
|
+
const authority_start = "https://".len;
|
|
691
|
+
const authority_end = std.mem.findAnyPos(u8, url, authority_start, "/?#") orelse url.len;
|
|
692
|
+
if (std.mem.findAny(u8, url[0..authority_end], "{}") != null) return error.InvalidUrl;
|
|
693
|
+
var index = authority_end;
|
|
694
|
+
while (index < url.len) : (index += 1) {
|
|
695
|
+
switch (url[index]) {
|
|
696
|
+
'{' => {
|
|
697
|
+
const rest = url[index..];
|
|
698
|
+
const placeholder = if (std.mem.startsWith(u8, rest, "{target}"))
|
|
699
|
+
"{target}"
|
|
700
|
+
else if (std.mem.startsWith(u8, rest, "{current_version}"))
|
|
701
|
+
"{current_version}"
|
|
702
|
+
else
|
|
703
|
+
return error.InvalidUrl;
|
|
704
|
+
index += placeholder.len - 1;
|
|
705
|
+
},
|
|
706
|
+
'}' => return error.InvalidUrl,
|
|
707
|
+
else => {},
|
|
708
|
+
}
|
|
671
709
|
}
|
|
672
710
|
}
|
|
673
711
|
|
package/src/root.zig
CHANGED
|
@@ -157,6 +157,7 @@ pub const EffectChannelSourceState = runtime.EffectChannelSourceState;
|
|
|
157
157
|
pub const EffectChannelSourceResult = runtime.EffectChannelSourceResult;
|
|
158
158
|
pub const max_channel_source_payload = runtime.max_channel_source_payload;
|
|
159
159
|
pub const ChannelHandle = runtime.ChannelHandle;
|
|
160
|
+
pub const single_instance = runtime.single_instance;
|
|
160
161
|
pub const max_effect_channels = runtime.max_effect_channels;
|
|
161
162
|
pub const max_effect_channel_bytes = runtime.max_effect_channel_bytes;
|
|
162
163
|
pub const max_effect_channel_pending = runtime.max_effect_channel_pending;
|
|
@@ -269,6 +270,13 @@ pub const SaveDialogOptions = platform.SaveDialogOptions;
|
|
|
269
270
|
pub const MessageDialogStyle = platform.MessageDialogStyle;
|
|
270
271
|
pub const MessageDialogResult = platform.MessageDialogResult;
|
|
271
272
|
pub const MessageDialogOptions = platform.MessageDialogOptions;
|
|
273
|
+
pub const DialogRequestId = platform.DialogRequestId;
|
|
274
|
+
pub const DialogKind = platform.DialogKind;
|
|
275
|
+
pub const DialogOptions = platform.DialogOptions;
|
|
276
|
+
pub const DialogOutcome = platform.DialogOutcome;
|
|
277
|
+
pub const DialogResult = platform.DialogResult;
|
|
278
|
+
pub const DialogCompletion = runtime.DialogCompletion;
|
|
279
|
+
pub const DialogRequestOptions = runtime.DialogRequestOptions;
|
|
272
280
|
pub const NotificationOptions = platform.NotificationOptions;
|
|
273
281
|
pub const StatusItemId = platform.StatusItemId;
|
|
274
282
|
pub const primary_status_item_id = platform.primary_status_item_id;
|
|
@@ -52,6 +52,7 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
|
|
|
52
52
|
const App = runtime_api.App(Runtime);
|
|
53
53
|
const SystemServiceMethods = runtime_system_services.RuntimeSystemServices(Runtime);
|
|
54
54
|
const WindowViewMethods = runtime_window_views.RuntimeWindowViews(Runtime);
|
|
55
|
+
const AsyncBridgeResponseSlot = @import("async_bridge.zig").AsyncBridgeResponseSlot(Runtime);
|
|
55
56
|
|
|
56
57
|
return struct {
|
|
57
58
|
const Self = @This();
|
|
@@ -125,15 +126,106 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
|
|
|
125
126
|
return bridge.writeSuccessResponse(response_buffer, request.id, result);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
fn dispatchCompiledDialogBridgeCommand(self: *Runtime, request: bridge.Request, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
129
|
+
fn dispatchCompiledDialogBridgeCommand(self: *Runtime, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
|
|
129
130
|
const plan = builtin_coordination.route(self.builtin_coordination_policy.?, .dialog, request.command);
|
|
130
|
-
const
|
|
131
|
-
.dialog_openFile =>
|
|
132
|
-
.dialog_saveFile =>
|
|
133
|
-
.dialog_showMessage =>
|
|
131
|
+
const kind: platform.DialogKind = switch (plan.verb()) {
|
|
132
|
+
.dialog_openFile => .open,
|
|
133
|
+
.dialog_saveFile => .save,
|
|
134
|
+
.dialog_showMessage => .message,
|
|
134
135
|
else => return bridge.writeErrorResponse(response_buffer, request.id, .unknown_command, plan.unknownMessage()),
|
|
135
136
|
};
|
|
136
|
-
return
|
|
137
|
+
return Self.startDialogBridgeCommand(self, kind, request, message, result_buffer, response_buffer);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Queue the dialog and answer the bridge request when it
|
|
141
|
+
/// finishes. Null means the response is deferred; a returned
|
|
142
|
+
/// response is an immediate refusal, or, on a platform without
|
|
143
|
+
/// asynchronous dialogs, the blocking dialog's answer.
|
|
144
|
+
fn startDialogBridgeCommand(self: *Runtime, kind: platform.DialogKind, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
|
|
145
|
+
var storage = json.StringStorage.init(result_buffer);
|
|
146
|
+
const options = SystemServiceMethods.dialogOptionsFromJson(kind, request.payload, &storage) catch |err| return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
|
|
147
|
+
if (!self.options.platform.services.supportsAsyncDialogs()) return Self.answerDialogBridgeCommand(self, kind, request.id, options, result_buffer, response_buffer);
|
|
148
|
+
const slot =Self.reserveDialogBridgeResponse(self, message) catch |err| return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
|
|
149
|
+
var tag: [1 + bridge.max_id_bytes]u8 = undefined;
|
|
150
|
+
tag[0] = @intFromEnum(kind);
|
|
151
|
+
@memcpy(tag[1..][0..request.id.len], request.id);
|
|
152
|
+
_ = self.requestDialog(.{
|
|
153
|
+
.options = options,
|
|
154
|
+
.parent_window_id = Self.openWindowId(self, message.window_id),
|
|
155
|
+
}, .{
|
|
156
|
+
.context = slot,
|
|
157
|
+
.tag = tag[0 .. 1 + request.id.len],
|
|
158
|
+
.on_result = Self.finishDialogBridgeCommand,
|
|
159
|
+
}) catch |err| {
|
|
160
|
+
slot.release();
|
|
161
|
+
return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
|
|
162
|
+
};
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn finishDialogBridgeCommand(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
|
|
167
|
+
_ = runtime;
|
|
168
|
+
const slot: *AsyncBridgeResponseSlot = @ptrCast(@alignCast(context));
|
|
169
|
+
const kind: platform.DialogKind = @enumFromInt(tag[0]);
|
|
170
|
+
const id = tag[1..];
|
|
171
|
+
var result_buffer: [bridge.max_result_bytes]u8 = undefined;
|
|
172
|
+
var response_buffer: [bridge.max_response_bytes]u8 = undefined;
|
|
173
|
+
const response = if (result.outcome == .failed)
|
|
174
|
+
bridge.writeErrorResponse(&response_buffer, id, .internal_error, "Dialog could not be shown")
|
|
175
|
+
else if (SystemServiceMethods.writeDialogResultJson(kind, result, &result_buffer)) |value|
|
|
176
|
+
bridge.writeSuccessResponse(&response_buffer, id, value)
|
|
177
|
+
else |err|
|
|
178
|
+
bridge.writeErrorResponse(&response_buffer, id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err));
|
|
179
|
+
try slot.respond(response);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Platforms without asynchronous dialogs (Chromium builds, the
|
|
183
|
+
/// null platform) answer from the blocking call, at once, as they
|
|
184
|
+
/// always have. `options` borrows `result_buffer`, which is free
|
|
185
|
+
/// again once the dialog has returned.
|
|
186
|
+
fn answerDialogBridgeCommand(self: *Runtime, kind: platform.DialogKind, id: []const u8, options: platform.DialogOptions, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
187
|
+
var paths: [platform.max_dialog_paths_bytes]u8 = undefined;
|
|
188
|
+
const result: platform.DialogResult = switch (options) {
|
|
189
|
+
.open => |open| opened: {
|
|
190
|
+
const opened = self.showOpenDialog(open, &paths) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
|
|
191
|
+
break :opened .{ .id = 0, .outcome = if (opened.count == 0) .cancelled else .selected, .paths = opened.paths, .count = opened.count };
|
|
192
|
+
},
|
|
193
|
+
.save => |save| saved: {
|
|
194
|
+
const saved = self.showSaveDialog(save, paths[0..platform.max_dialog_path_bytes]) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
|
|
195
|
+
const path = saved orelse break :saved .{ .id = 0, .outcome = .cancelled };
|
|
196
|
+
break :saved .{ .id = 0, .outcome = .selected, .paths = path, .count = 1 };
|
|
197
|
+
},
|
|
198
|
+
.message => |dialog| .{
|
|
199
|
+
.id = 0,
|
|
200
|
+
.outcome = .selected,
|
|
201
|
+
.button = self.showMessageDialog(dialog) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const value = SystemServiceMethods.writeDialogResultJson(kind, result, result_buffer) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
|
|
205
|
+
return bridge.writeSuccessResponse(response_buffer, id, value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn writeDialogBridgeError(self: *Runtime, id: []const u8, err: anyerror, output: []u8) []const u8 {
|
|
209
|
+
if (self.builtin_coordination_policy != null) return Self.writeCompiledBuiltinError(self, id, err, output);
|
|
210
|
+
return bridge.writeErrorResponse(output, id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn reserveDialogBridgeResponse(self: *Runtime, message: platform.BridgeMessage) !*AsyncBridgeResponseSlot {
|
|
214
|
+
for (&self.async_bridge_responses) |*slot| {
|
|
215
|
+
if (slot.in_use) continue;
|
|
216
|
+
try slot.init(self, .{ .origin = message.origin, .window_id = message.window_id, .webview_label = message.webview_label });
|
|
217
|
+
return slot;
|
|
218
|
+
}
|
|
219
|
+
return error.AsyncBridgeResponseLimitReached;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// The requesting window when the runtime knows it as open;
|
|
223
|
+
/// otherwise null lets the broker pick a parent.
|
|
224
|
+
fn openWindowId(self: *Runtime, window_id: platform.WindowId) ?platform.WindowId {
|
|
225
|
+
for (self.windows[0..self.window_count]) |window| {
|
|
226
|
+
if (window.info.id == window_id and window.info.open) return window_id;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
137
229
|
}
|
|
138
230
|
|
|
139
231
|
fn dispatchCompiledOsBridgeCommand(self: *Runtime, request: bridge.Request, origin: []const u8, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
@@ -306,17 +398,20 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
|
|
|
306
398
|
return bridge.writeSuccessResponse(response_buffer, request.id, result);
|
|
307
399
|
}
|
|
308
400
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
401
|
+
/// Dialog commands answer asynchronously: null means the response
|
|
402
|
+
/// will follow when the dialog finishes (see
|
|
403
|
+
/// `startDialogBridgeCommand`).
|
|
404
|
+
pub fn dispatchDialogBridgeCommand(self: *Runtime, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
|
|
405
|
+
if (self.builtin_coordination_policy != null) return Self.dispatchCompiledDialogBridgeCommand(self, request, message, result_buffer, response_buffer);
|
|
406
|
+
const kind: platform.DialogKind = if (std.mem.eql(u8, request.command, "native-sdk.dialog.openFile"))
|
|
407
|
+
.open
|
|
313
408
|
else if (std.mem.eql(u8, request.command, "native-sdk.dialog.saveFile"))
|
|
314
|
-
|
|
409
|
+
.save
|
|
315
410
|
else if (std.mem.eql(u8, request.command, "native-sdk.dialog.showMessage"))
|
|
316
|
-
|
|
411
|
+
.message
|
|
317
412
|
else
|
|
318
413
|
return bridge.writeErrorResponse(response_buffer, request.id, .unknown_command, "Unknown dialog command");
|
|
319
|
-
return
|
|
414
|
+
return Self.startDialogBridgeCommand(self, kind, request, message, result_buffer, response_buffer);
|
|
320
415
|
}
|
|
321
416
|
|
|
322
417
|
pub fn dispatchOsBridgeCommand(self: *Runtime, request: bridge.Request, origin: []const u8, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
@@ -1,19 +1,67 @@
|
|
|
1
1
|
//! Explicit native external-producer capabilities. Portable app state and
|
|
2
|
-
//! channel lifecycle stay in the app;
|
|
2
|
+
//! channel lifecycle stay in the app; `native-sdk.process.samples` owns a
|
|
3
|
+
//! detached OS sampler, `native-sdk.app.launches` hands the channel to
|
|
4
|
+
//! the single-instance listener (single_instance.zig), and
|
|
5
|
+
//! `native-sdk.updates` receives the updater's status records
|
|
6
|
+
//! (updater/engine.zig).
|
|
3
7
|
const std = @import("std");
|
|
4
8
|
const builtin = @import("builtin");
|
|
5
9
|
const effects = @import("effects.zig");
|
|
6
10
|
const clock = @import("clock.zig");
|
|
11
|
+
const single_instance = @import("single_instance.zig");
|
|
12
|
+
const update_engine = @import("../updater/engine.zig");
|
|
7
13
|
const sample_interval_ms = 500;
|
|
8
14
|
const max_line_bytes = 96;
|
|
9
15
|
|
|
10
16
|
pub fn start(name: []const u8, payload: []const u8, handle: effects.ChannelHandle) !void {
|
|
17
|
+
if (std.mem.eql(u8, name, single_instance.source_name)) return single_instance.attach(payload, handle);
|
|
18
|
+
if (std.mem.eql(u8, name, update_engine.channel_source)) return attachUpdates(payload, handle);
|
|
11
19
|
if (!std.mem.eql(u8, name, "native-sdk.process.samples")) return error.UnknownChannelSource;
|
|
12
20
|
if (payload.len != 0) return error.InvalidChannelSourceArguments;
|
|
13
21
|
if (comptime builtin.single_threaded) return error.UnsupportedChannelSource;
|
|
14
22
|
try startSamplerThread(handle);
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
/// The open `native-sdk.updates` channels, each the engine's sink. A slot
|
|
26
|
+
/// frees itself when its channel answers `.closed`.
|
|
27
|
+
const UpdateChannel = struct {
|
|
28
|
+
handle: effects.ChannelHandle = undefined,
|
|
29
|
+
used: std.atomic.Value(bool) = .init(false),
|
|
30
|
+
|
|
31
|
+
fn post(context: *anyopaque, bytes: []const u8) update_engine.Posted {
|
|
32
|
+
const self: *UpdateChannel = @ptrCast(@alignCast(context));
|
|
33
|
+
return switch (self.handle.post(bytes)) {
|
|
34
|
+
.accepted => .delivered,
|
|
35
|
+
.dropped_full, .dropped_oversized => .dropped,
|
|
36
|
+
.closed => {
|
|
37
|
+
self.used.store(false, .release);
|
|
38
|
+
return .closed;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var update_channels: [4]UpdateChannel = .{UpdateChannel{}} ** 4;
|
|
44
|
+
|
|
45
|
+
comptime {
|
|
46
|
+
// Every status record fits one post; notes travel by request.
|
|
47
|
+
std.debug.assert(update_engine.max_status_bytes <= effects.max_effect_channel_bytes);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn attachUpdates(payload: []const u8, handle: effects.ChannelHandle) !void {
|
|
51
|
+
if (payload.len != 0) return error.InvalidChannelSourceArguments;
|
|
52
|
+
for (&update_channels) |*channel| {
|
|
53
|
+
if (channel.used.cmpxchgStrong(false, true, .acq_rel, .acquire) == null) {
|
|
54
|
+
channel.handle = handle;
|
|
55
|
+
update_engine.attach(.{ .context = channel, .post = UpdateChannel.post }) catch {
|
|
56
|
+
channel.used.store(false, .release);
|
|
57
|
+
return error.UnsupportedChannelSource;
|
|
58
|
+
};
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return error.UnsupportedChannelSource;
|
|
63
|
+
}
|
|
64
|
+
|
|
17
65
|
fn startSamplerThread(handle: effects.ChannelHandle) std.Thread.SpawnError!void {
|
|
18
66
|
const thread = try std.Thread.spawn(.{}, samplerMain, .{handle});
|
|
19
67
|
thread.detach();
|
package/src/runtime/core.zig
CHANGED
|
@@ -30,6 +30,7 @@ const runtime_session_state = @import("session_state.zig");
|
|
|
30
30
|
const session_journal = @import("session_journal.zig");
|
|
31
31
|
const runtime_state = @import("state.zig");
|
|
32
32
|
const runtime_system_services = @import("system_services.zig");
|
|
33
|
+
const runtime_dialogs = @import("dialogs.zig");
|
|
33
34
|
const runtime_builtin_bridge = @import("builtin_bridge.zig");
|
|
34
35
|
const runtime_view = @import("view.zig");
|
|
35
36
|
const runtime_window_views = @import("window_views.zig");
|
|
@@ -110,6 +111,10 @@ const FocusTraversalDirection = runtime_state.FocusTraversalDirection;
|
|
|
110
111
|
const copySourceInto = runtime_state.copySourceInto;
|
|
111
112
|
const sourceWebViewUrl = runtime_state.sourceWebViewUrl;
|
|
112
113
|
const AsyncBridgeResponseSlot = runtime_async_bridge.AsyncBridgeResponseSlot(Runtime);
|
|
114
|
+
pub const DialogCompletion = runtime_dialogs.Completion(Runtime);
|
|
115
|
+
pub const DialogRequestOptions = runtime_dialogs.DialogRequestOptions;
|
|
116
|
+
pub const max_dialog_requests = runtime_dialogs.max_dialog_requests;
|
|
117
|
+
pub const max_dialog_tag_bytes = runtime_dialogs.max_dialog_tag_bytes;
|
|
113
118
|
const platformCursorFromCanvas = widget_bridge.platformCursorFromCanvas;
|
|
114
119
|
const widgetRoleName = widget_bridge.widgetRoleName;
|
|
115
120
|
const platformWidgetAccessibilityRole = widget_bridge.platformWidgetAccessibilityRole;
|
|
@@ -351,6 +356,8 @@ pub const Runtime = struct {
|
|
|
351
356
|
/// copy bound, so `initAt` assigns it explicitly.
|
|
352
357
|
frame_profile: runtime_frame_profile.FrameProfile = .{},
|
|
353
358
|
async_bridge_responses: [max_async_bridge_responses]AsyncBridgeResponseSlot = [_]AsyncBridgeResponseSlot{.{}} ** max_async_bridge_responses,
|
|
359
|
+
/// Asynchronous dialogs in flight (see `dialogs.zig`).
|
|
360
|
+
dialog_broker: runtime_dialogs.Broker(Runtime) = .{},
|
|
354
361
|
automation_windows: [automation.snapshot.max_windows]automation.snapshot.Window = undefined,
|
|
355
362
|
/// Snapshot-side storage for the frame profile's per-stage stats
|
|
356
363
|
/// (the snapshot Input only references runtime-owned memory).
|
|
@@ -562,6 +569,9 @@ pub const Runtime = struct {
|
|
|
562
569
|
item.title = "";
|
|
563
570
|
item.item_count = 0;
|
|
564
571
|
}
|
|
572
|
+
// The dialog broker's result buffer exceeds the copy bound too;
|
|
573
|
+
// reset its bookkeeping and leave the buffer alone.
|
|
574
|
+
self.dialog_broker.reset();
|
|
565
575
|
self.started_timestamp_ns = timestampToU64(nowNanoseconds());
|
|
566
576
|
self.text_measure_provider = if (options.platform.services.measure_text_fn) |measure_fn|
|
|
567
577
|
.{
|
|
@@ -600,6 +610,7 @@ pub const Runtime = struct {
|
|
|
600
610
|
/// host) call it to return the storage.
|
|
601
611
|
pub fn deinit(self: *Runtime) void {
|
|
602
612
|
self.disarmMediaSurfaceWakes();
|
|
613
|
+
self.releaseDialogs();
|
|
603
614
|
for (self.views[0..self.view_count]) |*view| {
|
|
604
615
|
if (view.widget_text_bytes_heap_owned) self.owned_allocator.free(view.widget_text_bytes);
|
|
605
616
|
if (view.canvas_widget_text_history_bytes_heap_owned) self.owned_allocator.free(view.canvas_widget_text_history_bytes);
|
|
@@ -832,6 +843,19 @@ pub const Runtime = struct {
|
|
|
832
843
|
pub const showOpenDialog = SystemServiceMethods.showOpenDialog;
|
|
833
844
|
pub const showSaveDialog = SystemServiceMethods.showSaveDialog;
|
|
834
845
|
pub const showMessageDialog = SystemServiceMethods.showMessageDialog;
|
|
846
|
+
|
|
847
|
+
const DialogMethods = runtime_dialogs.RuntimeDialogs(Runtime);
|
|
848
|
+
pub const requestDialog = DialogMethods.requestDialog;
|
|
849
|
+
pub const requestOpenDialog = DialogMethods.requestOpenDialog;
|
|
850
|
+
pub const requestSaveDialog = DialogMethods.requestSaveDialog;
|
|
851
|
+
pub const requestMessageDialog = DialogMethods.requestMessageDialog;
|
|
852
|
+
pub const cancelDialog = DialogMethods.cancelDialog;
|
|
853
|
+
pub const adoptDialogResults = DialogMethods.adoptDialogResults;
|
|
854
|
+
pub const cancelDialogsForWindow = DialogMethods.cancelDialogsForWindow;
|
|
855
|
+
pub const abandonDialogs = DialogMethods.abandonDialogs;
|
|
856
|
+
pub const pendingDialogCount = DialogMethods.pendingDialogCount;
|
|
857
|
+
const releaseDialogs = DialogMethods.releaseDialogs;
|
|
858
|
+
|
|
835
859
|
pub const showNotification = SystemServiceMethods.showNotification;
|
|
836
860
|
pub const setCredential = SystemServiceMethods.setCredential;
|
|
837
861
|
pub const getCredential = SystemServiceMethods.getCredential;
|
|
@@ -863,9 +887,6 @@ pub const Runtime = struct {
|
|
|
863
887
|
const revealPathFromJson = SystemServiceMethods.revealPathFromJson;
|
|
864
888
|
const addRecentDocumentFromJson = SystemServiceMethods.addRecentDocumentFromJson;
|
|
865
889
|
const clearRecentDocumentsFromJson = SystemServiceMethods.clearRecentDocumentsFromJson;
|
|
866
|
-
const openFileDialogFromJson = SystemServiceMethods.openFileDialogFromJson;
|
|
867
|
-
const saveFileDialogFromJson = SystemServiceMethods.saveFileDialogFromJson;
|
|
868
|
-
const showMessageDialogFromJson = SystemServiceMethods.showMessageDialogFromJson;
|
|
869
890
|
|
|
870
891
|
/// Start (or replace) a platform timer. The platform delivers `.timer`
|
|
871
892
|
/// events carrying `id` until `cancelTimer` (or after the first fire when
|