@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
|
@@ -817,6 +817,26 @@ static int NativeSdkCredentialStatus(OSStatus status, int missingCode) {
|
|
|
817
817
|
@property(nonatomic, strong) NSString *openCommand;
|
|
818
818
|
@end
|
|
819
819
|
|
|
820
|
+
/* One asynchronous dialog (native_sdk_appkit_request_dialog): what is
|
|
821
|
+
* showing, where, and once it finishes, the result poll hands back. */
|
|
822
|
+
@interface NativeSdkDialog : NSObject
|
|
823
|
+
@property(nonatomic, assign) uint64_t dialogId;
|
|
824
|
+
@property(nonatomic, assign) uint64_t parentWindowId;
|
|
825
|
+
@property(nonatomic, strong) NSSavePanel *panel;
|
|
826
|
+
@property(nonatomic, strong) NSAlert *alert;
|
|
827
|
+
/* The sheet's window, when the dialog is attached to one. */
|
|
828
|
+
@property(nonatomic, weak) NSWindow *sheetParent;
|
|
829
|
+
/* A parentless alert runs modally from the next loop turn. */
|
|
830
|
+
@property(nonatomic, assign) BOOL modalRunning;
|
|
831
|
+
@property(nonatomic, assign) BOOL cancelRequested;
|
|
832
|
+
@property(nonatomic, assign) int outcome;
|
|
833
|
+
@property(nonatomic, assign) int button;
|
|
834
|
+
@property(nonatomic, strong) NSArray<NSString *> *paths;
|
|
835
|
+
@end
|
|
836
|
+
|
|
837
|
+
@implementation NativeSdkDialog
|
|
838
|
+
@end
|
|
839
|
+
|
|
820
840
|
@interface NativeSdkAppKitHost : NSObject <WKNavigationDelegate, NSMenuDelegate, NSUserNotificationCenterDelegate>
|
|
821
841
|
@property(nonatomic, strong) NSWindow *window;
|
|
822
842
|
@property(nonatomic, strong) WKWebView *webView;
|
|
@@ -881,6 +901,10 @@ static int NativeSdkCredentialStatus(OSStatus status, int missingCode) {
|
|
|
881
901
|
* block starts still gets its own turn. */
|
|
882
902
|
@property(atomic, assign) BOOL crossThreadFramePending;
|
|
883
903
|
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSTimer *> *appTimers;
|
|
904
|
+
/* Asynchronous dialogs by id while presented, and finished ones in the
|
|
905
|
+
* order they finished, awaiting poll. */
|
|
906
|
+
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NativeSdkDialog *> *dialogs;
|
|
907
|
+
@property(nonatomic, strong) NSMutableArray<NativeSdkDialog *> *finishedDialogs;
|
|
884
908
|
/* Notification userInfo persists with the delivered notification, so it
|
|
885
909
|
* carries only a per-process token. Commands remain in this live host;
|
|
886
910
|
* stale notifications from an earlier execution cannot dispatch into a
|
|
@@ -1242,6 +1266,25 @@ static int NativeSdkCredentialStatus(OSStatus status, int missingCode) {
|
|
|
1242
1266
|
- (void)emitShortcutWithId:(NSString *)identifier key:(NSString *)key modifiers:(uint32_t)modifiers event:(NSEvent *)event;
|
|
1243
1267
|
@end
|
|
1244
1268
|
|
|
1269
|
+
static void NativeSdkCancelDialogsForWindow(NativeSdkAppKitHost *host, uint64_t windowId);
|
|
1270
|
+
static void NativeSdkCancelAllDialogs(NativeSdkAppKitHost *host);
|
|
1271
|
+
|
|
1272
|
+
/* [NSApp stop:] and stopModal take effect only after the loop handles its
|
|
1273
|
+
* next event; post an empty one so a stop requested from a timer or block
|
|
1274
|
+
* ends the loop now. */
|
|
1275
|
+
static void NativeSdkPostEmptyEvent(void) {
|
|
1276
|
+
NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
|
|
1277
|
+
location:NSZeroPoint
|
|
1278
|
+
modifierFlags:0
|
|
1279
|
+
timestamp:0
|
|
1280
|
+
windowNumber:0
|
|
1281
|
+
context:nil
|
|
1282
|
+
subtype:0
|
|
1283
|
+
data1:0
|
|
1284
|
+
data2:0];
|
|
1285
|
+
[NSApp postEvent:event atStart:NO];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1245
1288
|
// Recursively re-emit the gpu-surface resize event for every metal
|
|
1246
1289
|
// surface under `view` (the tall-titlebar chrome re-query path).
|
|
1247
1290
|
static void NativeSdkEmitGpuSurfaceResizes(NSView *view) {
|
|
@@ -1423,6 +1466,9 @@ static NSPoint NativeSdkViewLocalYDownPoint(NSView *view, NSPoint point) {
|
|
|
1423
1466
|
|
|
1424
1467
|
- (void)windowWillClose:(NSNotification *)notification {
|
|
1425
1468
|
(void)notification;
|
|
1469
|
+
// End this window's sheets while it can still take them down; a
|
|
1470
|
+
// sheet on a closed window never calls its completion handler.
|
|
1471
|
+
NativeSdkCancelDialogsForWindow(self.host, self.windowId);
|
|
1426
1472
|
if (self.observesContentLayout) {
|
|
1427
1473
|
NSWindow *window = self.host.windows[@(self.windowId)];
|
|
1428
1474
|
[window removeObserver:self forKeyPath:@"contentLayoutRect"];
|
|
@@ -8182,6 +8228,8 @@ static float NativeSdkCaptureReadRemixedSample(const AudioBufferList *buffers, c
|
|
|
8182
8228
|
self.nativeViewExplicitTextKeys = [[NSMutableSet alloc] init];
|
|
8183
8229
|
self.bridgeEnabledChildWebViewKeys = [[NSMutableSet alloc] init];
|
|
8184
8230
|
self.appTimers = [[NSMutableDictionary alloc] init];
|
|
8231
|
+
self.dialogs = [[NSMutableDictionary alloc] init];
|
|
8232
|
+
self.finishedDialogs = [[NSMutableArray alloc] init];
|
|
8185
8233
|
self.notificationActionCommands = [[NSMutableDictionary alloc] init];
|
|
8186
8234
|
self.notificationActionTokensByIdentifier = [[NSMutableDictionary alloc] init];
|
|
8187
8235
|
self.statusItems = [[NSMutableDictionary alloc] init];
|
|
@@ -8497,6 +8545,9 @@ static float NativeSdkCaptureReadRemixedSample(const AudioBufferList *buffers, c
|
|
|
8497
8545
|
- (void)closeWindowWithId:(uint64_t)windowId {
|
|
8498
8546
|
NSWindow *window = self.windows[@(windowId)];
|
|
8499
8547
|
if (!window) return;
|
|
8548
|
+
// performClose: refuses a window with a sheet attached, so end this
|
|
8549
|
+
// window's dialogs first (Electron's NativeWindowMac::Close does too).
|
|
8550
|
+
NativeSdkCancelDialogsForWindow(self, windowId);
|
|
8500
8551
|
// A runtime-initiated close is the app DECIDING to close: it must
|
|
8501
8552
|
// not bounce off the window's own .hide close policy, so it skips
|
|
8502
8553
|
// performClose: (which runs windowShouldClose:) and closes
|
|
@@ -10766,17 +10817,9 @@ static NSString *NativeSdkSigningTeamIdentifier(NSString *bundlePath) {
|
|
|
10766
10817
|
}
|
|
10767
10818
|
[self stopAppearanceObservers];
|
|
10768
10819
|
[self stopApplicationActivationObservers];
|
|
10820
|
+
NativeSdkCancelAllDialogs(self);
|
|
10769
10821
|
[NSApp stop:nil];
|
|
10770
|
-
|
|
10771
|
-
location:NSZeroPoint
|
|
10772
|
-
modifierFlags:0
|
|
10773
|
-
timestamp:0
|
|
10774
|
-
windowNumber:0
|
|
10775
|
-
context:nil
|
|
10776
|
-
subtype:0
|
|
10777
|
-
data1:0
|
|
10778
|
-
data2:0];
|
|
10779
|
-
[NSApp postEvent:event atStart:NO];
|
|
10822
|
+
NativeSdkPostEmptyEvent();
|
|
10780
10823
|
}
|
|
10781
10824
|
|
|
10782
10825
|
/* Drain the quit verb a pre-run dispatch parked (see
|
|
@@ -13271,6 +13314,372 @@ int native_sdk_appkit_check_for_updates(native_sdk_appkit_host_t *host, int user
|
|
|
13271
13314
|
return 1;
|
|
13272
13315
|
}
|
|
13273
13316
|
|
|
13317
|
+
// ---------------------------------------------------------------------------
|
|
13318
|
+
// App-driven updates (`updates.mode = "app"`): the platform half of
|
|
13319
|
+
// src/updater/engine.zig, reached through src/updater/macos.zig. Unlike the
|
|
13320
|
+
// host flow above, nothing here shows UI; the engine calls these from its
|
|
13321
|
+
// worker threads and from the app loop. The release stages beside the app,
|
|
13322
|
+
// `<parent>/.<App.app>.native-update/`, so installing it is a rename on the
|
|
13323
|
+
// same volume, and a helper installs it once this process exits.
|
|
13324
|
+
|
|
13325
|
+
enum {
|
|
13326
|
+
NativeSdkUpdateOk = 0,
|
|
13327
|
+
NativeSdkUpdateNetwork = 1,
|
|
13328
|
+
NativeSdkUpdateHttpStatus = 2,
|
|
13329
|
+
NativeSdkUpdateTooLarge = 3,
|
|
13330
|
+
NativeSdkUpdateCancelled = 4,
|
|
13331
|
+
NativeSdkUpdateDisk = 5,
|
|
13332
|
+
};
|
|
13333
|
+
|
|
13334
|
+
static NSString *NativeSdkUpdateString(const char *bytes, size_t length) {
|
|
13335
|
+
if (!bytes || length == 0) return @"";
|
|
13336
|
+
return [[NSString alloc] initWithBytes:bytes length:length encoding:NSUTF8StringEncoding] ?: @"";
|
|
13337
|
+
}
|
|
13338
|
+
|
|
13339
|
+
static size_t NativeSdkUpdateCopy(NSString *value, char *output, size_t capacity) {
|
|
13340
|
+
const char *bytes = value.UTF8String;
|
|
13341
|
+
const size_t length = bytes ? strlen(bytes) : 0;
|
|
13342
|
+
if (length == 0 || length > capacity) return 0;
|
|
13343
|
+
memcpy(output, bytes, length);
|
|
13344
|
+
return length;
|
|
13345
|
+
}
|
|
13346
|
+
|
|
13347
|
+
static NSString *NativeSdkUpdateBundlePath(void) {
|
|
13348
|
+
return NSBundle.mainBundle.bundlePath.stringByStandardizingPath;
|
|
13349
|
+
}
|
|
13350
|
+
|
|
13351
|
+
static NSString *NativeSdkUpdateSiblingPath(NSString *suffix) {
|
|
13352
|
+
NSString *bundlePath = NativeSdkUpdateBundlePath();
|
|
13353
|
+
return [bundlePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:[NSString stringWithFormat:@".%@.%@", bundlePath.lastPathComponent, suffix]];
|
|
13354
|
+
}
|
|
13355
|
+
|
|
13356
|
+
static NSString *NativeSdkUpdateResultPath(NSString *bundleIdentifier) {
|
|
13357
|
+
NSURL *support = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask].firstObject;
|
|
13358
|
+
if (!support || bundleIdentifier.length == 0) return nil;
|
|
13359
|
+
return [[support.path stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:@".native-update-result"];
|
|
13360
|
+
}
|
|
13361
|
+
|
|
13362
|
+
static BOOL NativeSdkUpdateURLAllowed(NSURL *url, BOOL allowLoopbackHttp) {
|
|
13363
|
+
NSString *scheme = url.scheme.lowercaseString;
|
|
13364
|
+
if ([scheme isEqualToString:@"https"]) return url.host.length > 0;
|
|
13365
|
+
if (!allowLoopbackHttp || ![scheme isEqualToString:@"http"]) return NO;
|
|
13366
|
+
NSString *host = url.host.lowercaseString;
|
|
13367
|
+
return [host isEqualToString:@"127.0.0.1"] || [host isEqualToString:@"::1"] || [host isEqualToString:@"localhost"];
|
|
13368
|
+
}
|
|
13369
|
+
|
|
13370
|
+
/// The one app bundle a staged release unpacked to, when it is this app at
|
|
13371
|
+
/// `version`; Info.plist is read directly because NSBundle caches bundles
|
|
13372
|
+
/// by path and the staging path is reused.
|
|
13373
|
+
static NSString *NativeSdkUpdateStagedBundle(NSString *stagePath, NSString *bundleIdentifier, NSString *version) {
|
|
13374
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13375
|
+
NSArray<NSString *> *entries = [manager contentsOfDirectoryAtPath:stagePath error:nil];
|
|
13376
|
+
NSString *bundleName = nil;
|
|
13377
|
+
for (NSString *entry in entries ?: @[]) {
|
|
13378
|
+
if ([entry isEqualToString:@".native-staged"]) continue;
|
|
13379
|
+
if (bundleName || ![[entry pathExtension].lowercaseString isEqualToString:@"app"]) return nil;
|
|
13380
|
+
bundleName = entry;
|
|
13381
|
+
}
|
|
13382
|
+
if (!bundleName) return nil;
|
|
13383
|
+
NSString *candidate = [stagePath stringByAppendingPathComponent:bundleName];
|
|
13384
|
+
NSNumber *isDirectory = nil;
|
|
13385
|
+
NSNumber *isSymlink = nil;
|
|
13386
|
+
[[NSURL fileURLWithPath:candidate] getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
|
|
13387
|
+
[[NSURL fileURLWithPath:candidate] getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil];
|
|
13388
|
+
if (!isDirectory.boolValue || isSymlink.boolValue) return nil;
|
|
13389
|
+
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[candidate stringByAppendingPathComponent:@"Contents/Info.plist"]];
|
|
13390
|
+
id identifier = info[@"CFBundleIdentifier"];
|
|
13391
|
+
id bundleVersion = info[@"CFBundleVersion"];
|
|
13392
|
+
if (![identifier isKindOfClass:[NSString class]] || ![identifier isEqualToString:bundleIdentifier]) return nil;
|
|
13393
|
+
if (![bundleVersion isKindOfClass:[NSString class]] || ![bundleVersion isEqualToString:version]) return nil;
|
|
13394
|
+
return candidate;
|
|
13395
|
+
}
|
|
13396
|
+
|
|
13397
|
+
/// A Developer ID app accepts only a release signed by the same team, as
|
|
13398
|
+
/// the host flow does; unsigned and ad-hoc installs rely on the feed's
|
|
13399
|
+
/// Ed25519 signature alone.
|
|
13400
|
+
static BOOL NativeSdkUpdateSignatureMatches(NSString *candidate, NSString *bundleIdentifier) {
|
|
13401
|
+
NSString *teamIdentifier = NativeSdkSigningTeamIdentifier(NativeSdkUpdateBundlePath());
|
|
13402
|
+
if (teamIdentifier.length == 0) return YES;
|
|
13403
|
+
if ([teamIdentifier rangeOfCharacterFromSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]].location != NSNotFound ||
|
|
13404
|
+
[bundleIdentifier rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\\\""]].location != NSNotFound) return NO;
|
|
13405
|
+
NSString *requirement = [NSString stringWithFormat:@"anchor apple generic and certificate leaf[subject.OU] = \"%@\" and identifier \"%@\"", teamIdentifier, bundleIdentifier];
|
|
13406
|
+
return NativeSdkRunTask(@"/usr/bin/codesign", @[ @"--verify", @"--deep", @"--strict", [@"-R=" stringByAppendingString:requirement], candidate ], NULL);
|
|
13407
|
+
}
|
|
13408
|
+
|
|
13409
|
+
typedef void (*NativeSdkUpdateProgressFn)(void *context, uint64_t received);
|
|
13410
|
+
typedef int (*NativeSdkUpdateCancelledFn)(void *context);
|
|
13411
|
+
|
|
13412
|
+
@interface NativeSdkUpdateTransfer : NSObject <NSURLSessionDataDelegate>
|
|
13413
|
+
@property(nonatomic) BOOL allowLoopbackHttp;
|
|
13414
|
+
@property(nonatomic) uint64_t limit;
|
|
13415
|
+
@property(nonatomic) uint64_t received;
|
|
13416
|
+
@property(atomic) int result;
|
|
13417
|
+
@property(nonatomic, strong) NSMutableData *data;
|
|
13418
|
+
@property(nonatomic, strong) NSFileHandle *file;
|
|
13419
|
+
@property(nonatomic) void *context;
|
|
13420
|
+
@property(nonatomic) NativeSdkUpdateProgressFn progress;
|
|
13421
|
+
@property(nonatomic, strong) dispatch_semaphore_t done;
|
|
13422
|
+
@end
|
|
13423
|
+
|
|
13424
|
+
@implementation NativeSdkUpdateTransfer
|
|
13425
|
+
|
|
13426
|
+
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler {
|
|
13427
|
+
(void)session;
|
|
13428
|
+
(void)response;
|
|
13429
|
+
if (NativeSdkUpdateURLAllowed(request.URL, self.allowLoopbackHttp)) {
|
|
13430
|
+
completionHandler(request);
|
|
13431
|
+
return;
|
|
13432
|
+
}
|
|
13433
|
+
self.result = NativeSdkUpdateNetwork;
|
|
13434
|
+
completionHandler(nil);
|
|
13435
|
+
[task cancel];
|
|
13436
|
+
}
|
|
13437
|
+
|
|
13438
|
+
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
|
|
13439
|
+
(void)session;
|
|
13440
|
+
(void)dataTask;
|
|
13441
|
+
NSHTTPURLResponse *http = [response isKindOfClass:[NSHTTPURLResponse class]] ? (NSHTTPURLResponse *)response : nil;
|
|
13442
|
+
if (!http || !NativeSdkUpdateURLAllowed(response.URL, self.allowLoopbackHttp)) {
|
|
13443
|
+
self.result = NativeSdkUpdateNetwork;
|
|
13444
|
+
} else if (http.statusCode < 200 || http.statusCode >= 300) {
|
|
13445
|
+
self.result = NativeSdkUpdateHttpStatus;
|
|
13446
|
+
} else if (response.expectedContentLength > 0 && (uint64_t)response.expectedContentLength > self.limit) {
|
|
13447
|
+
self.result = NativeSdkUpdateTooLarge;
|
|
13448
|
+
}
|
|
13449
|
+
completionHandler(self.result == NativeSdkUpdateOk ? NSURLSessionResponseAllow : NSURLSessionResponseCancel);
|
|
13450
|
+
}
|
|
13451
|
+
|
|
13452
|
+
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
|
|
13453
|
+
(void)session;
|
|
13454
|
+
if (self.result != NativeSdkUpdateOk) return;
|
|
13455
|
+
if (data.length > self.limit - self.received) {
|
|
13456
|
+
self.result = NativeSdkUpdateTooLarge;
|
|
13457
|
+
[dataTask cancel];
|
|
13458
|
+
return;
|
|
13459
|
+
}
|
|
13460
|
+
if (self.file) {
|
|
13461
|
+
NSError *error = nil;
|
|
13462
|
+
if (![self.file writeData:data error:&error]) {
|
|
13463
|
+
self.result = NativeSdkUpdateDisk;
|
|
13464
|
+
[dataTask cancel];
|
|
13465
|
+
return;
|
|
13466
|
+
}
|
|
13467
|
+
} else {
|
|
13468
|
+
[self.data appendData:data];
|
|
13469
|
+
}
|
|
13470
|
+
self.received += data.length;
|
|
13471
|
+
if (self.progress) self.progress(self.context, self.received);
|
|
13472
|
+
}
|
|
13473
|
+
|
|
13474
|
+
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
|
|
13475
|
+
(void)session;
|
|
13476
|
+
(void)task;
|
|
13477
|
+
if (error && self.result == NativeSdkUpdateOk) self.result = NativeSdkUpdateNetwork;
|
|
13478
|
+
dispatch_semaphore_signal(self.done);
|
|
13479
|
+
}
|
|
13480
|
+
|
|
13481
|
+
@end
|
|
13482
|
+
|
|
13483
|
+
/// GETs `url` through the user's network settings, waiting on the calling
|
|
13484
|
+
/// (worker) thread and honoring `cancelled` while the transfer stalls.
|
|
13485
|
+
static int NativeSdkUpdateTransferRun(NSURL *url, NativeSdkUpdateTransfer *transfer, void *context, NativeSdkUpdateCancelledFn cancelled) {
|
|
13486
|
+
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
|
13487
|
+
configuration.timeoutIntervalForRequest = 30;
|
|
13488
|
+
configuration.timeoutIntervalForResource = 60 * 60;
|
|
13489
|
+
configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
|
13490
|
+
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
|
|
13491
|
+
queue.maxConcurrentOperationCount = 1;
|
|
13492
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:transfer delegateQueue:queue];
|
|
13493
|
+
transfer.done = dispatch_semaphore_create(0);
|
|
13494
|
+
NSURLSessionDataTask *task = [session dataTaskWithURL:url];
|
|
13495
|
+
[task resume];
|
|
13496
|
+
while (dispatch_semaphore_wait(transfer.done, dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC)) != 0) {
|
|
13497
|
+
if (cancelled && cancelled(context) && transfer.result == NativeSdkUpdateOk) {
|
|
13498
|
+
transfer.result = NativeSdkUpdateCancelled;
|
|
13499
|
+
[task cancel];
|
|
13500
|
+
}
|
|
13501
|
+
}
|
|
13502
|
+
[session finishTasksAndInvalidate];
|
|
13503
|
+
return transfer.result;
|
|
13504
|
+
}
|
|
13505
|
+
|
|
13506
|
+
int native_sdk_macos_updates_eligibility(void) {
|
|
13507
|
+
@autoreleasepool {
|
|
13508
|
+
NSString *bundlePath = NativeSdkUpdateBundlePath();
|
|
13509
|
+
if (![bundlePath.pathExtension.lowercaseString isEqualToString:@"app"]) return 1;
|
|
13510
|
+
// A quarantined app Gatekeeper runs from a randomized read-only
|
|
13511
|
+
// mount cannot replace itself until the user moves it.
|
|
13512
|
+
if ([bundlePath rangeOfString:@"/AppTranslocation/"].location != NSNotFound) return 2;
|
|
13513
|
+
if (![[NSFileManager defaultManager] isWritableFileAtPath:bundlePath.stringByDeletingLastPathComponent]) return 2;
|
|
13514
|
+
return 0;
|
|
13515
|
+
}
|
|
13516
|
+
}
|
|
13517
|
+
|
|
13518
|
+
size_t native_sdk_macos_updates_staged_version(const char *bundle_id, size_t bundle_id_len, char *output, size_t capacity) {
|
|
13519
|
+
@autoreleasepool {
|
|
13520
|
+
NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
|
|
13521
|
+
NSString *version = [NSString stringWithContentsOfFile:[stagePath stringByAppendingPathComponent:@".native-staged"] encoding:NSUTF8StringEncoding error:nil];
|
|
13522
|
+
if (version.length == 0 || !NativeSdkUpdateStagedBundle(stagePath, NativeSdkUpdateString(bundle_id, bundle_id_len), version)) return 0;
|
|
13523
|
+
return NativeSdkUpdateCopy(version, output, capacity);
|
|
13524
|
+
}
|
|
13525
|
+
}
|
|
13526
|
+
|
|
13527
|
+
size_t native_sdk_macos_updates_take_last(const char *bundle_id, size_t bundle_id_len, char *output, size_t capacity) {
|
|
13528
|
+
@autoreleasepool {
|
|
13529
|
+
NSString *resultPath = NativeSdkUpdateResultPath(NativeSdkUpdateString(bundle_id, bundle_id_len));
|
|
13530
|
+
if (!resultPath) return 0;
|
|
13531
|
+
NSString *record = [NSString stringWithContentsOfFile:resultPath encoding:NSUTF8StringEncoding error:nil];
|
|
13532
|
+
if (!record) return 0;
|
|
13533
|
+
[[NSFileManager defaultManager] removeItemAtPath:resultPath error:nil];
|
|
13534
|
+
NSString *line = [record stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
13535
|
+
if (![line hasPrefix:@"updated "]) return 0;
|
|
13536
|
+
return NativeSdkUpdateCopy([line substringFromIndex:8], output, capacity);
|
|
13537
|
+
}
|
|
13538
|
+
}
|
|
13539
|
+
|
|
13540
|
+
int native_sdk_macos_updates_fetch(const char *url, size_t url_len, int allow_loopback_http, uint8_t *output, size_t capacity, size_t *output_len) {
|
|
13541
|
+
@autoreleasepool {
|
|
13542
|
+
NSURL *target = [NSURL URLWithString:NativeSdkUpdateString(url, url_len)];
|
|
13543
|
+
if (!target || !NativeSdkUpdateURLAllowed(target, allow_loopback_http != 0)) return NativeSdkUpdateNetwork;
|
|
13544
|
+
NativeSdkUpdateTransfer *transfer = [[NativeSdkUpdateTransfer alloc] init];
|
|
13545
|
+
transfer.allowLoopbackHttp = allow_loopback_http != 0;
|
|
13546
|
+
transfer.limit = capacity;
|
|
13547
|
+
transfer.data = [NSMutableData data];
|
|
13548
|
+
const int result = NativeSdkUpdateTransferRun(target, transfer, NULL, NULL);
|
|
13549
|
+
if (result != NativeSdkUpdateOk) return result;
|
|
13550
|
+
memcpy(output, transfer.data.bytes, transfer.data.length);
|
|
13551
|
+
*output_len = transfer.data.length;
|
|
13552
|
+
return NativeSdkUpdateOk;
|
|
13553
|
+
}
|
|
13554
|
+
}
|
|
13555
|
+
|
|
13556
|
+
int native_sdk_macos_updates_download(const char *url, size_t url_len, int allow_loopback_http, uint64_t limit, char *path_output, size_t path_capacity, size_t *path_len, void *context, NativeSdkUpdateProgressFn progress, NativeSdkUpdateCancelledFn cancelled) {
|
|
13557
|
+
@autoreleasepool {
|
|
13558
|
+
NSURL *target = [NSURL URLWithString:NativeSdkUpdateString(url, url_len)];
|
|
13559
|
+
if (!target || !NativeSdkUpdateURLAllowed(target, allow_loopback_http != 0)) return NativeSdkUpdateNetwork;
|
|
13560
|
+
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"native-update-%@.zip", NSUUID.UUID.UUIDString]];
|
|
13561
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13562
|
+
if (![manager createFileAtPath:path contents:nil attributes:@{ NSFilePosixPermissions: @0600 }]) return NativeSdkUpdateDisk;
|
|
13563
|
+
NSFileHandle *file = [NSFileHandle fileHandleForWritingAtPath:path];
|
|
13564
|
+
if (!file) {
|
|
13565
|
+
[manager removeItemAtPath:path error:nil];
|
|
13566
|
+
return NativeSdkUpdateDisk;
|
|
13567
|
+
}
|
|
13568
|
+
NativeSdkUpdateTransfer *transfer = [[NativeSdkUpdateTransfer alloc] init];
|
|
13569
|
+
transfer.allowLoopbackHttp = allow_loopback_http != 0;
|
|
13570
|
+
transfer.limit = limit;
|
|
13571
|
+
transfer.file = file;
|
|
13572
|
+
transfer.context = context;
|
|
13573
|
+
transfer.progress = progress;
|
|
13574
|
+
int result = NativeSdkUpdateTransferRun(target, transfer, context, cancelled);
|
|
13575
|
+
if (![file closeAndReturnError:nil] && result == NativeSdkUpdateOk) result = NativeSdkUpdateDisk;
|
|
13576
|
+
const size_t length = NativeSdkUpdateCopy(path, path_output, path_capacity);
|
|
13577
|
+
if (result == NativeSdkUpdateOk && length == 0) result = NativeSdkUpdateDisk;
|
|
13578
|
+
if (result != NativeSdkUpdateOk) {
|
|
13579
|
+
[manager removeItemAtPath:path error:nil];
|
|
13580
|
+
return result;
|
|
13581
|
+
}
|
|
13582
|
+
*path_len = length;
|
|
13583
|
+
return NativeSdkUpdateOk;
|
|
13584
|
+
}
|
|
13585
|
+
}
|
|
13586
|
+
|
|
13587
|
+
int native_sdk_macos_updates_stage(const char *archive_path, size_t archive_path_len, const char *bundle_id, size_t bundle_id_len, const char *version, size_t version_len) {
|
|
13588
|
+
@autoreleasepool {
|
|
13589
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13590
|
+
NSString *bundleIdentifier = NativeSdkUpdateString(bundle_id, bundle_id_len);
|
|
13591
|
+
NSString *expectedVersion = NativeSdkUpdateString(version, version_len);
|
|
13592
|
+
NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
|
|
13593
|
+
[manager removeItemAtPath:stagePath error:nil];
|
|
13594
|
+
if (![manager createDirectoryAtPath:stagePath withIntermediateDirectories:NO attributes:nil error:nil]) return 2;
|
|
13595
|
+
NSString *candidate = nil;
|
|
13596
|
+
if (NativeSdkRunTask(@"/usr/bin/ditto", @[ @"-x", @"-k", NativeSdkUpdateString(archive_path, archive_path_len), stagePath ], NULL)) {
|
|
13597
|
+
candidate = NativeSdkUpdateStagedBundle(stagePath, bundleIdentifier, expectedVersion);
|
|
13598
|
+
}
|
|
13599
|
+
if (!candidate || !NativeSdkUpdateSignatureMatches(candidate, bundleIdentifier)) {
|
|
13600
|
+
[manager removeItemAtPath:stagePath error:nil];
|
|
13601
|
+
return 1;
|
|
13602
|
+
}
|
|
13603
|
+
if (![expectedVersion writeToFile:[stagePath stringByAppendingPathComponent:@".native-staged"] atomically:YES encoding:NSUTF8StringEncoding error:nil]) {
|
|
13604
|
+
[manager removeItemAtPath:stagePath error:nil];
|
|
13605
|
+
return 2;
|
|
13606
|
+
}
|
|
13607
|
+
return 0;
|
|
13608
|
+
}
|
|
13609
|
+
}
|
|
13610
|
+
|
|
13611
|
+
void native_sdk_macos_updates_discard(void) {
|
|
13612
|
+
@autoreleasepool {
|
|
13613
|
+
[[NSFileManager defaultManager] removeItemAtPath:NativeSdkUpdateSiblingPath(@"native-update") error:nil];
|
|
13614
|
+
}
|
|
13615
|
+
}
|
|
13616
|
+
|
|
13617
|
+
/// Starts the helper that waits for this process to exit, replaces the app
|
|
13618
|
+
/// with the staged release (the old one returns if that fails), records
|
|
13619
|
+
/// the switch for the new version to report, and with `relaunch` opens it.
|
|
13620
|
+
int native_sdk_macos_updates_install(const char *bundle_id, size_t bundle_id_len, const char *from_version, size_t from_version_len, int relaunch) {
|
|
13621
|
+
@autoreleasepool {
|
|
13622
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13623
|
+
NSString *bundleIdentifier = NativeSdkUpdateString(bundle_id, bundle_id_len);
|
|
13624
|
+
NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
|
|
13625
|
+
NSString *version = [NSString stringWithContentsOfFile:[stagePath stringByAppendingPathComponent:@".native-staged"] encoding:NSUTF8StringEncoding error:nil];
|
|
13626
|
+
NSString *candidate = version.length > 0 ? NativeSdkUpdateStagedBundle(stagePath, bundleIdentifier, version) : nil;
|
|
13627
|
+
if (!candidate) return 0;
|
|
13628
|
+
NSString *scriptPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"native-update-install-%@.sh", NSUUID.UUID.UUIDString]];
|
|
13629
|
+
NSString *script = @"#!/bin/sh\n"
|
|
13630
|
+
"set -u\n"
|
|
13631
|
+
"pid=\"$1\"; current=\"$2\"; candidate=\"$3\"; backup=\"$4\"; stage=\"$5\"; relaunch=\"$6\"; result=\"$7\"; from=\"$8\"; to=\"$9\"\n"
|
|
13632
|
+
"count=0\n"
|
|
13633
|
+
"while /bin/kill -0 \"$pid\" 2>/dev/null; do\n"
|
|
13634
|
+
" count=$((count + 1))\n"
|
|
13635
|
+
" if [ \"$count\" -ge 600 ]; then /bin/rm -f \"$0\"; exit 1; fi\n"
|
|
13636
|
+
" /bin/sleep 0.1\n"
|
|
13637
|
+
"done\n"
|
|
13638
|
+
"/bin/rm -rf \"$backup\"\n"
|
|
13639
|
+
"if /bin/mv \"$current\" \"$backup\"; then\n"
|
|
13640
|
+
" if /bin/mv \"$candidate\" \"$current\"; then\n"
|
|
13641
|
+
" if [ -n \"$result\" ]; then /bin/mkdir -p \"${result%/*}\" && /usr/bin/printf 'updated %s %s\\n' \"$from\" \"$to\" > \"$result\"; fi\n"
|
|
13642
|
+
" if [ \"$relaunch\" != 1 ] || /usr/bin/open \"$current\"; then\n"
|
|
13643
|
+
" /bin/rm -rf \"$backup\" \"$stage\"\n"
|
|
13644
|
+
" /bin/rm -f \"$0\"\n"
|
|
13645
|
+
" exit 0\n"
|
|
13646
|
+
" fi\n"
|
|
13647
|
+
" if [ -n \"$result\" ]; then /bin/rm -f \"$result\"; fi\n"
|
|
13648
|
+
" /bin/rm -rf \"$current\"\n"
|
|
13649
|
+
" fi\n"
|
|
13650
|
+
" /bin/mv \"$backup\" \"$current\"\n"
|
|
13651
|
+
"fi\n"
|
|
13652
|
+
"/bin/rm -rf \"$stage\"\n"
|
|
13653
|
+
"if [ \"$relaunch\" = 1 ]; then /usr/bin/open \"$current\" >/dev/null 2>&1 || true; fi\n"
|
|
13654
|
+
"/bin/rm -f \"$0\"\n"
|
|
13655
|
+
"exit 1\n";
|
|
13656
|
+
if (![script writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil] ||
|
|
13657
|
+
![manager setAttributes:@{ NSFilePosixPermissions: @0700 } ofItemAtPath:scriptPath error:nil]) {
|
|
13658
|
+
[manager removeItemAtPath:scriptPath error:nil];
|
|
13659
|
+
return 0;
|
|
13660
|
+
}
|
|
13661
|
+
NSTask *installer = [[NSTask alloc] init];
|
|
13662
|
+
installer.executableURL = [NSURL fileURLWithPath:@"/bin/sh"];
|
|
13663
|
+
installer.arguments = @[
|
|
13664
|
+
scriptPath,
|
|
13665
|
+
[NSString stringWithFormat:@"%d", getpid()],
|
|
13666
|
+
NativeSdkUpdateBundlePath(),
|
|
13667
|
+
candidate,
|
|
13668
|
+
NativeSdkUpdateSiblingPath(@"native-update-backup"),
|
|
13669
|
+
stagePath,
|
|
13670
|
+
relaunch ? @"1" : @"0",
|
|
13671
|
+
NativeSdkUpdateResultPath(bundleIdentifier) ?: @"",
|
|
13672
|
+
NativeSdkUpdateString(from_version, from_version_len),
|
|
13673
|
+
version,
|
|
13674
|
+
];
|
|
13675
|
+
if (![installer launchAndReturnError:nil]) {
|
|
13676
|
+
[manager removeItemAtPath:scriptPath error:nil];
|
|
13677
|
+
return 0;
|
|
13678
|
+
}
|
|
13679
|
+
return 1;
|
|
13680
|
+
}
|
|
13681
|
+
}
|
|
13682
|
+
|
|
13274
13683
|
void native_sdk_appkit_run(native_sdk_appkit_host_t *host, native_sdk_appkit_event_callback_t callback, void *context) {
|
|
13275
13684
|
NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
|
|
13276
13685
|
[object runWithCallback:callback context:context];
|
|
@@ -13992,23 +14401,71 @@ static void NativeSdkConfigurePanelExtensions(NSSavePanel *panel, NSArray<NSStri
|
|
|
13992
14401
|
}
|
|
13993
14402
|
}
|
|
13994
14403
|
|
|
14404
|
+
static NSString *NativeSdkDialogString(const char *bytes, size_t len) {
|
|
14405
|
+
if (!bytes || len == 0) return nil;
|
|
14406
|
+
return [[NSString alloc] initWithBytes:bytes length:len encoding:NSUTF8StringEncoding];
|
|
14407
|
+
}
|
|
14408
|
+
|
|
14409
|
+
static NSOpenPanel *NativeSdkMakeOpenPanel(const native_sdk_appkit_open_dialog_opts_t *opts) {
|
|
14410
|
+
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
|
14411
|
+
NSString *title = NativeSdkDialogString(opts->title, opts->title_len);
|
|
14412
|
+
if (title) panel.title = title;
|
|
14413
|
+
NSString *path = NativeSdkDialogString(opts->default_path, opts->default_path_len);
|
|
14414
|
+
if (path) panel.directoryURL = [NSURL fileURLWithPath:path];
|
|
14415
|
+
panel.canChooseFiles = opts->allow_directories == 0;
|
|
14416
|
+
panel.canChooseDirectories = opts->allow_directories != 0;
|
|
14417
|
+
panel.allowsMultipleSelection = opts->allow_multiple != 0;
|
|
14418
|
+
NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
|
|
14419
|
+
return panel;
|
|
14420
|
+
}
|
|
14421
|
+
|
|
14422
|
+
static NSSavePanel *NativeSdkMakeSavePanel(const native_sdk_appkit_save_dialog_opts_t *opts) {
|
|
14423
|
+
NSSavePanel *panel = [NSSavePanel savePanel];
|
|
14424
|
+
NSString *title = NativeSdkDialogString(opts->title, opts->title_len);
|
|
14425
|
+
if (title) panel.title = title;
|
|
14426
|
+
NSString *path = NativeSdkDialogString(opts->default_path, opts->default_path_len);
|
|
14427
|
+
if (path) panel.directoryURL = [NSURL fileURLWithPath:path];
|
|
14428
|
+
NSString *name = NativeSdkDialogString(opts->default_name, opts->default_name_len);
|
|
14429
|
+
if (name) panel.nameFieldStringValue = name;
|
|
14430
|
+
NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
|
|
14431
|
+
return panel;
|
|
14432
|
+
}
|
|
14433
|
+
|
|
14434
|
+
static NSAlert *NativeSdkMakeAlert(const native_sdk_appkit_message_dialog_opts_t *opts) {
|
|
14435
|
+
NSAlert *alert = [[NSAlert alloc] init];
|
|
14436
|
+
switch (opts->style) {
|
|
14437
|
+
case 1: alert.alertStyle = NSAlertStyleWarning; break;
|
|
14438
|
+
case 2: alert.alertStyle = NSAlertStyleCritical; break;
|
|
14439
|
+
default: alert.alertStyle = NSAlertStyleInformational; break;
|
|
14440
|
+
}
|
|
14441
|
+
NSString *title = NativeSdkDialogString(opts->title, opts->title_len);
|
|
14442
|
+
NSString *message = NativeSdkDialogString(opts->message, opts->message_len);
|
|
14443
|
+
NSString *informative = NativeSdkDialogString(opts->informative_text, opts->informative_text_len);
|
|
14444
|
+
if (message.length > 0) {
|
|
14445
|
+
alert.messageText = message;
|
|
14446
|
+
} else if (title.length > 0) {
|
|
14447
|
+
alert.messageText = title;
|
|
14448
|
+
}
|
|
14449
|
+
if (informative.length > 0) {
|
|
14450
|
+
alert.informativeText = informative;
|
|
14451
|
+
}
|
|
14452
|
+
if (opts->message && opts->message_len > 0) {
|
|
14453
|
+
alert.window.title = title.length > 0 ? title : @"";
|
|
14454
|
+
}
|
|
14455
|
+
NSString *primary = NativeSdkDialogString(opts->primary_button, opts->primary_button_len);
|
|
14456
|
+
[alert addButtonWithTitle:primary ?: @"OK"];
|
|
14457
|
+
NSString *secondary = NativeSdkDialogString(opts->secondary_button, opts->secondary_button_len);
|
|
14458
|
+
if (secondary) [alert addButtonWithTitle:secondary];
|
|
14459
|
+
NSString *tertiary = NativeSdkDialogString(opts->tertiary_button, opts->tertiary_button_len);
|
|
14460
|
+
if (tertiary) [alert addButtonWithTitle:tertiary];
|
|
14461
|
+
return alert;
|
|
14462
|
+
}
|
|
14463
|
+
|
|
13995
14464
|
native_sdk_appkit_open_dialog_result_t native_sdk_appkit_show_open_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_open_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
|
|
13996
14465
|
(void)host;
|
|
13997
14466
|
native_sdk_appkit_open_dialog_result_t result = { .count = 0, .bytes_written = 0 };
|
|
13998
14467
|
@autoreleasepool {
|
|
13999
|
-
NSOpenPanel *panel =
|
|
14000
|
-
if (opts->title && opts->title_len > 0) {
|
|
14001
|
-
panel.title = [[NSString alloc] initWithBytes:opts->title length:opts->title_len encoding:NSUTF8StringEncoding];
|
|
14002
|
-
}
|
|
14003
|
-
if (opts->default_path && opts->default_path_len > 0) {
|
|
14004
|
-
NSString *path = [[NSString alloc] initWithBytes:opts->default_path length:opts->default_path_len encoding:NSUTF8StringEncoding];
|
|
14005
|
-
panel.directoryURL = [NSURL fileURLWithPath:path];
|
|
14006
|
-
}
|
|
14007
|
-
panel.canChooseFiles = opts->allow_directories == 0;
|
|
14008
|
-
panel.canChooseDirectories = opts->allow_directories != 0;
|
|
14009
|
-
panel.allowsMultipleSelection = opts->allow_multiple != 0;
|
|
14010
|
-
NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
|
|
14011
|
-
|
|
14468
|
+
NSOpenPanel *panel = NativeSdkMakeOpenPanel(opts);
|
|
14012
14469
|
if ([panel runModal] != NSModalResponseOK) return result;
|
|
14013
14470
|
|
|
14014
14471
|
size_t offset = 0;
|
|
@@ -14035,19 +14492,7 @@ native_sdk_appkit_open_dialog_result_t native_sdk_appkit_show_open_dialog(native
|
|
|
14035
14492
|
size_t native_sdk_appkit_show_save_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_save_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
|
|
14036
14493
|
(void)host;
|
|
14037
14494
|
@autoreleasepool {
|
|
14038
|
-
NSSavePanel *panel =
|
|
14039
|
-
if (opts->title && opts->title_len > 0) {
|
|
14040
|
-
panel.title = [[NSString alloc] initWithBytes:opts->title length:opts->title_len encoding:NSUTF8StringEncoding];
|
|
14041
|
-
}
|
|
14042
|
-
if (opts->default_path && opts->default_path_len > 0) {
|
|
14043
|
-
NSString *path = [[NSString alloc] initWithBytes:opts->default_path length:opts->default_path_len encoding:NSUTF8StringEncoding];
|
|
14044
|
-
panel.directoryURL = [NSURL fileURLWithPath:path];
|
|
14045
|
-
}
|
|
14046
|
-
if (opts->default_name && opts->default_name_len > 0) {
|
|
14047
|
-
panel.nameFieldStringValue = [[NSString alloc] initWithBytes:opts->default_name length:opts->default_name_len encoding:NSUTF8StringEncoding];
|
|
14048
|
-
}
|
|
14049
|
-
NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
|
|
14050
|
-
|
|
14495
|
+
NSSavePanel *panel = NativeSdkMakeSavePanel(opts);
|
|
14051
14496
|
if ([panel runModal] != NSModalResponseOK) return 0;
|
|
14052
14497
|
|
|
14053
14498
|
NSString *path = panel.URL.path;
|
|
@@ -14063,45 +14508,200 @@ size_t native_sdk_appkit_show_save_dialog(native_sdk_appkit_host_t *host, const
|
|
|
14063
14508
|
int native_sdk_appkit_show_message_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_message_dialog_opts_t *opts) {
|
|
14064
14509
|
(void)host;
|
|
14065
14510
|
@autoreleasepool {
|
|
14066
|
-
NSAlert *alert =
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14511
|
+
NSAlert *alert = NativeSdkMakeAlert(opts);
|
|
14512
|
+
NSModalResponse response = [alert runModal];
|
|
14513
|
+
if (response == NSAlertFirstButtonReturn) return 0;
|
|
14514
|
+
if (response == NSAlertSecondButtonReturn) return 1;
|
|
14515
|
+
return 2;
|
|
14516
|
+
}
|
|
14517
|
+
}
|
|
14518
|
+
|
|
14519
|
+
/* Record a dialog's result and wake the loop; the runtime collects it with
|
|
14520
|
+
* poll. Never runs app code: AppKit calls completion handlers inside its
|
|
14521
|
+
* own transaction (electron#26884), so the result waits for the wake. */
|
|
14522
|
+
static void NativeSdkFinishDialog(NativeSdkAppKitHost *host, NativeSdkDialog *dialog, NSModalResponse response) {
|
|
14523
|
+
if (!host) return;
|
|
14524
|
+
NSNumber *key = @(dialog.dialogId);
|
|
14525
|
+
if (host.dialogs[key] != dialog) return;
|
|
14526
|
+
[host.dialogs removeObjectForKey:key];
|
|
14527
|
+
dialog.outcome = NATIVE_SDK_APPKIT_DIALOG_CANCELLED;
|
|
14528
|
+
if (dialog.alert) {
|
|
14529
|
+
if (response >= NSAlertFirstButtonReturn && response <= NSAlertThirdButtonReturn) {
|
|
14530
|
+
dialog.outcome = NATIVE_SDK_APPKIT_DIALOG_SELECTED;
|
|
14531
|
+
dialog.button = (int)(response - NSAlertFirstButtonReturn);
|
|
14532
|
+
}
|
|
14533
|
+
} else if (response == NSModalResponseOK) {
|
|
14534
|
+
NSMutableArray<NSString *> *paths = [NSMutableArray array];
|
|
14535
|
+
if ([dialog.panel isKindOfClass:[NSOpenPanel class]]) {
|
|
14536
|
+
for (NSURL *url in ((NSOpenPanel *)dialog.panel).URLs) {
|
|
14537
|
+
if (url.path) [paths addObject:url.path];
|
|
14538
|
+
}
|
|
14539
|
+
} else if (dialog.panel.URL.path) {
|
|
14540
|
+
[paths addObject:dialog.panel.URL.path];
|
|
14541
|
+
}
|
|
14542
|
+
dialog.paths = paths;
|
|
14543
|
+
if (paths.count > 0) dialog.outcome = NATIVE_SDK_APPKIT_DIALOG_SELECTED;
|
|
14544
|
+
}
|
|
14545
|
+
dialog.panel = nil;
|
|
14546
|
+
dialog.alert = nil;
|
|
14547
|
+
if (host.didShutdown) return;
|
|
14548
|
+
[host.finishedDialogs addObject:dialog];
|
|
14549
|
+
[host wakeFromAnyThread];
|
|
14550
|
+
}
|
|
14551
|
+
|
|
14552
|
+
static void NativeSdkPresentDialog(NativeSdkAppKitHost *host, NativeSdkDialog *dialog, NSWindow *parent) {
|
|
14553
|
+
__weak NativeSdkAppKitHost *weakHost = host;
|
|
14554
|
+
if (dialog.panel) {
|
|
14555
|
+
void (^done)(NSModalResponse) = ^(NSModalResponse response) {
|
|
14556
|
+
NativeSdkFinishDialog(weakHost, dialog, response);
|
|
14557
|
+
};
|
|
14558
|
+
if (parent) {
|
|
14559
|
+
dialog.sheetParent = parent;
|
|
14560
|
+
[dialog.panel beginSheetModalForWindow:parent completionHandler:done];
|
|
14088
14561
|
} else {
|
|
14089
|
-
[
|
|
14562
|
+
[dialog.panel beginWithCompletionHandler:done];
|
|
14090
14563
|
}
|
|
14091
|
-
|
|
14092
|
-
|
|
14564
|
+
return;
|
|
14565
|
+
}
|
|
14566
|
+
if (parent) {
|
|
14567
|
+
dialog.sheetParent = parent;
|
|
14568
|
+
[dialog.alert beginSheetModalForWindow:parent completionHandler:^(NSModalResponse response) {
|
|
14569
|
+
NativeSdkFinishDialog(weakHost, dialog, response);
|
|
14570
|
+
}];
|
|
14571
|
+
return;
|
|
14572
|
+
}
|
|
14573
|
+
// NSAlert has no modeless form. With no window to attach a sheet to,
|
|
14574
|
+
// run it modally from a fresh loop turn, outside any runtime
|
|
14575
|
+
// dispatch, so events keep reaching the app meanwhile (Electron runs
|
|
14576
|
+
// a parentless message box modally too). A run loop block, not
|
|
14577
|
+
// dispatch_async: a modal loop inside a main-queue block keeps the
|
|
14578
|
+
// serial main queue from draining, and wakes and quits ride it.
|
|
14579
|
+
CFRunLoopRef mainLoop = CFRunLoopGetMain();
|
|
14580
|
+
CFRunLoopPerformBlock(mainLoop, kCFRunLoopCommonModes, ^{
|
|
14581
|
+
NativeSdkAppKitHost *strongHost = weakHost;
|
|
14582
|
+
if (!strongHost || dialog.cancelRequested || strongHost.dialogs[@(dialog.dialogId)] != dialog) {
|
|
14583
|
+
NativeSdkFinishDialog(strongHost, dialog, NSModalResponseCancel);
|
|
14584
|
+
return;
|
|
14093
14585
|
}
|
|
14094
|
-
|
|
14095
|
-
|
|
14586
|
+
dialog.modalRunning = YES;
|
|
14587
|
+
const NSModalResponse response = [dialog.alert runModal];
|
|
14588
|
+
dialog.modalRunning = NO;
|
|
14589
|
+
NativeSdkFinishDialog(strongHost, dialog, response);
|
|
14590
|
+
// A stop during the alert ended only the modal loop; [NSApp stop:]
|
|
14591
|
+
// inside a modal session never reaches the main one.
|
|
14592
|
+
if (strongHost.didShutdown) {
|
|
14593
|
+
[NSApp stop:nil];
|
|
14594
|
+
NativeSdkPostEmptyEvent();
|
|
14096
14595
|
}
|
|
14596
|
+
});
|
|
14597
|
+
CFRunLoopWakeUp(mainLoop);
|
|
14598
|
+
}
|
|
14097
14599
|
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14600
|
+
static void NativeSdkCancelDialog(NativeSdkDialog *dialog) {
|
|
14601
|
+
NSWindow *sheet = dialog.panel ?: dialog.alert.window;
|
|
14602
|
+
if (dialog.sheetParent && sheet) {
|
|
14603
|
+
// Ending the sheet on its window detaches it at once, so a close
|
|
14604
|
+
// right after is not refused for a sheet still attached.
|
|
14605
|
+
[dialog.sheetParent endSheet:sheet returnCode:NSModalResponseCancel];
|
|
14606
|
+
return;
|
|
14607
|
+
}
|
|
14608
|
+
if (dialog.panel) {
|
|
14609
|
+
[dialog.panel cancel:nil];
|
|
14610
|
+
return;
|
|
14611
|
+
}
|
|
14612
|
+
if (!dialog.alert) return;
|
|
14613
|
+
if (dialog.modalRunning) {
|
|
14614
|
+
[NSApp stopModalWithCode:NSModalResponseCancel];
|
|
14615
|
+
NativeSdkPostEmptyEvent();
|
|
14616
|
+
} else {
|
|
14617
|
+
// Not started yet: the deferred presentation finishes it instead.
|
|
14618
|
+
dialog.cancelRequested = YES;
|
|
14619
|
+
}
|
|
14620
|
+
}
|
|
14621
|
+
|
|
14622
|
+
static void NativeSdkCancelDialogsForWindow(NativeSdkAppKitHost *host, uint64_t windowId) {
|
|
14623
|
+
for (NativeSdkDialog *dialog in [host.dialogs.allValues copy]) {
|
|
14624
|
+
if (dialog.parentWindowId == windowId) NativeSdkCancelDialog(dialog);
|
|
14102
14625
|
}
|
|
14103
14626
|
}
|
|
14104
14627
|
|
|
14628
|
+
static void NativeSdkCancelAllDialogs(NativeSdkAppKitHost *host) {
|
|
14629
|
+
for (NativeSdkDialog *dialog in [host.dialogs.allValues copy]) NativeSdkCancelDialog(dialog);
|
|
14630
|
+
[host.finishedDialogs removeAllObjects];
|
|
14631
|
+
}
|
|
14632
|
+
|
|
14633
|
+
int native_sdk_appkit_request_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_dialog_request_t *request) {
|
|
14634
|
+
NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
|
|
14635
|
+
if (!object || !request || object.didShutdown || request->id == 0) return 0;
|
|
14636
|
+
@autoreleasepool {
|
|
14637
|
+
NativeSdkDialog *dialog = [[NativeSdkDialog alloc] init];
|
|
14638
|
+
dialog.dialogId = request->id;
|
|
14639
|
+
dialog.parentWindowId = request->parent_window_id;
|
|
14640
|
+
switch (request->kind) {
|
|
14641
|
+
case NATIVE_SDK_APPKIT_DIALOG_OPEN:
|
|
14642
|
+
if (!request->open_opts) return 0;
|
|
14643
|
+
dialog.panel = NativeSdkMakeOpenPanel(request->open_opts);
|
|
14644
|
+
break;
|
|
14645
|
+
case NATIVE_SDK_APPKIT_DIALOG_SAVE:
|
|
14646
|
+
if (!request->save_opts) return 0;
|
|
14647
|
+
dialog.panel = NativeSdkMakeSavePanel(request->save_opts);
|
|
14648
|
+
break;
|
|
14649
|
+
case NATIVE_SDK_APPKIT_DIALOG_MESSAGE:
|
|
14650
|
+
if (!request->message_opts) return 0;
|
|
14651
|
+
dialog.alert = NativeSdkMakeAlert(request->message_opts);
|
|
14652
|
+
break;
|
|
14653
|
+
default:
|
|
14654
|
+
return 0;
|
|
14655
|
+
}
|
|
14656
|
+
// A parent that cannot host a sheet right now (hidden, minimized,
|
|
14657
|
+
// already showing one) gets a modeless panel instead, as Electron
|
|
14658
|
+
// does for invisible parents (electron#22671).
|
|
14659
|
+
NSWindow *parent = request->parent_window_id != 0 ? object.windows[@(request->parent_window_id)] : nil;
|
|
14660
|
+
if (parent && (!parent.isVisible || parent.isMiniaturized || parent.attachedSheet)) parent = nil;
|
|
14661
|
+
if (!parent) dialog.parentWindowId = 0;
|
|
14662
|
+
object.dialogs[@(dialog.dialogId)] = dialog;
|
|
14663
|
+
NativeSdkPresentDialog(object, dialog, parent);
|
|
14664
|
+
return 1;
|
|
14665
|
+
}
|
|
14666
|
+
}
|
|
14667
|
+
|
|
14668
|
+
void native_sdk_appkit_cancel_dialog(native_sdk_appkit_host_t *host, uint64_t id) {
|
|
14669
|
+
NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
|
|
14670
|
+
if (!object) return;
|
|
14671
|
+
NativeSdkDialog *dialog = object.dialogs[@(id)];
|
|
14672
|
+
if (dialog) NativeSdkCancelDialog(dialog);
|
|
14673
|
+
}
|
|
14674
|
+
|
|
14675
|
+
int native_sdk_appkit_poll_dialog_result(native_sdk_appkit_host_t *host, char *buffer, size_t buffer_len, native_sdk_appkit_dialog_result_t *result) {
|
|
14676
|
+
NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
|
|
14677
|
+
if (!object || !result || object.finishedDialogs.count == 0) return 0;
|
|
14678
|
+
NativeSdkDialog *dialog = object.finishedDialogs.firstObject;
|
|
14679
|
+
[object.finishedDialogs removeObjectAtIndex:0];
|
|
14680
|
+
*result = (native_sdk_appkit_dialog_result_t){ .id = dialog.dialogId, .outcome = dialog.outcome, .button = dialog.button };
|
|
14681
|
+
size_t offset = 0;
|
|
14682
|
+
size_t count = 0;
|
|
14683
|
+
for (NSString *path in dialog.paths) {
|
|
14684
|
+
NSData *data = [path dataUsingEncoding:NSUTF8StringEncoding];
|
|
14685
|
+
if (!data) continue;
|
|
14686
|
+
const size_t needed = data.length + (count > 0 ? 1 : 0);
|
|
14687
|
+
if (!buffer || needed > buffer_len - offset) {
|
|
14688
|
+
// More paths than the runtime can hold: report failure rather
|
|
14689
|
+
// than a silently shortened selection.
|
|
14690
|
+
result->outcome = NATIVE_SDK_APPKIT_DIALOG_FAILED;
|
|
14691
|
+
offset = 0;
|
|
14692
|
+
count = 0;
|
|
14693
|
+
break;
|
|
14694
|
+
}
|
|
14695
|
+
if (count > 0) buffer[offset++] = '\n';
|
|
14696
|
+
memcpy(buffer + offset, data.bytes, data.length);
|
|
14697
|
+
offset += data.length;
|
|
14698
|
+
count++;
|
|
14699
|
+
}
|
|
14700
|
+
result->count = count;
|
|
14701
|
+
result->bytes_written = offset;
|
|
14702
|
+
return 1;
|
|
14703
|
+
}
|
|
14704
|
+
|
|
14105
14705
|
static NSImage *NativeSdkTrayImageWithOpacity(NSImage *source, double opacity) {
|
|
14106
14706
|
if (!source) return nil;
|
|
14107
14707
|
const CGFloat fraction = MAX(0.0, MIN(1.0, opacity));
|