@appium/coresim 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +66 -0
  3. package/binding.gyp +53 -0
  4. package/lib/src/commands/app.d.ts +88 -0
  5. package/lib/src/commands/app.d.ts.map +1 -0
  6. package/lib/src/commands/app.js +133 -0
  7. package/lib/src/commands/app.js.map +1 -0
  8. package/lib/src/commands/biometric.d.ts +33 -0
  9. package/lib/src/commands/biometric.d.ts.map +1 -0
  10. package/lib/src/commands/biometric.js +44 -0
  11. package/lib/src/commands/biometric.js.map +1 -0
  12. package/lib/src/commands/darwin-notification.d.ts +36 -0
  13. package/lib/src/commands/darwin-notification.d.ts.map +1 -0
  14. package/lib/src/commands/darwin-notification.js +35 -0
  15. package/lib/src/commands/darwin-notification.js.map +1 -0
  16. package/lib/src/commands/interaction.d.ts +48 -0
  17. package/lib/src/commands/interaction.d.ts.map +1 -0
  18. package/lib/src/commands/interaction.js +48 -0
  19. package/lib/src/commands/interaction.js.map +1 -0
  20. package/lib/src/commands/keychain.d.ts +25 -0
  21. package/lib/src/commands/keychain.d.ts.map +1 -0
  22. package/lib/src/commands/keychain.js +52 -0
  23. package/lib/src/commands/keychain.js.map +1 -0
  24. package/lib/src/commands/lifecycle.d.ts +101 -0
  25. package/lib/src/commands/lifecycle.d.ts.map +1 -0
  26. package/lib/src/commands/lifecycle.js +165 -0
  27. package/lib/src/commands/lifecycle.js.map +1 -0
  28. package/lib/src/commands/media.d.ts +28 -0
  29. package/lib/src/commands/media.d.ts.map +1 -0
  30. package/lib/src/commands/media.js +27 -0
  31. package/lib/src/commands/media.js.map +1 -0
  32. package/lib/src/commands/misc.d.ts +15 -0
  33. package/lib/src/commands/misc.d.ts.map +1 -0
  34. package/lib/src/commands/misc.js +12 -0
  35. package/lib/src/commands/misc.js.map +1 -0
  36. package/lib/src/commands/pasteboard.d.ts +24 -0
  37. package/lib/src/commands/pasteboard.d.ts.map +1 -0
  38. package/lib/src/commands/pasteboard.js +22 -0
  39. package/lib/src/commands/pasteboard.js.map +1 -0
  40. package/lib/src/commands/permissions.d.ts +48 -0
  41. package/lib/src/commands/permissions.d.ts.map +1 -0
  42. package/lib/src/commands/permissions.js +84 -0
  43. package/lib/src/commands/permissions.js.map +1 -0
  44. package/lib/src/commands/process.d.ts +16 -0
  45. package/lib/src/commands/process.d.ts.map +1 -0
  46. package/lib/src/commands/process.js +68 -0
  47. package/lib/src/commands/process.js.map +1 -0
  48. package/lib/src/commands/screenshot.d.ts +29 -0
  49. package/lib/src/commands/screenshot.d.ts.map +1 -0
  50. package/lib/src/commands/screenshot.js +30 -0
  51. package/lib/src/commands/screenshot.js.map +1 -0
  52. package/lib/src/commands/spawn.d.ts +48 -0
  53. package/lib/src/commands/spawn.d.ts.map +1 -0
  54. package/lib/src/commands/spawn.js +108 -0
  55. package/lib/src/commands/spawn.js.map +1 -0
  56. package/lib/src/commands/ui.d.ts +42 -0
  57. package/lib/src/commands/ui.d.ts.map +1 -0
  58. package/lib/src/commands/ui.js +44 -0
  59. package/lib/src/commands/ui.js.map +1 -0
  60. package/lib/src/commands/webinspector.d.ts +19 -0
  61. package/lib/src/commands/webinspector.d.ts.map +1 -0
  62. package/lib/src/commands/webinspector.js +16 -0
  63. package/lib/src/commands/webinspector.js.map +1 -0
  64. package/lib/src/errors.d.ts +51 -0
  65. package/lib/src/errors.d.ts.map +1 -0
  66. package/lib/src/errors.js +82 -0
  67. package/lib/src/errors.js.map +1 -0
  68. package/lib/src/index.d.ts +7 -0
  69. package/lib/src/index.d.ts.map +1 -0
  70. package/lib/src/index.js +5 -0
  71. package/lib/src/index.js.map +1 -0
  72. package/lib/src/native-simctl.d.ts +62 -0
  73. package/lib/src/native-simctl.d.ts.map +1 -0
  74. package/lib/src/native-simctl.js +221 -0
  75. package/lib/src/native-simctl.js.map +1 -0
  76. package/lib/src/types.d.ts +267 -0
  77. package/lib/src/types.d.ts.map +1 -0
  78. package/lib/src/types.js +29 -0
  79. package/lib/src/types.js.map +1 -0
  80. package/lib/src/utils/index.d.ts +3 -0
  81. package/lib/src/utils/index.d.ts.map +1 -0
  82. package/lib/src/utils/index.js +3 -0
  83. package/lib/src/utils/index.js.map +1 -0
  84. package/lib/src/utils/pkg-root.d.ts +6 -0
  85. package/lib/src/utils/pkg-root.d.ts.map +1 -0
  86. package/lib/src/utils/pkg-root.js +12 -0
  87. package/lib/src/utils/pkg-root.js.map +1 -0
  88. package/lib/src/utils/run-catching.d.ts +3 -0
  89. package/lib/src/utils/run-catching.d.ts.map +1 -0
  90. package/lib/src/utils/run-catching.js +11 -0
  91. package/lib/src/utils/run-catching.js.map +1 -0
  92. package/package.json +73 -0
  93. package/scripts/install.mjs +22 -0
  94. package/src/commands/app.ts +186 -0
  95. package/src/commands/biometric.ts +69 -0
  96. package/src/commands/darwin-notification.ts +51 -0
  97. package/src/commands/interaction.ts +74 -0
  98. package/src/commands/keychain.ts +63 -0
  99. package/src/commands/lifecycle.ts +210 -0
  100. package/src/commands/media.ts +38 -0
  101. package/src/commands/misc.ts +20 -0
  102. package/src/commands/pasteboard.ts +31 -0
  103. package/src/commands/permissions.ts +122 -0
  104. package/src/commands/process.ts +80 -0
  105. package/src/commands/screenshot.ts +46 -0
  106. package/src/commands/spawn.ts +133 -0
  107. package/src/commands/ui.ts +61 -0
  108. package/src/commands/webinspector.ts +23 -0
  109. package/src/coresim.mm +1066 -0
  110. package/src/errors.ts +93 -0
  111. package/src/index.ts +23 -0
  112. package/src/native/async_bridge.h +164 -0
  113. package/src/native/nserror_bridge.h +64 -0
  114. package/src/native/nserror_bridge.mm +30 -0
  115. package/src/native/objc_runtime.h +45 -0
  116. package/src/native/objc_runtime.mm +76 -0
  117. package/src/native/safe_dispatch.h +38 -0
  118. package/src/native/sim_device.h +143 -0
  119. package/src/native/sim_device.mm +355 -0
  120. package/src/native/sim_device_set.h +16 -0
  121. package/src/native/sim_device_set.mm +40 -0
  122. package/src/native/sim_pasteboard.h +16 -0
  123. package/src/native/sim_pasteboard.mm +295 -0
  124. package/src/native/sim_process.h +13 -0
  125. package/src/native/sim_process.mm +139 -0
  126. package/src/native/sim_screenshot.h +28 -0
  127. package/src/native/sim_screenshot.mm +226 -0
  128. package/src/native/sim_service_context.h +42 -0
  129. package/src/native/sim_service_context.mm +92 -0
  130. package/src/native/tcc_privacy.h +35 -0
  131. package/src/native/tcc_privacy.mm +256 -0
  132. package/src/native/value_bridge.h +18 -0
  133. package/src/native/value_bridge.mm +111 -0
  134. package/src/native-simctl.ts +288 -0
  135. package/src/types.ts +302 -0
  136. package/src/utils/index.ts +2 -0
  137. package/src/utils/pkg-root.ts +14 -0
  138. package/src/utils/run-catching.ts +10 -0
