@appium/coresim 1.12.0 → 1.13.1

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/lib/src/commands/permissions.d.ts +15 -7
  4. package/lib/src/commands/permissions.d.ts.map +1 -1
  5. package/lib/src/commands/permissions.js +66 -6
  6. package/lib/src/commands/permissions.js.map +1 -1
  7. package/lib/src/index.d.ts +1 -1
  8. package/lib/src/index.d.ts.map +1 -1
  9. package/lib/src/index.js.map +1 -1
  10. package/lib/src/native-simctl.d.ts +27 -9
  11. package/lib/src/native-simctl.d.ts.map +1 -1
  12. package/lib/src/native-simctl.js +9 -16
  13. package/lib/src/native-simctl.js.map +1 -1
  14. package/lib/src/types.d.ts +6 -4
  15. package/lib/src/types.d.ts.map +1 -1
  16. package/lib/src/utils/bulletin-board-model-v2.d.ts +10 -0
  17. package/lib/src/utils/bulletin-board-model-v2.d.ts.map +1 -0
  18. package/lib/src/utils/bulletin-board-model-v2.js +73 -0
  19. package/lib/src/utils/bulletin-board-model-v2.js.map +1 -0
  20. package/lib/src/utils/bulletin-board-model-v3.d.ts +4 -0
  21. package/lib/src/utils/bulletin-board-model-v3.d.ts.map +1 -0
  22. package/lib/src/utils/bulletin-board-model-v3.js +16 -0
  23. package/lib/src/utils/bulletin-board-model-v3.js.map +1 -0
  24. package/lib/src/utils/bulletin-board-model.d.ts +19 -0
  25. package/lib/src/utils/bulletin-board-model.d.ts.map +1 -0
  26. package/lib/src/utils/bulletin-board-model.js +2 -0
  27. package/lib/src/utils/bulletin-board-model.js.map +1 -0
  28. package/lib/src/utils/bulletin-board-store.d.ts +41 -0
  29. package/lib/src/utils/bulletin-board-store.d.ts.map +1 -0
  30. package/lib/src/utils/bulletin-board-store.js +114 -0
  31. package/lib/src/utils/bulletin-board-store.js.map +1 -0
  32. package/lib/src/utils/index.d.ts +3 -0
  33. package/lib/src/utils/index.d.ts.map +1 -1
  34. package/lib/src/utils/index.js +2 -0
  35. package/lib/src/utils/index.js.map +1 -1
  36. package/lib/src/utils/springboard.d.ts +26 -0
  37. package/lib/src/utils/springboard.d.ts.map +1 -0
  38. package/lib/src/utils/springboard.js +106 -0
  39. package/lib/src/utils/springboard.js.map +1 -0
  40. package/package.json +3 -3
  41. package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
  42. package/src/commands/permissions.ts +75 -11
  43. package/src/coresim.mm +7 -0
  44. package/src/index.ts +1 -1
  45. package/src/native-simctl.ts +45 -19
  46. package/src/types.ts +6 -3
  47. package/src/utils/bulletin-board-model-v2.ts +79 -0
  48. package/src/utils/bulletin-board-model-v3.ts +16 -0
  49. package/src/utils/bulletin-board-model.ts +20 -0
  50. package/src/utils/bulletin-board-store.ts +126 -0
  51. package/src/utils/index.ts +3 -0
  52. package/src/utils/springboard.ts +119 -0