@@ -0,0 +1,226 @@
1
+ #include "sim_screenshot.h"
2
+
3
+ #import <CoreImage/CoreImage.h>
4
+ #import <IOSurface/IOSurface.h>
5
+ #import <ImageIO/ImageIO.h>
6
+ #import <objc/message.h>
7
+
8
+ #include "objc_runtime.h"
9
+ #include "safe_dispatch.h"
10
+
11
+ namespace coresim {
12
+
13
+ namespace {
14
+
15
+ NSString* const kScreenshotErrorDomain = @"com.appium.coresim.Screenshot";
16
+
17
+ NSError* MakeError(NSInteger code, NSString* message) {
18
+ return [NSError errorWithDomain:kScreenshotErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
19
+ }
20
+
21
+ id IdGetter(id target, const std::string& selectorName) {
22
+ RequireSelector(target, selectorName);
23
+ SEL selector = SelectorNamed(selectorName);
24
+ return SafeInvoke([&] {
25
+ using Fn = id (*)(id, SEL);
26
+ return ((Fn)objc_msgSend)(target, selector);
27
+ });
28
+ }
29
+
30
+ // -[<SimDisplayDescriptorState> displayClass] — iOS's main display is class 0; tvOS renders only
31
+ // on the (non-zero) TVOut class.
32
+ unsigned short DisplayClass(id descriptorState) {
33
+ static const std::string kSelectorName = "displayClass";
34
+ RequireSelector(descriptorState, kSelectorName);
35
+ SEL selector = SelectorNamed(kSelectorName);
36
+ return SafeInvoke([&] {
37
+ using Fn = unsigned short (*)(id, SEL);
38
+ return ((Fn)objc_msgSend)(descriptorState, selector);
39
+ });
40
+ }
41
+
42
+ // One renderable display IO port found among -[SimDeviceIOClient ioPorts]. Keeps "descriptor"
43
+ // alongside the JS-facing "id"/"displayClass" fields so CaptureScreenshot can resolve a requested
44
+ // displayId against the same list ListDisplays reports, without walking ioPorts twice.
45
+ NSString* const kCandidateUUIDKey = @"id";
46
+ NSString* const kCandidateDisplayClassKey = @"displayClass";
47
+ NSString* const kCandidateDescriptorKey = @"descriptor";
48
+
49
+ // Every IO port that actually renders a screen, in -[SimDeviceIOClient ioPorts]'s own order. Only
50
+ // a port conforming to the (private, headerless) SimDisplayIOSurfaceRenderable/SimDisplayRenderable
51
+ // protocols carries a "displayClass" or either surface accessor — other ports (audio, the
52
+ // screenshot mach service itself) don't, so the respondsToSelector: checks below are the actual
53
+ // filter, not defensive leftovers. Returns nil (and sets *error) only if the device has no IO
54
+ // client at all (e.g. not booted); no renderable port is an empty array, not an error — the caller
55
+ // decides whether that's fatal.
56
+ NSArray<NSDictionary*>* RenderableDisplayCandidates(id device, NSError** error) {
57
+ id ioClient = IdGetter(device, "io");
58
+ if (ioClient == nil) {
59
+ *error = MakeError(1, @"Device has no IO client available — is it booted?");
60
+ return nil;
61
+ }
62
+
63
+ NSArray* ports = IdGetter(ioClient, "ioPorts");
64
+ NSMutableArray<NSDictionary*>* candidates = [NSMutableArray array];
65
+ for (id port in ports) {
66
+ id descriptor = IdGetter(port, "descriptor");
67
+ if (descriptor == nil) continue;
68
+ if (![descriptor respondsToSelector:NSSelectorFromString(@"framebufferSurface")] &&
69
+ ![descriptor respondsToSelector:NSSelectorFromString(@"ioSurface")]) {
70
+ continue;
71
+ }
72
+ if (![descriptor respondsToSelector:NSSelectorFromString(@"state")]) {
73
+ continue;
74
+ }
75
+ id state = IdGetter(descriptor, "state");
76
+ if (state == nil || ![state respondsToSelector:NSSelectorFromString(@"displayClass")]) {
77
+ continue;
78
+ }
79
+ NSUUID* uuid = IdGetter(port, "uuid");
80
+ [candidates addObject:@{
81
+ kCandidateUUIDKey : uuid ? uuid.UUIDString : @"",
82
+ kCandidateDisplayClassKey : @(DisplayClass(state)),
83
+ kCandidateDescriptorKey : descriptor,
84
+ }];
85
+ }
86
+ return candidates;
87
+ }
88
+
89
+ // Picks which candidate CaptureScreenshot should read from: the one matching `displayId` if given
90
+ // (an error if none match), else the primary display (displayClass 0), falling back to the first
91
+ // renderable display found so a target with no class-0 display (e.g. tvOS) still gets a
92
+ // screenshot instead of an outright failure.
93
+ id ResolveDisplayDescriptor(NSArray<NSDictionary*>* candidates, NSString* displayId, NSError** error) {
94
+ if (displayId != nil) {
95
+ for (NSDictionary* candidate in candidates) {
96
+ if ([candidate[kCandidateUUIDKey] isEqualToString:displayId]) {
97
+ return candidate[kCandidateDescriptorKey];
98
+ }
99
+ }
100
+ *error = MakeError(2, [NSString stringWithFormat:@"No display with id '%@' was found on this device", displayId]);
101
+ return nil;
102
+ }
103
+ id fallback = nil;
104
+ for (NSDictionary* candidate in candidates) {
105
+ if ([candidate[kCandidateDisplayClassKey] unsignedShortValue] == 0) {
106
+ return candidate[kCandidateDescriptorKey];
107
+ }
108
+ if (fallback == nil) {
109
+ fallback = candidate[kCandidateDescriptorKey];
110
+ }
111
+ }
112
+ if (fallback == nil) {
113
+ *error = MakeError(3, @"No renderable display port was found on this device");
114
+ }
115
+ return fallback;
116
+ }
117
+
118
+ // Unlike IdGetter, doesn't RequireSelector: a display proxy that has lost its connection (e.g. the
119
+ // device just shut down) can legitimately stop responding to a selector it answered a moment ago —
120
+ // that's "no surface right now", not "unsupported on this CoreSimulator", so it must not raise
121
+ // NativeSimUnavailableError the way a genuinely missing selector should.
122
+ id OptionalIdGetter(id target, NSString* selectorName) {
123
+ SEL selector = NSSelectorFromString(selectorName);
124
+ if (![target respondsToSelector:selector]) {
125
+ return nil;
126
+ }
127
+ return SafeInvoke([&] {
128
+ using Fn = id (*)(id, SEL);
129
+ return ((Fn)objc_msgSend)(target, selector);
130
+ });
131
+ }
132
+
133
+ // `framebufferSurface` is the primary surface since Xcode 13.2 split what used to be a single
134
+ // `ioSurface`; both are real (non-optional) members of the descriptor's protocol once it's passed
135
+ // RenderableDisplayCandidates' filter above, but the underlying remote proxy can still legitimately
136
+ // vend nil for either — or stop responding entirely if the connection just dropped — so both are
137
+ // tried before giving up.
138
+ id RenderableSurface(id descriptor) {
139
+ return OptionalIdGetter(descriptor, @"framebufferSurface") ?: OptionalIdGetter(descriptor, @"ioSurface");
140
+ }
141
+
142
+ NSString* const kPNGUTI = @"public.png";
143
+ NSString* const kJPEGUTI = @"public.jpeg";
144
+
145
+ } // namespace
146
+
147
+ NSArray<NSDictionary*>* ListDisplays(id device, NSError** error) {
148
+ NSArray<NSDictionary*>* candidates = RenderableDisplayCandidates(device, error);
149
+ if (candidates == nil) {
150
+ return nil;
151
+ }
152
+ NSMutableArray<NSDictionary*>* result = [NSMutableArray arrayWithCapacity:candidates.count];
153
+ for (NSDictionary* candidate in candidates) {
154
+ unsigned short displayClass = [candidate[kCandidateDisplayClassKey] unsignedShortValue];
155
+ [result addObject:@{
156
+ @"id" : candidate[kCandidateUUIDKey],
157
+ @"displayClass" : candidate[kCandidateDisplayClassKey],
158
+ @"isMain" : @(displayClass == 0),
159
+ }];
160
+ }
161
+ return result;
162
+ }
163
+
164
+ NSData* CaptureScreenshot(id device, NSString* displayId, ScreenshotFormat format, NSNumber* jpegQualityPercent,
165
+ NSError** error) {
166
+ NSArray<NSDictionary*>* candidates = RenderableDisplayCandidates(device, error);
167
+ if (candidates == nil) {
168
+ return nil;
169
+ }
170
+
171
+ id descriptor = ResolveDisplayDescriptor(candidates, displayId, error);
172
+ if (descriptor == nil) {
173
+ return nil;
174
+ }
175
+
176
+ id surfaceObj = RenderableSurface(descriptor);
177
+ if (surfaceObj == nil) {
178
+ *error = MakeError(4, @"The device's display surface is not available yet");
179
+ return nil;
180
+ }
181
+
182
+ IOSurfaceRef surfaceRef = (__bridge IOSurfaceRef)surfaceObj;
183
+ CIImage* ciImage = [CIImage imageWithIOSurface:surfaceRef];
184
+ if (ciImage == nil) {
185
+ *error = MakeError(5, @"Failed to wrap the device's display surface as an image");
186
+ return nil;
187
+ }
188
+
189
+ // A fresh CIContext per call, matching this operation's one-shot semantics (mirrors
190
+ // simctl's own screenshot command) rather than the persistent, reused context a
191
+ // continuous video/streaming path would want.
192
+ CIContext* context = [CIContext contextWithOptions:nil];
193
+ CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
194
+ if (cgImage == nil) {
195
+ *error = MakeError(6, @"Failed to render the device's display surface");
196
+ return nil;
197
+ }
198
+
199
+ NSString* uti = format == ScreenshotFormat::kJPEG ? kJPEGUTI : kPNGUTI;
200
+ NSMutableData* imageData = [NSMutableData data];
201
+ CGImageDestinationRef destination =
202
+ CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, (__bridge CFStringRef)uti, 1, NULL);
203
+ if (destination == nullptr) {
204
+ CGImageRelease(cgImage);
205
+ *error = MakeError(7, @"Failed to create an image encoder for the captured screenshot");
206
+ return nil;
207
+ }
208
+ // kCGImageDestinationLossyCompressionQuality is meaningless for PNG (always lossless) — ImageIO
209
+ // silently ignores properties a format doesn't use, so this is only gated on jpegQualityPercent
210
+ // being present, not on `format` too.
211
+ NSDictionary* properties =
212
+ jpegQualityPercent != nil
213
+ ? @{(NSString*)kCGImageDestinationLossyCompressionQuality : @(jpegQualityPercent.doubleValue / 100.0)}
214
+ : nil;
215
+ CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
216
+ BOOL ok = CGImageDestinationFinalize(destination);
217
+ CFRelease(destination);
218
+ CGImageRelease(cgImage);
219
+ if (!ok) {
220
+ *error = MakeError(8, @"Failed to encode the captured screenshot");
221
+ return nil;
222
+ }
223
+ return imageData;
224
+ }
225
+
226
+ } // namespace coresim
@@ -0,0 +1,42 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ namespace coresim {
6
+
7
+ // +[SimServiceContext sharedServiceContextForDeveloperDir:error:]. Returns nil (with *error set)
8
+ // on failure. `developerDir` is the "which Xcode" selector — the actual physical CoreSimulator
9
+ // binary loaded is always the one shared system-wide copy; this only picks which runtimes/device
10
+ // types/behaviors that shared framework resolves against.
11
+ id SharedServiceContext(NSString* developerDir, NSError** error);
12
+
13
+ // -[SimServiceContext defaultDeviceSetWithError:]
14
+ id DefaultDeviceSet(id serviceContext, NSError** error);
15
+
16
+ // -[SimServiceContext supportedDeviceTypes] -> NSArray<SimDeviceType*>
17
+ NSArray* SupportedDeviceTypes(id serviceContext);
18
+
19
+ // -[SimServiceContext supportedRuntimes] -> NSArray<SimRuntime*>
20
+ NSArray* SupportedRuntimes(id serviceContext);
21
+
22
+ // -[SimServiceContext supportedDeviceTypesByIdentifier] -> NSDictionary<NSString*, SimDeviceType*>
23
+ NSDictionary* SupportedDeviceTypesByIdentifier(id serviceContext);
24
+
25
+ // -[SimServiceContext supportedRuntimesByIdentifier] -> NSDictionary<NSString*, SimRuntime*>
26
+ NSDictionary* SupportedRuntimesByIdentifier(id serviceContext);
27
+
28
+ // -[SimDeviceType identifier] / -[SimDeviceType name]
29
+ NSString* DeviceTypeIdentifier(id deviceType);
30
+ NSString* DeviceTypeName(id deviceType);
31
+
32
+ // -[SimRuntime identifier] / -[SimRuntime name] / -[SimRuntime versionString]
33
+ NSString* RuntimeIdentifier(id runtime);
34
+ NSString* RuntimeName(id runtime);
35
+ NSString* RuntimeVersionString(id runtime);
36
+
37
+ // -[SimRuntime root] -> the runtime bundle's `RuntimeRoot` directory, same as a spawned process's
38
+ // own `$SIMULATOR_ROOT`. Used to resolve guest-OS-specific executables (e.g. `launchctl` — see
39
+ // CLAUDE.md) before spawning them, since SimDevice's spawn API takes a literal path, no PATH search.
40
+ NSString* RuntimeRootPath(id runtime);
41
+
42
+ } // namespace coresim
@@ -0,0 +1,92 @@
1
+ #include "sim_service_context.h"
2
+
3
+ #import <objc/message.h>
4
+
5
+ #include "objc_runtime.h"
6
+ #include "safe_dispatch.h"
7
+
8
+ namespace coresim {
9
+
10
+ id SharedServiceContext(NSString* developerDir, NSError** error) {
11
+ Class cls = RequireClass("SimServiceContext");
12
+ static const std::string kSelectorName = "sharedServiceContextForDeveloperDir:error:";
13
+ RequireClassSelector(cls, kSelectorName);
14
+ SEL selector = SelectorNamed(kSelectorName);
15
+ return SafeInvoke([&] {
16
+ using Fn = id (*)(Class, SEL, NSString*, NSError**);
17
+ return ((Fn)objc_msgSend)(cls, selector, developerDir, error);
18
+ });
19
+ }
20
+
21
+ id DefaultDeviceSet(id serviceContext, NSError** error) {
22
+ static const std::string kSelectorName = "defaultDeviceSetWithError:";
23
+ RequireSelector(serviceContext, kSelectorName);
24
+ SEL selector = SelectorNamed(kSelectorName);
25
+ return SafeInvoke([&] {
26
+ using Fn = id (*)(id, SEL, NSError**);
27
+ return ((Fn)objc_msgSend)(serviceContext, selector, error);
28
+ });
29
+ }
30
+
31
+ NSArray* SupportedDeviceTypes(id serviceContext) {
32
+ static const std::string kSelectorName = "supportedDeviceTypes";
33
+ RequireSelector(serviceContext, kSelectorName);
34
+ SEL selector = SelectorNamed(kSelectorName);
35
+ return SafeInvoke([&] {
36
+ using Fn = NSArray* (*)(id, SEL);
37
+ return ((Fn)objc_msgSend)(serviceContext, selector);
38
+ });
39
+ }
40
+
41
+ NSArray* SupportedRuntimes(id serviceContext) {
42
+ static const std::string kSelectorName = "supportedRuntimes";
43
+ RequireSelector(serviceContext, kSelectorName);
44
+ SEL selector = SelectorNamed(kSelectorName);
45
+ return SafeInvoke([&] {
46
+ using Fn = NSArray* (*)(id, SEL);
47
+ return ((Fn)objc_msgSend)(serviceContext, selector);
48
+ });
49
+ }
50
+
51
+ namespace {
52
+
53
+ // Shared shape for every zero-arg id-returning getter used below (identifier/name/versionString).
54
+ NSString* StringGetter(id target, const std::string& selectorName) {
55
+ RequireSelector(target, selectorName);
56
+ SEL selector = SelectorNamed(selectorName);
57
+ return SafeInvoke([&] {
58
+ using Fn = NSString* (*)(id, SEL);
59
+ return ((Fn)objc_msgSend)(target, selector);
60
+ });
61
+ }
62
+
63
+ } // namespace
64
+
65
+ NSDictionary* SupportedDeviceTypesByIdentifier(id serviceContext) {
66
+ static const std::string kSelectorName = "supportedDeviceTypesByIdentifier";
67
+ RequireSelector(serviceContext, kSelectorName);
68
+ SEL selector = SelectorNamed(kSelectorName);
69
+ return SafeInvoke([&] {
70
+ using Fn = NSDictionary* (*)(id, SEL);
71
+ return ((Fn)objc_msgSend)(serviceContext, selector);
72
+ });
73
+ }
74
+
75
+ NSDictionary* SupportedRuntimesByIdentifier(id serviceContext) {
76
+ static const std::string kSelectorName = "supportedRuntimesByIdentifier";
77
+ RequireSelector(serviceContext, kSelectorName);
78
+ SEL selector = SelectorNamed(kSelectorName);
79
+ return SafeInvoke([&] {
80
+ using Fn = NSDictionary* (*)(id, SEL);
81
+ return ((Fn)objc_msgSend)(serviceContext, selector);
82
+ });
83
+ }
84
+
85
+ NSString* DeviceTypeIdentifier(id deviceType) { return StringGetter(deviceType, "identifier"); }
86
+ NSString* DeviceTypeName(id deviceType) { return StringGetter(deviceType, "name"); }
87
+ NSString* RuntimeIdentifier(id runtime) { return StringGetter(runtime, "identifier"); }
88
+ NSString* RuntimeName(id runtime) { return StringGetter(runtime, "name"); }
89
+ NSString* RuntimeVersionString(id runtime) { return StringGetter(runtime, "versionString"); }
90
+ NSString* RuntimeRootPath(id runtime) { return StringGetter(runtime, "root"); }
91
+
92
+ } // namespace coresim
@@ -0,0 +1,35 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ namespace coresim {
6
+
7
+ // Grants or revokes a privacy permission for `bundleId` in the simulator's own TCC (privacy)
8
+ // database, located under the device's data directory (see DeviceDataPath, sim_device.h) at
9
+ // `Library/TCC/TCC.db`. `service` is the raw TCC service identifier (e.g. "kTCCServiceCamera"),
10
+ // not a friendly name — callers map friendly names to these before calling in (see
11
+ // commands/permissions.ts). Fails with a descriptive NSError if the device has never been booted
12
+ // (TCC.db doesn't exist yet) or the database can't be written to.
13
+ BOOL SetTCCAccess(NSString* dataPath, NSString* service, NSString* bundleId, BOOL granted, NSError** error);
14
+
15
+ // Resets a previously granted/revoked permission back to its default (unprompted, "unset") state
16
+ // by deleting its row from TCC.db, if one exists.
17
+ BOOL ResetTCCAccess(NSString* dataPath, NSString* service, NSString* bundleId, NSError** error);
18
+
19
+ // Own status codes GetTCCAccess reports through `outStatus` — deliberately not TCC's own raw wire
20
+ // values (which reuse 0 for "denied", not "no row"), so a missing row and an explicit denial are
21
+ // never confusable. `kTCCAuthLimited` only ever occurs for kTCCServicePhotos's "selected photos"
22
+ // grant — SetTCCAccess never writes it, but a row can still carry it if something else did.
23
+ enum TCCAuthStatus {
24
+ kTCCAuthNotDetermined = 0,
25
+ kTCCAuthDenied,
26
+ kTCCAuthGranted,
27
+ kTCCAuthLimited,
28
+ };
29
+
30
+ // Reads TCC.db for the current authorization status of (service, bundleId), translating either
31
+ // schema (see HasAuthValueColumn in the .mm) into the TCCAuthStatus values above. Fails with the
32
+ // same descriptive NSError as SetTCCAccess if the device has never been booted (no TCC.db yet).
33
+ BOOL GetTCCAccess(NSString* dataPath, NSString* service, NSString* bundleId, TCCAuthStatus* outStatus, NSError** error);
34
+
35
+ } // namespace coresim
@@ -0,0 +1,256 @@
1
+ #include "tcc_privacy.h"
2
+
3
+ #include <sqlite3.h>
4
+
5
+ #include <cstring>
6
+ #include <string>
7
+
8
+ namespace coresim {
9
+
10
+ namespace {
11
+
12
+ NSString* const kTCCErrorDomain = @"com.appium.coresim.TCCPrivacy";
13
+
14
+ NSError* MakeError(int code, NSString* message) {
15
+ return [NSError errorWithDomain:kTCCErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
16
+ }
17
+
18
+ NSString* TCCDatabasePath(NSString* dataPath) {
19
+ return [dataPath stringByAppendingPathComponent:@"Library/TCC/TCC.db"];
20
+ }
21
+
22
+ // SQLITE_OPEN_READWRITE only (no _CREATE): fails with SQLITE_CANTOPEN instead of silently
23
+ // creating an empty, schema-less database when the simulator has never been booted.
24
+ sqlite3* OpenDatabase(NSString* dataPath, NSError** error) {
25
+ NSString* path = TCCDatabasePath(dataPath);
26
+ sqlite3* db = nullptr;
27
+ int rc = sqlite3_open_v2(path.UTF8String, &db, SQLITE_OPEN_READWRITE, nullptr);
28
+ if (rc != SQLITE_OK) {
29
+ if (error) {
30
+ *error = MakeError(rc, [NSString stringWithFormat:@"Could not open TCC database at '%@' — the device may "
31
+ @"need to be booted at least once first: %s",
32
+ path, sqlite3_errstr(rc)]);
33
+ }
34
+ if (db) {
35
+ sqlite3_close(db);
36
+ }
37
+ return nullptr;
38
+ }
39
+ // Waits (rather than immediately failing) if the simulator's own tccd process has the database
40
+ // locked, instead of a manual sleep/retry loop.
41
+ sqlite3_busy_timeout(db, 5000);
42
+ return db;
43
+ }
44
+
45
+ // TCC schema gained `auth_value` (replacing the older boolean `allowed` column) in iOS 14; checked
46
+ // against the actual database rather than an OS version threshold, since the schema version tracks
47
+ // the *simulator's* iOS release, not the host's.
48
+ bool HasAuthValueColumn(sqlite3* db) {
49
+ sqlite3_stmt* stmt = nullptr;
50
+ if (sqlite3_prepare_v2(db, "PRAGMA table_info(access)", -1, &stmt, nullptr) != SQLITE_OK) {
51
+ return false;
52
+ }
53
+ bool found = false;
54
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
55
+ const unsigned char* name = sqlite3_column_text(stmt, 1);
56
+ if (name && std::strcmp(reinterpret_cast<const char*>(name), "auth_value") == 0) {
57
+ found = true;
58
+ break;
59
+ }
60
+ }
61
+ sqlite3_finalize(stmt);
62
+ return found;
63
+ }
64
+
65
+ void BindText(sqlite3_stmt* stmt, int index, NSString* value) {
66
+ sqlite3_bind_text(stmt, index, value.UTF8String, -1, SQLITE_TRANSIENT);
67
+ }
68
+
69
+ // Runs one parameterized statement to completion. `bind` receives the prepared statement to bind
70
+ // its own parameters onto.
71
+ template <typename BindFn>
72
+ bool ExecuteStatement(sqlite3* db, const char* sql, BindFn bind, NSError** error) {
73
+ sqlite3_stmt* stmt = nullptr;
74
+ if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
75
+ if (error) {
76
+ *error =
77
+ MakeError(sqlite3_errcode(db),
78
+ [NSString stringWithFormat:@"Failed to prepare TCC database statement: %s", sqlite3_errmsg(db)]);
79
+ }
80
+ return false;
81
+ }
82
+ bind(stmt);
83
+ int rc = sqlite3_step(stmt);
84
+ sqlite3_finalize(stmt);
85
+ if (rc != SQLITE_DONE) {
86
+ if (error) {
87
+ *error = MakeError(rc, [NSString stringWithFormat:@"Failed to update TCC database: %s", sqlite3_errmsg(db)]);
88
+ }
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+
94
+ bool DeleteAccessRow(sqlite3* db, NSString* service, NSString* bundleId, NSError** error) {
95
+ return ExecuteStatement(
96
+ db, "DELETE FROM access WHERE service = ? AND client = ? AND client_type = 0",
97
+ [&](sqlite3_stmt* stmt) {
98
+ BindText(stmt, 1, service);
99
+ BindText(stmt, 2, bundleId);
100
+ },
101
+ error);
102
+ }
103
+
104
+ // The delete+insert pair below must not be allowed to commit independently — a failure between
105
+ // them would otherwise permanently drop the prior row instead of leaving it unchanged.
106
+ bool BeginTransaction(sqlite3* db, NSError** error) {
107
+ if (sqlite3_exec(db, "BEGIN IMMEDIATE TRANSACTION", nullptr, nullptr, nullptr) == SQLITE_OK) {
108
+ return true;
109
+ }
110
+ if (error) {
111
+ *error = MakeError(sqlite3_errcode(db),
112
+ [NSString stringWithFormat:@"Failed to begin TCC database transaction: %s", sqlite3_errmsg(db)]);
113
+ }
114
+ return false;
115
+ }
116
+
117
+ // Returns whether the transaction actually ended up committed. On a COMMIT failure (e.g.
118
+ // SQLITE_BUSY from a concurrent reader — SQLite documents that a busy COMMIT leaves the
119
+ // transaction still active, not rolled back automatically) the transaction is explicitly rolled
120
+ // back here rather than left for sqlite3_close to implicitly discard, so the caller gets an
121
+ // accurate failure instead of the `true` that the statements before COMMIT itself had reported.
122
+ bool CommitOrRollback(sqlite3* db, bool commit, NSError** error) {
123
+ if (commit) {
124
+ if (sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr) == SQLITE_OK) {
125
+ return true;
126
+ }
127
+ if (error) {
128
+ *error =
129
+ MakeError(sqlite3_errcode(db),
130
+ [NSString stringWithFormat:@"Failed to commit TCC database transaction: %s", sqlite3_errmsg(db)]);
131
+ }
132
+ }
133
+ sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr);
134
+ return false;
135
+ }
136
+
137
+ } // namespace
138
+
139
+ BOOL SetTCCAccess(NSString* dataPath, NSString* service, NSString* bundleId, BOOL granted, NSError** error) {
140
+ sqlite3* db = OpenDatabase(dataPath, error);
141
+ if (!db) {
142
+ return NO;
143
+ }
144
+
145
+ BOOL success = BeginTransaction(db, error);
146
+ if (success) {
147
+ success = DeleteAccessRow(db, service, bundleId, error);
148
+ }
149
+ if (success) {
150
+ if (HasAuthValueColumn(db)) {
151
+ // `kTCCServicePhotos` rows use auth_version 2 (supports the "limited" library access
152
+ // introduced alongside it); every other service uses version 1. Not exposing "limited"
153
+ // as a grantable status here — only plain granted/denied, matching grantPermission's BOOL.
154
+ BOOL isPhotos = [service isEqualToString:@"kTCCServicePhotos"];
155
+ success = ExecuteStatement(
156
+ db,
157
+ "INSERT INTO access (service, client, client_type, auth_value, auth_reason, auth_version, flags) "
158
+ "VALUES (?, ?, 0, ?, 2, ?, 0)",
159
+ [&](sqlite3_stmt* stmt) {
160
+ BindText(stmt, 1, service);
161
+ BindText(stmt, 2, bundleId);
162
+ sqlite3_bind_int(stmt, 3, granted ? 2 : 0);
163
+ sqlite3_bind_int(stmt, 4, isPhotos ? 2 : 1);
164
+ },
165
+ error);
166
+ } else {
167
+ success = ExecuteStatement(
168
+ db, "REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES (?, ?, 0, ?, 1)",
169
+ [&](sqlite3_stmt* stmt) {
170
+ BindText(stmt, 1, service);
171
+ BindText(stmt, 2, bundleId);
172
+ sqlite3_bind_int(stmt, 3, granted ? 1 : 0);
173
+ },
174
+ error);
175
+ }
176
+ }
177
+
178
+ success = CommitOrRollback(db, success, error);
179
+ sqlite3_close(db);
180
+ return success;
181
+ }
182
+
183
+ BOOL ResetTCCAccess(NSString* dataPath, NSString* service, NSString* bundleId, NSError** error) {
184
+ sqlite3* db = OpenDatabase(dataPath, error);
185
+ if (!db) {
186
+ return NO;
187
+ }
188
+ BOOL success = DeleteAccessRow(db, service, bundleId, error);
189
+ sqlite3_close(db);
190
+ return success;
191
+ }
192
+
193
+ BOOL GetTCCAccess(NSString* dataPath, NSString* service, NSString* bundleId, TCCAuthStatus* outStatus,
194
+ NSError** error) {
195
+ sqlite3* db = OpenDatabase(dataPath, error);
196
+ if (!db) {
197
+ return NO;
198
+ }
199
+
200
+ bool hasAuthValue = HasAuthValueColumn(db);
201
+ const char* sql = hasAuthValue ? "SELECT auth_value FROM access WHERE service = ? AND client = ? AND client_type = 0"
202
+ : "SELECT allowed FROM access WHERE service = ? AND client = ? AND client_type = 0";
203
+ sqlite3_stmt* stmt = nullptr;
204
+ if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
205
+ if (error) {
206
+ *error =
207
+ MakeError(sqlite3_errcode(db),
208
+ [NSString stringWithFormat:@"Failed to prepare TCC database statement: %s", sqlite3_errmsg(db)]);
209
+ }
210
+ sqlite3_close(db);
211
+ return NO;
212
+ }
213
+ BindText(stmt, 1, service);
214
+ BindText(stmt, 2, bundleId);
215
+
216
+ BOOL success = YES;
217
+ TCCAuthStatus status = kTCCAuthNotDetermined;
218
+ int rc = sqlite3_step(stmt);
219
+ if (rc == SQLITE_ROW) {
220
+ int raw = sqlite3_column_int(stmt, 0);
221
+ if (hasAuthValue) {
222
+ // 0/2/3 per the schema comment in SetTCCAccess above; any other raw value (a reserved/future
223
+ // one this addon doesn't know about) is reported as not-determined rather than guessed at.
224
+ switch (raw) {
225
+ case 0:
226
+ status = kTCCAuthDenied;
227
+ break;
228
+ case 2:
229
+ status = kTCCAuthGranted;
230
+ break;
231
+ case 3:
232
+ status = kTCCAuthLimited;
233
+ break;
234
+ default:
235
+ status = kTCCAuthNotDetermined;
236
+ break;
237
+ }
238
+ } else {
239
+ status = raw ? kTCCAuthGranted : kTCCAuthDenied;
240
+ }
241
+ } else if (rc != SQLITE_DONE) {
242
+ success = NO;
243
+ if (error) {
244
+ *error = MakeError(rc, [NSString stringWithFormat:@"Failed to read TCC database: %s", sqlite3_errmsg(db)]);
245
+ }
246
+ }
247
+ sqlite3_finalize(stmt);
248
+ sqlite3_close(db);
249
+
250
+ if (success && outStatus) {
251
+ *outStatus = status;
252
+ }
253
+ return success;
254
+ }
255
+
256
+ } // namespace coresim
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ #include <napi.h>
6
+
7
+ namespace coresim {
8
+
9
+ // Converts a JS value (string/number/boolean/null/undefined/Array/plain Object/Buffer) into the
10
+ // corresponding Foundation object (NSString/NSNumber/NSNull/NSArray/NSDictionary/NSData).
11
+ // Used for option dictionaries (boot/install/uninstall options) and JSON payloads (push).
12
+ NSObject* JsValueToNSObject(Napi::Env env, Napi::Value value);
13
+
14
+ // Converts a Foundation object back into a JS value. Used for propertiesOfApplication/
15
+ // installedApps/getenv results. NSNull -> null; unrecognized types -> their -description string.
16
+ Napi::Value NSObjectToJsValue(Napi::Env env, id object);
17
+
18
+ } // namespace coresim