@@ -0,0 +1,16 @@
1
+ import { buildSectionInfo } from './bulletin-board-model-v2.js';
2
+ /** The format used by SpringBoard since iOS 27: v2 with `isLocked` in the `BBSectionInfo`. */
3
+ export const bulletinBoardModelV3 = {
4
+ version: 3,
5
+ createStore: () => ({ sectionInfoVersionNumber: 3 }),
6
+ setNotificationsStatus(store, bundleId, status) {
7
+ const sectionInfo = (store.sectionInfo ??= {});
8
+ if (status === 'unset') {
9
+ delete sectionInfo[bundleId];
10
+ }
11
+ else {
12
+ sectionInfo[bundleId] = buildSectionInfo(bundleId, status, { isLocked: false });
13
+ }
14
+ },
15
+ };
16
+ //# sourceMappingURL=bulletin-board-model-v3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulletin-board-model-v3.js","sourceRoot":"","sources":["../../../src/utils/bulletin-board-model-v3.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,gBAAgB,EAAC,MAAM,8BAA8B,CAAC;AAG9D,8FAA8F;AAC9F,MAAM,CAAC,MAAM,oBAAoB,GAAuB;IACtD,OAAO,EAAE,CAAC;IACV,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,EAAC,wBAAwB,EAAE,CAAC,EAAC,CAAC;IAClD,sBAAsB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM;QAC5C,MAAM,WAAW,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,EAAE,CAAC,CAAC;QAC/C,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,WAAW,CAAC,QAAQ,CAAC,GAAG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAC,QAAQ,EAAE,KAAK,EAAC,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;CACF,CAAC"}
@@ -0,0 +1,19 @@
1
+ export type NotificationsStatus = 'granted' | 'denied' | 'critical' | 'unset';
2
+ /** Content of `VersionedSectionInfo.plist`. */
3
+ export interface BulletinBoardStoreContent {
4
+ sectionInfoVersionNumber?: number;
5
+ sectionInfo?: Record<string, Uint8Array>;
6
+ }
7
+ /**
8
+ * The data model of one `sectionInfoVersionNumber` of the BulletinBoard store. The store operations
9
+ * (locking, atomic replace, ...) don't depend on it, so a new format version only needs a new model.
10
+ */
11
+ export interface BulletinBoardModel {
12
+ /** The `sectionInfoVersionNumber` this model reads and writes. */
13
+ readonly version: number;
14
+ /** An empty store, as SpringBoard of this version would start it. */
15
+ createStore(): BulletinBoardStoreContent;
16
+ /** Adds, replaces or (`unset`) removes the app's notifications section in place. */
17
+ setNotificationsStatus(store: BulletinBoardStoreContent, bundleId: string, status: NotificationsStatus): void;
18
+ }
19
+ //# sourceMappingURL=bulletin-board-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulletin-board-model.d.ts","sourceRoot":"","sources":["../../../src/utils/bulletin-board-model.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAE9E,+CAA+C;AAC/C,MAAM,WAAW,yBAAyB;IACxC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,WAAW,IAAI,yBAAyB,CAAC;IACzC,oFAAoF;IACpF,sBAAsB,CAAC,KAAK,EAAE,yBAAyB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC/G"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bulletin-board-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulletin-board-model.js","sourceRoot":"","sources":["../../../src/utils/bulletin-board-model.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import type { NotificationsStatus } from './bulletin-board-model.js';
2
+ /**
3
+ * SpringBoard's BulletinBoard store of a device (`Library/BulletinBoard/VersionedSectionInfo.plist`),
4
+ * which keeps the notifications settings of every app as one archived `BBSectionInfo` per bundle id.
5
+ */
6
+ export declare class BulletinBoardStore {
7
+ readonly path: string;
8
+ private readonly models;
9
+ /**
10
+ * @param dataPath — data directory of the device
11
+ * @param versions — the `sectionInfoVersionNumber`s supported by the runtime, the oldest first
12
+ * (all the known ones by default). A store of any other version is refused, as the format may
13
+ * change in a future runtime. A missing store (a never-booted device) is created in the first
14
+ * one, as SpringBoard migrates older versions but not newer ones.
15
+ * @throws {Error} if a version is unknown
16
+ */
17
+ constructor(dataPath: string, versions?: readonly number[]);
18
+ /**
19
+ * Runs `update` once every earlier update of the same store has settled: concurrent updates for
20
+ * different apps would otherwise read the same store, and the last write would drop the other app's
21
+ * section. Only the calls made by this process are serialized.
22
+ */
23
+ withLock<T>(update: () => Promise<T>): Promise<T>;
24
+ /**
25
+ * Adds, replaces or (`unset`) removes the app's section. The store is replaced atomically, so a
26
+ * failed write leaves the previous one intact. A device that has never been booted has no store
27
+ * yet: it is created with just this section, and SpringBoard adds the rest on boot.
28
+ *
29
+ * @returns `false` if there was nothing to write (`unset` without a store)
30
+ * @throws {Error} if the existing store is of an unsupported format version
31
+ */
32
+ setNotificationsStatus(bundleId: string, status: NotificationsStatus): Promise<boolean>;
33
+ /**
34
+ * Makes the store immutable (`uchg`) so that nothing, including a dying SpringBoard, can rewrite it.
35
+ * Best effort.
36
+ */
37
+ setImmutable(immutable: boolean): Promise<void>;
38
+ private modelOf;
39
+ private replace;
40
+ }
41
+ //# sourceMappingURL=bulletin-board-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulletin-board-store.d.ts","sourceRoot":"","sources":["../../../src/utils/bulletin-board-store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAgD,mBAAmB,EAAC,MAAM,2BAA2B,CAAC;AAUlH;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IAEvD;;;;;;;OAOG;gBACS,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,MAAM,EAA6C;IAWpG;;;;OAIG;IACG,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAgBvD;;;;;;;OAOG;IACG,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkB7F;;;OAGG;IACG,YAAY,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD,OAAO,CAAC,OAAO;YAWD,OAAO;CAWtB"}
@@ -0,0 +1,114 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { fs, plist } from '@appium/support';
6
+ import { bulletinBoardModelV2 } from './bulletin-board-model-v2.js';
7
+ import { bulletinBoardModelV3 } from './bulletin-board-model-v3.js';
8
+ const execFileAsync = promisify(execFile);
9
+ // All the known data models, the oldest first.
10
+ const KNOWN_MODELS = [bulletinBoardModelV2, bulletinBoardModelV3];
11
+ // The last pending update of each store, see BulletinBoardStore.withLock.
12
+ const pendingUpdates = new Map();
13
+ /**
14
+ * SpringBoard's BulletinBoard store of a device (`Library/BulletinBoard/VersionedSectionInfo.plist`),
15
+ * which keeps the notifications settings of every app as one archived `BBSectionInfo` per bundle id.
16
+ */
17
+ export class BulletinBoardStore {
18
+ path;
19
+ models;
20
+ /**
21
+ * @param dataPath — data directory of the device
22
+ * @param versions — the `sectionInfoVersionNumber`s supported by the runtime, the oldest first
23
+ * (all the known ones by default). A store of any other version is refused, as the format may
24
+ * change in a future runtime. A missing store (a never-booted device) is created in the first
25
+ * one, as SpringBoard migrates older versions but not newer ones.
26
+ * @throws {Error} if a version is unknown
27
+ */
28
+ constructor(dataPath, versions = KNOWN_MODELS.map(({ version }) => version)) {
29
+ this.models = versions.map((version) => {
30
+ const model = KNOWN_MODELS.find((m) => m.version === version);
31
+ if (!model) {
32
+ throw new Error(`Unknown BulletinBoard store version ${version}`);
33
+ }
34
+ return model;
35
+ });
36
+ this.path = path.join(dataPath, 'Library', 'BulletinBoard', 'VersionedSectionInfo.plist');
37
+ }
38
+ /**
39
+ * Runs `update` once every earlier update of the same store has settled: concurrent updates for
40
+ * different apps would otherwise read the same store, and the last write would drop the other app's
41
+ * section. Only the calls made by this process are serialized.
42
+ */
43
+ async withLock(update) {
44
+ const result = (pendingUpdates.get(this.path) ?? Promise.resolve()).then(update);
45
+ const settled = result.then(() => { }, () => { });
46
+ pendingUpdates.set(this.path, settled);
47
+ try {
48
+ return await result;
49
+ }
50
+ finally {
51
+ if (pendingUpdates.get(this.path) === settled) {
52
+ pendingUpdates.delete(this.path);
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Adds, replaces or (`unset`) removes the app's section. The store is replaced atomically, so a
58
+ * failed write leaves the previous one intact. A device that has never been booted has no store
59
+ * yet: it is created with just this section, and SpringBoard adds the rest on boot.
60
+ *
61
+ * @returns `false` if there was nothing to write (`unset` without a store)
62
+ * @throws {Error} if the existing store is of an unsupported format version
63
+ */
64
+ async setNotificationsStatus(bundleId, status) {
65
+ let store;
66
+ let model;
67
+ if (await fs.exists(this.path)) {
68
+ store = (await plist.parsePlistFile(this.path));
69
+ model = this.modelOf(store);
70
+ }
71
+ else if (status === 'unset') {
72
+ return false;
73
+ }
74
+ else {
75
+ await fs.mkdir(path.dirname(this.path), { recursive: true });
76
+ model = this.models[0];
77
+ store = model.createStore();
78
+ }
79
+ model.setNotificationsStatus(store, bundleId, status);
80
+ await this.replace(store);
81
+ return true;
82
+ }
83
+ /**
84
+ * Makes the store immutable (`uchg`) so that nothing, including a dying SpringBoard, can rewrite it.
85
+ * Best effort.
86
+ */
87
+ async setImmutable(immutable) {
88
+ try {
89
+ await execFileAsync('chflags', [immutable ? 'uchg' : 'nouchg', this.path]);
90
+ }
91
+ catch { }
92
+ }
93
+ modelOf(store) {
94
+ const model = this.models.find(({ version }) => version === store.sectionInfoVersionNumber);
95
+ if (!model) {
96
+ throw new Error(`Unsupported version of ${this.path}: sectionInfoVersionNumber is ${store.sectionInfoVersionNumber}, ` +
97
+ `supported: ${this.models.map(({ version }) => version).join(', ')}`);
98
+ }
99
+ return model;
100
+ }
101
+ async replace(store) {
102
+ // In the same directory, so that the rename is atomic.
103
+ const tmpPath = `${this.path}.${randomUUID()}.tmp`;
104
+ try {
105
+ await fs.writeFile(tmpPath, plist.createPlist(store, false), { flag: 'wx' });
106
+ await fs.rename(tmpPath, this.path);
107
+ }
108
+ catch (err) {
109
+ await fs.unlink(tmpPath).catch(() => { });
110
+ throw err;
111
+ }
112
+ }
113
+ }
114
+ //# sourceMappingURL=bulletin-board-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulletin-board-store.js","sourceRoot":"","sources":["../../../src/utils/bulletin-board-store.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,7 @@
1
1
  export { getPkgRoot, PACKAGE_NAME } from './pkg-root.js';
2
2
  export { runCatchingAsync, toTypedError } from './run-catching.js';
3
3
  export { resolveDisplayPortId, validateDisplayId } from './display.js';
4
+ export { BulletinBoardStore } from './bulletin-board-store.js';
5
+ export type { NotificationsStatus } from './bulletin-board-model.js';
6
+ export { SpringBoard } from './springboard.js';
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAE,YAAY,EAAC,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAE,YAAY,EAAC,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC;AACrE,OAAO,EAAC,kBAAkB,EAAC,MAAM,2BAA2B,CAAC;AAC7D,YAAY,EAAC,mBAAmB,EAAC,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC"}
@@ -1,4 +1,6 @@
1
1
  export { getPkgRoot, PACKAGE_NAME } from './pkg-root.js';
2
2
  export { runCatchingAsync, toTypedError } from './run-catching.js';
3
3
  export { resolveDisplayPortId, validateDisplayId } from './display.js';
4
+ export { BulletinBoardStore } from './bulletin-board-store.js';
5
+ export { SpringBoard } from './springboard.js';
4
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAE,YAAY,EAAC,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,eAAe,CAAC;AACvD,OAAO,EAAC,gBAAgB,EAAE,YAAY,EAAC,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC;AACrE,OAAO,EAAC,kBAAkB,EAAC,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { NativeSimctl } from '../native-simctl.js';
2
+ /**
3
+ * SpringBoard of a booted device: the process that owns the BulletinBoard store, see
4
+ * setNotificationsAccess. The simulator shares the host kernel, so it is signalled directly.
5
+ */
6
+ export declare class SpringBoard {
7
+ private readonly simctl;
8
+ private readonly udid;
9
+ private pausedPid;
10
+ constructor(simctl: NativeSimctl, udid: string);
11
+ /**
12
+ * Pauses the running SpringBoard so that it cannot rewrite the BulletinBoard store. It can be in
13
+ * the middle of a restart, hence the retries.
14
+ */
15
+ pause(): Promise<void>;
16
+ /** Lets the paused SpringBoard run again. */
17
+ resume(): void;
18
+ /**
19
+ * Replaces the paused SpringBoard with a new one: asks launchd to stop it (launchd starts a new
20
+ * one right away), resumes it so that it can exit, then waits for the new process.
21
+ */
22
+ restart(): Promise<void>;
23
+ private pid;
24
+ private stop;
25
+ }
26
+ //# sourceMappingURL=springboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"springboard.d.ts","sourceRoot":"","sources":["../../../src/utils/springboard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AAKtD;;;GAGG;AACH,qBAAa,WAAW;IAIpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,SAAS,CAAqB;gBAGnB,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM;IAG/B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB5B,6CAA6C;IAC7C,MAAM,IAAI,IAAI;IAMd;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBhB,GAAG;YAIH,IAAI;CAwBnB"}
@@ -0,0 +1,106 @@
1
+ import { once } from 'node:events';
2
+ import { waitForCondition } from 'asyncbox';
3
+ const SPRINGBOARD_SERVICE = 'com.apple.SpringBoard';
4
+ const SPRINGBOARD_RESTART_TIMEOUT_MS = 30000;
5
+ /**
6
+ * SpringBoard of a booted device: the process that owns the BulletinBoard store, see
7
+ * setNotificationsAccess. The simulator shares the host kernel, so it is signalled directly.
8
+ */
9
+ export class SpringBoard {
10
+ simctl;
11
+ udid;
12
+ pausedPid;
13
+ constructor(simctl, udid) {
14
+ this.simctl = simctl;
15
+ this.udid = udid;
16
+ }
17
+ /**
18
+ * Pauses the running SpringBoard so that it cannot rewrite the BulletinBoard store. It can be in
19
+ * the middle of a restart, hence the retries.
20
+ */
21
+ async pause() {
22
+ try {
23
+ await waitForCondition(async () => {
24
+ const pid = await this.pid().catch(() => undefined);
25
+ if (pid && sendSignal(pid, 'SIGSTOP')) {
26
+ this.pausedPid = pid;
27
+ }
28
+ return Boolean(this.pausedPid);
29
+ }, { waitMs: SPRINGBOARD_RESTART_TIMEOUT_MS, intervalMs: 300 });
30
+ }
31
+ catch { }
32
+ if (!this.pausedPid) {
33
+ throw new Error(`${SPRINGBOARD_SERVICE} is not running`);
34
+ }
35
+ }
36
+ /** Lets the paused SpringBoard run again. */
37
+ resume() {
38
+ if (this.pausedPid) {
39
+ sendSignal(this.pausedPid, 'SIGCONT');
40
+ }
41
+ }
42
+ /**
43
+ * Replaces the paused SpringBoard with a new one: asks launchd to stop it (launchd starts a new
44
+ * one right away), resumes it so that it can exit, then waits for the new process.
45
+ */
46
+ async restart() {
47
+ const previousPid = this.pausedPid;
48
+ try {
49
+ await this.stop();
50
+ }
51
+ finally {
52
+ this.resume();
53
+ this.pausedPid = undefined;
54
+ }
55
+ try {
56
+ await waitForCondition(async () => {
57
+ const pid = await this.pid().catch(() => undefined);
58
+ return Number.isInteger(pid) && pid !== previousPid;
59
+ }, { waitMs: SPRINGBOARD_RESTART_TIMEOUT_MS, intervalMs: 300 });
60
+ }
61
+ catch {
62
+ throw new Error(`${SPRINGBOARD_SERVICE} did not restart within ${SPRINGBOARD_RESTART_TIMEOUT_MS}ms`);
63
+ }
64
+ }
65
+ async pid() {
66
+ return (await this.simctl.listProcesses(this.udid)).find(({ name }) => name === SPRINGBOARD_SERVICE)?.pid;
67
+ }
68
+ async stop() {
69
+ // Bare name: resolved against the guest runtime's own bin dirs, like in listProcesses.
70
+ const proc = await this.simctl.spawnProcess(this.udid, 'launchctl', {
71
+ arguments: ['launchctl', 'stop', SPRINGBOARD_SERVICE],
72
+ });
73
+ let stderr = '';
74
+ proc.stdout.resume();
75
+ proc.stderr.on('data', (chunk) => {
76
+ stderr += chunk;
77
+ });
78
+ const streamError = Promise.race([once(proc.stdout, 'error'), once(proc.stderr, 'error')]).then(([err]) => {
79
+ throw err;
80
+ });
81
+ const [[code, signal]] = await Promise.race([
82
+ Promise.all([once(proc, 'exit'), once(proc.stderr, 'end')]),
83
+ streamError,
84
+ ]);
85
+ if (code !== 0) {
86
+ const reason = signal ? `signal ${signal}` : `exit code ${code}`;
87
+ throw new Error(`'launchctl stop ${SPRINGBOARD_SERVICE}' failed with ${reason}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * @returns `false` if the process no longer exists
93
+ */
94
+ function sendSignal(pid, name) {
95
+ try {
96
+ process.kill(pid, name);
97
+ return true;
98
+ }
99
+ catch (err) {
100
+ if (err?.code === 'ESRCH') {
101
+ return false;
102
+ }
103
+ throw err;
104
+ }
105
+ }
106
+ //# sourceMappingURL=springboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"springboard.js","sourceRoot":"","sources":["../../../src/utils/springboard.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appium/coresim",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "description": "Native bindings to CoreSimulator.framework for Apple platform simulator control",
5
5
  "bugs": {
6
6
  "url": "https://github.com/appium/appium-ios/issues"
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "@appium/tsconfig": "^1.2.0",
57
- "@types/node": "^26.6.3",
57
+ "@types/node": "^26.6.5",
58
58
  "prebuildify": "^6.0.1"
59
59
  },
60
60
  "engines": {
@@ -62,5 +62,5 @@
62
62
  "npm": ">=10"
63
63
  },
64
64
  "gypfile": true,
65
- "gitHead": "9093de3b79fba810459e0cb42c1aee3e9d30fbe5"
65
+ "gitHead": "9dce08cd8d4162196de0504f57500ec18f83b1c8"
66
66
  }
@@ -1,6 +1,8 @@
1
1
  import type {NativeSimctl} from '../native-simctl.js';
2
- import type {SimPermissionService, SimPermissionStatus} from '../types.js';
3
- import {runCatchingAsync} from '../utils/index.js';
2
+ import {SimDeviceState, type SimPermissionService, type SimPermissionStatus} from '../types.js';
3
+ import {BulletinBoardStore, runCatchingAsync, SpringBoard, type NotificationsStatus} from '../utils/index.js';
4
+
5
+ const NOTIFICATIONS_SERVICE = 'notifications';
4
6
 
5
7
  declare module '../native-simctl.js' {
6
8
  interface NativeSimctl {
@@ -8,7 +10,7 @@ declare module '../native-simctl.js' {
8
10
  udid: string,
9
11
  service: SimPermissionService,
10
12
  bundleId: string,
11
- status?: 'granted' | 'limited',
13
+ status?: 'granted' | 'limited' | 'critical',
12
14
  ): Promise<void>;
13
15
  revokePermission(udid: string, service: SimPermissionService, bundleId: string): Promise<void>;
14
16
  resetPermission(udid: string, service: SimPermissionService, bundleId: string): Promise<void>;
@@ -21,30 +23,41 @@ declare module '../native-simctl.js' {
21
23
  * simulator's own TCC (privacy) database — see CLAUDE.md for why this bypasses CoreSimulator's own
22
24
  * privacy API.
23
25
  *
26
+ * `notifications` isn't a TCC row: it is written to SpringBoard's BulletinBoard store instead. On a
27
+ * booted device SpringBoard is restarted to load it, which terminates every running app; a shut down
28
+ * device picks it up on its next boot.
29
+ *
24
30
  * @param udid — UDID of the target device
25
- * @param service — permission to grant, e.g. `"camera"`, `"contacts"`, `"photos"`
31
+ * @param service — permission to grant, e.g. `"camera"`, `"contacts"`, `"photos"`, `"notifications"`
26
32
  * @param bundleId — bundle identifier of the app the permission applies to
27
33
  * @param status — defaults to `'granted'`. `'limited'` ("selected photos" access) is only valid
28
- * for the `photos` service.
34
+ * for the `photos` service, `'critical'` (critical alerts allowed too) — for `notifications`.
29
35
  */
30
36
  export async function grantPermission(
31
37
  this: NativeSimctl,
32
38
  udid: string,
33
39
  service: SimPermissionService,
34
40
  bundleId: string,
35
- status: 'granted' | 'limited' = 'granted',
41
+ status: 'granted' | 'limited' | 'critical' = 'granted',
36
42
  ): Promise<void> {
37
43
  return runCatchingAsync(async () => {
38
44
  if (status === 'limited' && service !== 'photos') {
39
45
  throw new Error(`'limited' is only a valid status for the 'photos' service, not '${service}'`);
40
46
  }
47
+ if (service === NOTIFICATIONS_SERVICE) {
48
+ return await setNotificationsAccess.call(this, udid, bundleId, status === 'critical' ? 'critical' : 'granted');
49
+ }
50
+ if (status === 'critical') {
51
+ throw new Error(`'critical' is only a valid status for the 'notifications' service, not '${service}'`);
52
+ }
41
53
  const tccIdentifier = toTCCIdentifier(service);
42
54
  return (await this._findDevice(udid)).grantPermission(tccIdentifier, bundleId, status);
43
55
  });
44
56
  }
45
57
 
46
58
  /**
47
- * Revokes a previously granted privacy permission from the given app.
59
+ * Revokes a previously granted privacy permission from the given app. For `notifications`, a booted
60
+ * device's SpringBoard is restarted, see {@link grantPermission}.
48
61
  *
49
62
  * @param udid — UDID of the target device
50
63
  * @param service — permission to revoke (see {@link grantPermission})
@@ -57,13 +70,18 @@ export async function revokePermission(
57
70
  bundleId: string,
58
71
  ): Promise<void> {
59
72
  return runCatchingAsync(async () => {
73
+ if (service === NOTIFICATIONS_SERVICE) {
74
+ return await setNotificationsAccess.call(this, udid, bundleId, 'denied');
75
+ }
60
76
  const tccIdentifier = toTCCIdentifier(service);
61
77
  return (await this._findDevice(udid)).revokePermission(tccIdentifier, bundleId);
62
78
  });
63
79
  }
64
80
 
65
81
  /**
66
- * Resets a privacy permission for the given app to its default (unprompted) state.
82
+ * Resets a privacy permission for the given app to its default (unprompted) state. For
83
+ * `notifications`, the app's section is removed from SpringBoard's store and a booted device's
84
+ * SpringBoard is restarted, see {@link grantPermission}.
67
85
  *
68
86
  * @param udid — UDID of the target device
69
87
  * @param service — permission to reset (see {@link grantPermission})
@@ -76,6 +94,9 @@ export async function resetPermission(
76
94
  bundleId: string,
77
95
  ): Promise<void> {
78
96
  return runCatchingAsync(async () => {
97
+ if (service === NOTIFICATIONS_SERVICE) {
98
+ return await setNotificationsAccess.call(this, udid, bundleId, 'unset');
99
+ }
79
100
  const tccIdentifier = toTCCIdentifier(service);
80
101
  return (await this._findDevice(udid)).resetPermission(tccIdentifier, bundleId);
81
102
  });
@@ -90,6 +111,7 @@ export async function resetPermission(
90
111
  * @param service — permission to check (see {@link grantPermission})
91
112
  * @param bundleId — bundle identifier of the app the permission applies to
92
113
  * @returns `'unset'` if the app has never been prompted/decided for this permission
114
+ * @throws {Error} for `notifications`: reading it isn't supported
93
115
  */
94
116
  export async function getPermission(
95
117
  this: NativeSimctl,
@@ -98,6 +120,9 @@ export async function getPermission(
98
120
  bundleId: string,
99
121
  ): Promise<SimPermissionStatus> {
100
122
  return runCatchingAsync(async () => {
123
+ if (service === NOTIFICATIONS_SERVICE) {
124
+ throw new Error(`The status of the '${NOTIFICATIONS_SERVICE}' permission cannot be read`);
125
+ }
101
126
  const tccIdentifier = toTCCIdentifier(service);
102
127
  return (await this._findDevice(udid)).getPermission(tccIdentifier, bundleId);
103
128
  });
@@ -106,8 +131,8 @@ export async function getPermission(
106
131
  // Maps a friendly service name to the internal TCC service identifier its row in the simulator's
107
132
  // own TCC.db is keyed on (see native/tcc_privacy.h). `location` is deliberately absent: it isn't a
108
133
  // plain TCC row (CoreLocation simulation has its own subsystem), so it isn't supported by this
109
- // TCC.db-based implementation.
110
- const SERVICE_TO_TCC_IDENTIFIER: Record<SimPermissionService, string> = {
134
+ // TCC.db-based implementation. `notifications` isn't a TCC row either, see setNotificationsAccess.
135
+ const SERVICE_TO_TCC_IDENTIFIER: Record<Exclude<SimPermissionService, 'notifications'>, string> = {
111
136
  calendar: 'kTCCServiceCalendar',
112
137
  camera: 'kTCCServiceCamera',
113
138
  contacts: 'kTCCServiceAddressBook',
@@ -124,7 +149,7 @@ const SERVICE_TO_TCC_IDENTIFIER: Record<SimPermissionService, string> = {
124
149
  usertracking: 'kTCCServiceUserTracking',
125
150
  };
126
151
 
127
- function toTCCIdentifier(service: SimPermissionService): string {
152
+ function toTCCIdentifier(service: Exclude<SimPermissionService, 'notifications'>): string {
128
153
  const identifier = SERVICE_TO_TCC_IDENTIFIER[service];
129
154
  if (!identifier) {
130
155
  throw new Error(
@@ -133,3 +158,42 @@ function toTCCIdentifier(service: SimPermissionService): string {
133
158
  }
134
159
  return identifier;
135
160
  }
161
+
162
+ /**
163
+ * `notifications` isn't a TCC row: SpringBoard keeps it per app in its BulletinBoard store, reads
164
+ * that file only on its own start and rewrites it from memory from time to time (e.g. a few seconds
165
+ * after it starts). So SpringBoard of a booted device is paused while the app's section is written,
166
+ * then restarted with the store kept immutable until the old process is gone. The new SpringBoard
167
+ * loads the change and terminates every running app. A shut down device only gets the file,
168
+ * SpringBoard loads it on boot.
169
+ */
170
+ async function setNotificationsAccess(
171
+ this: NativeSimctl,
172
+ udid: string,
173
+ bundleId: string,
174
+ status: NotificationsStatus,
175
+ ): Promise<void> {
176
+ const device = await this._findDevice(udid);
177
+ const store = new BulletinBoardStore(device.dataPath(), this._options.bulletinBoardVersions);
178
+ await store.withLock(async () => {
179
+ const springBoard = device.state() === SimDeviceState.Booted ? new SpringBoard(this, udid) : undefined;
180
+ await springBoard?.pause();
181
+ let written = false;
182
+ try {
183
+ written = await store.setNotificationsStatus(bundleId, status);
184
+ } finally {
185
+ if (!written) {
186
+ springBoard?.resume();
187
+ }
188
+ }
189
+ if (!springBoard || !written) {
190
+ return;
191
+ }
192
+ await store.setImmutable(true);
193
+ try {
194
+ await springBoard.restart();
195
+ } finally {
196
+ await store.setImmutable(false);
197
+ }
198
+ });
199
+ }
package/src/coresim.mm CHANGED
@@ -619,6 +619,12 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
619
619
  return Napi::String::New(info.Env(), coresim::RuntimeRootPath(runtime).UTF8String);
620
620
  });
621
621
  }
622
+ Napi::Value DataPath(const Napi::CallbackInfo& info) {
623
+ return CatchToJs(info.Env(), [&]() -> Napi::Value {
624
+ NSString* dataPath = coresim::DeviceDataPath(device_);
625
+ return Napi::String::New(info.Env(), dataPath == nil ? "" : dataPath.UTF8String);
626
+ });
627
+ }
622
628
 
623
629
  // Bridges SimDevice's GCD-completion-block-based bootAsyncWithOptions:completionQueue:
624
630
  // completionHandler: — this is what replaces the CLI's bootstatus poll-until-timeout race with
@@ -1724,6 +1730,7 @@ void NativeDevice::Init(Napi::Env env) {
1724
1730
  InstanceMethod<&NativeDevice::DeviceTypeIdentifier>("deviceTypeIdentifier"),
1725
1731
  InstanceMethod<&NativeDevice::RuntimeIdentifier>("runtimeIdentifier"),
1726
1732
  InstanceMethod<&NativeDevice::RuntimeRootPath>("runtimeRootPath"),
1733
+ InstanceMethod<&NativeDevice::DataPath>("dataPath"),
1727
1734
  InstanceMethod<&NativeDevice::Boot>("boot"),
1728
1735
  InstanceMethod<&NativeDevice::GetBootStatus>("getBootStatus"),
1729
1736
  InstanceMethod<&NativeDevice::Shutdown>("shutdown"),
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export {NativeSimError, NativeSimUnavailableError, NativeSimDispatchError, NativeSimOperationError} from './errors.js';
2
- export {NativeSimctl} from './native-simctl.js';
2
+ export {NativeSimctl, type NativeSimctlOptions} from './native-simctl.js';
3
3
  export {SpawnedProcess} from './commands/spawn.js';
4
4
  export {VideoStream} from './commands/video-stream.js';
5
5
  export {JpegStream} from './commands/jpeg-stream.js';