appium-ios-remotexpc 0.18.0 → 0.19.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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/src/lib/apple-tv/encryption/ed25519.d.ts.map +1 -1
  3. package/build/src/lib/apple-tv/encryption/ed25519.js +1 -1
  4. package/build/src/lib/tunnel/packet-stream-client.js +1 -1
  5. package/build/src/lib/types.d.ts +46 -0
  6. package/build/src/lib/types.d.ts.map +1 -1
  7. package/build/src/services/ios/dvt/channel.d.ts +5 -0
  8. package/build/src/services/ios/dvt/channel.d.ts.map +1 -1
  9. package/build/src/services/ios/dvt/channel.js +7 -0
  10. package/build/src/services/ios/dvt/instruments/application-listing.d.ts +2 -9
  11. package/build/src/services/ios/dvt/instruments/application-listing.d.ts.map +1 -1
  12. package/build/src/services/ios/dvt/instruments/application-listing.js +2 -15
  13. package/build/src/services/ios/dvt/instruments/base-instrument.d.ts +19 -0
  14. package/build/src/services/ios/dvt/instruments/base-instrument.d.ts.map +1 -0
  15. package/build/src/services/ios/dvt/instruments/base-instrument.js +24 -0
  16. package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts +2 -9
  17. package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts.map +1 -1
  18. package/build/src/services/ios/dvt/instruments/condition-inducer.js +2 -15
  19. package/build/src/services/ios/dvt/instruments/device-info.d.ts +2 -9
  20. package/build/src/services/ios/dvt/instruments/device-info.d.ts.map +1 -1
  21. package/build/src/services/ios/dvt/instruments/device-info.js +2 -14
  22. package/build/src/services/ios/dvt/instruments/graphics.d.ts +2 -6
  23. package/build/src/services/ios/dvt/instruments/graphics.d.ts.map +1 -1
  24. package/build/src/services/ios/dvt/instruments/graphics.js +2 -11
  25. package/build/src/services/ios/dvt/instruments/location-simulation.d.ts +2 -9
  26. package/build/src/services/ios/dvt/instruments/location-simulation.d.ts.map +1 -1
  27. package/build/src/services/ios/dvt/instruments/location-simulation.js +2 -15
  28. package/build/src/services/ios/dvt/instruments/notifications.d.ts +85 -0
  29. package/build/src/services/ios/dvt/instruments/notifications.d.ts.map +1 -0
  30. package/build/src/services/ios/dvt/instruments/notifications.js +68 -0
  31. package/build/src/services/ios/dvt/instruments/screenshot.d.ts +2 -6
  32. package/build/src/services/ios/dvt/instruments/screenshot.d.ts.map +1 -1
  33. package/build/src/services/ios/dvt/instruments/screenshot.js +2 -11
  34. package/build/src/services.d.ts.map +1 -1
  35. package/build/src/services.js +3 -0
  36. package/package.json +5 -2
  37. package/src/lib/apple-tv/encryption/ed25519.ts +2 -6
  38. package/src/lib/tunnel/packet-stream-client.ts +1 -1
  39. package/src/lib/types.ts +47 -0
  40. package/src/services/ios/dvt/channel.ts +8 -0
  41. package/src/services/ios/dvt/instruments/application-listing.ts +2 -17
  42. package/src/services/ios/dvt/instruments/base-instrument.ts +27 -0
  43. package/src/services/ios/dvt/instruments/condition-inducer.ts +2 -17
  44. package/src/services/ios/dvt/instruments/device-info.ts +2 -15
  45. package/src/services/ios/dvt/instruments/graphics.ts +2 -13
  46. package/src/services/ios/dvt/instruments/location-simulation.ts +2 -18
  47. package/src/services/ios/dvt/instruments/notifications.ts +144 -0
  48. package/src/services/ios/dvt/instruments/screenshot.ts +2 -13
  49. package/src/services.ts +3 -0
@@ -0,0 +1,68 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import { MessageAux } from '../dtx-message.js';
3
+ import { decodeNSKeyedArchiver } from '../nskeyedarchiver-decoder.js';
4
+ import { BaseInstrument } from './base-instrument.js';
5
+ const log = getLogger('Notifications');
6
+ /**
7
+ * Notifications service for monitoring iOS system events
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * for await (const msg of dvtService.notifications.messages()) {
12
+ * if (!msg) continue;
13
+ *
14
+ * if (msg.selector === 'applicationStateNotification:') {
15
+ * const app = msg.data;
16
+ * console.log(`${app.appName}: ${app.state_description}`);
17
+ * }
18
+ * }
19
+ * ```
20
+ */
21
+ export class Notifications extends BaseInstrument {
22
+ /** DTX service identifier for mobile notifications */
23
+ static IDENTIFIER = 'com.apple.instruments.server.services.mobilenotifications';
24
+ async start() {
25
+ await this.initialize();
26
+ const args = new MessageAux().appendObj(true);
27
+ await this.channel.call('setApplicationStateNotificationsEnabled_')(args);
28
+ await this.channel.call('setMemoryNotificationsEnabled_')(args);
29
+ }
30
+ async stop() {
31
+ const args = new MessageAux().appendObj(false);
32
+ await this.channel.call('setApplicationStateNotificationsEnabled_')(args);
33
+ await this.channel.call('setMemoryNotificationsEnabled_')(args);
34
+ }
35
+ /**
36
+ * Yields notification messages from the iOS device
37
+ */
38
+ async *messages() {
39
+ log.debug('Network monitoring has started');
40
+ await this.start();
41
+ try {
42
+ while (true) {
43
+ const [selector, auxiliaries] = await this.channel.receivePlistWithAux();
44
+ // Decode NSKeyedArchiver format in auxiliaries first index
45
+ const decodedData = auxiliaries[0];
46
+ if (decodedData &&
47
+ typeof decodedData === 'object' &&
48
+ decodedData.$archiver === 'NSKeyedArchiver') {
49
+ try {
50
+ const data = decodeNSKeyedArchiver(decodedData);
51
+ yield {
52
+ selector: selector,
53
+ data,
54
+ };
55
+ }
56
+ catch (error) {
57
+ log.warn('Failed to decode NSKeyedArchiver data:', error);
58
+ await this.stop();
59
+ }
60
+ }
61
+ }
62
+ }
63
+ finally {
64
+ log.debug('Network monitoring has ended');
65
+ await this.stop();
66
+ }
67
+ }
68
+ }
@@ -1,13 +1,9 @@
1
- import type { DVTSecureSocketProxyService } from '../index.js';
1
+ import { BaseInstrument } from './base-instrument.js';
2
2
  /**
3
3
  * Screenshot service for capturing device screenshots
4
4
  */
5
- export declare class Screenshot {
6
- private readonly dvt;
5
+ export declare class Screenshot extends BaseInstrument {
7
6
  static readonly IDENTIFIER = "com.apple.instruments.server.services.screenshot";
8
- private channel;
9
- constructor(dvt: DVTSecureSocketProxyService);
10
- initialize(): Promise<void>;
11
7
  /**
12
8
  * Capture a screenshot from the device
13
9
  * @returns The screenshot data as a Buffer
@@ -1 +1 @@
1
- {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../../../../../src/services/ios/dvt/instruments/screenshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAI/D;;GAEG;AACH,qBAAa,UAAU;IAMT,OAAO,CAAC,QAAQ,CAAC,GAAG;IALhC,MAAM,CAAC,QAAQ,CAAC,UAAU,sDAC2B;IAErD,OAAO,CAAC,OAAO,CAAwB;gBAEV,GAAG,EAAE,2BAA2B;IAEvD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAMjC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;CAmBvC"}
1
+ {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../../../../../src/services/ios/dvt/instruments/screenshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAItD;;GAEG;AACH,qBAAa,UAAW,SAAQ,cAAc;IAC5C,MAAM,CAAC,QAAQ,CAAC,UAAU,sDAC2B;IAErD;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;CAmBvC"}
@@ -1,20 +1,11 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
+ import { BaseInstrument } from './base-instrument.js';
2
3
  const log = getLogger('Screenshot');
3
4
  /**
4
5
  * Screenshot service for capturing device screenshots
5
6
  */
6
- export class Screenshot {
7
- dvt;
7
+ export class Screenshot extends BaseInstrument {
8
8
  static IDENTIFIER = 'com.apple.instruments.server.services.screenshot';
9
- channel = null;
10
- constructor(dvt) {
11
- this.dvt = dvt;
12
- }
13
- async initialize() {
14
- if (!this.channel) {
15
- this.channel = await this.dvt.makeChannel(Screenshot.IDENTIFIER);
16
- }
17
- }
18
9
  /**
19
10
  * Capture a screenshot from the device
20
11
  * @returns The screenshot data as a Buffer
@@ -1 +1 @@
1
- {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/services.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAGhF,OAAO,KAAK,EACV,wBAAwB,EACxB,gCAAgC,EAChC,6BAA6B,EAC7B,iCAAiC,EACjC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,gCAAgC,EAChC,aAAa,IAAI,iBAAiB,EAClC,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AACxB,OAAO,UAAU,MAAM,6BAA6B,CAAC;AAqBrD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sCAAsC,CAAC,CAYjD;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,uCAAuC,CAAC,CAYlD;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,6BAA6B,CAAC,CAYxC;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,mCAAmC,CAAC,CAY9C;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAG5B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAOvE;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,wBAAwB,CAAC,CAiCnC;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/services.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAGhF,OAAO,KAAK,EACV,wBAAwB,EACxB,gCAAgC,EAChC,6BAA6B,EAC7B,iCAAiC,EACjC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,gCAAgC,EAChC,aAAa,IAAI,iBAAiB,EAClC,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AACxB,OAAO,UAAU,MAAM,6BAA6B,CAAC;AAsBrD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sCAAsC,CAAC,CAYjD;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,uCAAuC,CAAC,CAYlD;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,6BAA6B,CAAC,CAYxC;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,mCAAmC,CAAC,CAY9C;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAG5B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAOvE;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,wBAAwB,CAAC,CAmCnC;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
@@ -10,6 +10,7 @@ import { ConditionInducer } from './services/ios/dvt/instruments/condition-induc
10
10
  import { DeviceInfo } from './services/ios/dvt/instruments/device-info.js';
11
11
  import { Graphics } from './services/ios/dvt/instruments/graphics.js';
12
12
  import { LocationSimulation } from './services/ios/dvt/instruments/location-simulation.js';
13
+ import { Notifications } from './services/ios/dvt/instruments/notifications.js';
13
14
  import { Screenshot } from './services/ios/dvt/instruments/screenshot.js';
14
15
  import { MisagentService } from './services/ios/misagent/index.js';
15
16
  import { MobileConfigService } from './services/ios/mobile-config/index.js';
@@ -142,6 +143,7 @@ export async function startDVTService(udid) {
142
143
  const appListing = new ApplicationListing(dvtService);
143
144
  const graphics = new Graphics(dvtService);
144
145
  const deviceInfo = new DeviceInfo(dvtService);
146
+ const notification = new Notifications(dvtService);
145
147
  return {
146
148
  remoteXPC: remoteXPC,
147
149
  dvtService,
@@ -151,6 +153,7 @@ export async function startDVTService(udid) {
151
153
  appListing,
152
154
  graphics,
153
155
  deviceInfo,
156
+ notification,
154
157
  };
155
158
  }
156
159
  export async function createRemoteXPCConnection(udid) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-remotexpc",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "main": "build/src/index.js",
5
5
  "types": "build/src/index.d.ts",
6
6
  "type": "module",
@@ -46,6 +46,7 @@
46
46
  "test:dvt:screenshot": "mocha test/integration/dvt_instruments/screenshot-test.ts --exit --timeout 1m",
47
47
  "test:dvt:device-info": "mocha test/integration/dvt_instruments/device-info-test.ts --exit --timeout 1m",
48
48
  "test:dvt:applist": "mocha test/integration/dvt_instruments/app-listing-test.ts --exit --timeout 1m",
49
+ "test:dvt:notification": "mocha test/integration/dvt_instruments/notifications-test.ts --exit --timeout 1m",
49
50
  "test:tunnel-creation": "sudo tsx scripts/test-tunnel-creation.ts",
50
51
  "test:tunnel-creation:lsof": "sudo tsx scripts/test-tunnel-creation.ts --keep-open"
51
52
  },
@@ -69,6 +70,7 @@
69
70
  "@types/chai": "^5.2.1",
70
71
  "@types/chai-as-promised": "^8.0.2",
71
72
  "@types/mocha": "^10.0.10",
73
+ "@types/sinon": "^21.0.0",
72
74
  "chai": "^6.0.1",
73
75
  "chai-as-promised": "^8.0.1",
74
76
  "conventional-changelog-conventionalcommits": "^9.1.0",
@@ -77,6 +79,7 @@
77
79
  "prettier": "^3.5.3",
78
80
  "rimraf": "^6.0.1",
79
81
  "semantic-release": "^25.0.2",
82
+ "sinon": "^21.0.0",
80
83
  "ts-node": "^10.9.2",
81
84
  "tsx": "^4.7.0",
82
85
  "typescript": "^5.2.2"
@@ -84,7 +87,7 @@
84
87
  "dependencies": {
85
88
  "@appium/strongbox": "^1.0.0-rc.1",
86
89
  "@appium/support": "^7.0.0-rc.1",
87
- "@types/node": "^24.0.10",
90
+ "@types/node": "^25.0.2",
88
91
  "@xmldom/xmldom": "^0.9.8",
89
92
  "appium-ios-tuntap": "^0.x",
90
93
  "axios": "^1.12.0",
@@ -1,8 +1,4 @@
1
- import {
2
- type KeyPairKeyObjectResult,
3
- generateKeyPairSync,
4
- sign,
5
- } from 'node:crypto';
1
+ import { generateKeyPairSync, sign } from 'node:crypto';
6
2
 
7
3
  import { getLogger } from '../../logger.js';
8
4
  import { CryptographyError } from '../errors.js';
@@ -24,7 +20,7 @@ const ED25519_PKCS8_PREFIX = Buffer.from(
24
20
  */
25
21
  export function generateEd25519KeyPair(): PairingKeys {
26
22
  try {
27
- const keyPair: KeyPairKeyObjectResult = generateKeyPairSync('ed25519');
23
+ const keyPair = generateKeyPairSync('ed25519');
28
24
 
29
25
  const publicKeyDer = keyPair.publicKey.export({
30
26
  type: 'spki',
@@ -47,7 +47,7 @@ export class PacketStreamClient extends EventEmitter {
47
47
  );
48
48
 
49
49
  this.socket.on('data', (data) => {
50
- this.handleData(data);
50
+ this.handleData(Buffer.isBuffer(data) ? data : Buffer.from(data));
51
51
  });
52
52
 
53
53
  this.socket.once('close', () => {
package/src/lib/types.ts CHANGED
@@ -8,6 +8,7 @@ import type { ServiceConnection } from '../service-connection.js';
8
8
  import type { BaseService, Service } from '../services/ios/base-service.js';
9
9
  import type { iOSApplication } from '../services/ios/dvt/instruments/application-listing.js';
10
10
  import type { LocationCoordinates } from '../services/ios/dvt/instruments/location-simulation.js';
11
+ import type { NotificationMessage } from '../services/ios/dvt/instruments/notifications.js';
11
12
  import { ProvisioningProfile } from '../services/ios/misagent/provisioning-profile.js';
12
13
  import type { PowerAssertionOptions } from '../services/ios/power-assertion/index.js';
13
14
  import { PowerAssertionType } from '../services/ios/power-assertion/index.js';
@@ -796,6 +797,50 @@ export interface DeviceInfoService {
796
797
  nameForGid(gid: number): Promise<string>;
797
798
  }
798
799
 
800
+ /**
801
+ * Notification service monitor memory and app notifications
802
+ */
803
+ export interface NotificationService {
804
+ /**
805
+ * Yields notification from memory and application state changes
806
+ * @example:
807
+ * {
808
+ * selector: 'applicationStateNotification:',
809
+ * data:
810
+ * {
811
+ * mach_absolute_time: 58061793038,
812
+ * execName: '/Applications/Spotlight.app',
813
+ * appName: 'Spotlight',
814
+ * pid: 327,
815
+ * state_description: 'Suspended'
816
+ * }
817
+ * },
818
+ * {
819
+ * selector: 'applicationStateNotification:',
820
+ * data:
821
+ * {
822
+ * mach_absolute_time: 58061827502,
823
+ * execName: '/private/var/containers/Bundle/Application/28AF0B11-363A-4242-9164-CF690064402B/MobileCal.app',
824
+ * appName: 'MobileCal',
825
+ * pid: 449,
826
+ * state_description: 'Suspended'
827
+ * }
828
+ * },
829
+ * {
830
+ * selector: 'memoryLevelNotification:',
831
+ * data:
832
+ * {
833
+ * code: 3,
834
+ * mach_absolute_time: 101524320437,
835
+ * timestamp: [Object],
836
+ * pid: -1
837
+ * }
838
+ *
839
+ * }
840
+ */
841
+ messages(): AsyncGenerator<NotificationMessage, void, undefined>;
842
+ }
843
+
799
844
  /**
800
845
  * DVT service with connection
801
846
  * This allows callers to properly manage the connection lifecycle
@@ -815,6 +860,8 @@ export interface DVTServiceWithConnection {
815
860
  graphics: GraphicsService;
816
861
  /** The DeviceInfo service instance */
817
862
  deviceInfo: DeviceInfoService;
863
+ /** The Notifications service instance */
864
+ notification: NotificationService;
818
865
  /** The RemoteXPC connection that can be used to close the connection */
819
866
  remoteXPC: RemoteXpcConnection;
820
867
  }
@@ -23,6 +23,14 @@ export class Channel {
23
23
  return data;
24
24
  }
25
25
 
26
+ /**
27
+ * Receive a plist response from the channel with auxiliaries
28
+ * @returns Tuple of [selector, auxiliaries]
29
+ */
30
+ async receivePlistWithAux(): Promise<[string, any[]]> {
31
+ return await this.service.recvPlist(this.channelCode);
32
+ }
33
+
26
34
  /**
27
35
  * Call a method on this channel with automatic ObjectiveC selector conversion
28
36
  *
@@ -1,7 +1,6 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
- import type { Channel } from '../channel.js';
3
2
  import { MessageAux } from '../dtx-message.js';
4
- import type { DVTSecureSocketProxyService } from '../index.js';
3
+ import { BaseInstrument } from './base-instrument.js';
5
4
 
6
5
  const log = getLogger('ApplicationListing');
7
6
 
@@ -46,24 +45,10 @@ export interface iOSApplication {
46
45
  /**
47
46
  * Application Listing service for retrieving installed applications
48
47
  */
49
- export class ApplicationListing {
48
+ export class ApplicationListing extends BaseInstrument {
50
49
  static readonly IDENTIFIER =
51
50
  'com.apple.instruments.server.services.device.applictionListing';
52
51
 
53
- private channel: Channel | null = null;
54
-
55
- constructor(private readonly dvt: DVTSecureSocketProxyService) {}
56
-
57
- /**
58
- * Initialize the application listing channel
59
- */
60
- async initialize(): Promise<void> {
61
- if (this.channel) {
62
- return;
63
- }
64
- this.channel = await this.dvt.makeChannel(ApplicationListing.IDENTIFIER);
65
- }
66
-
67
52
  /**
68
53
  * Get the list of installed applications from the device
69
54
  * @returns {Promise<iOSApplication[]>}
@@ -0,0 +1,27 @@
1
+ import type { Channel } from '../channel.js';
2
+ import type { DVTSecureSocketProxyService } from '../index.js';
3
+
4
+ /**
5
+ * Base class for DVT instrument services.
6
+ *
7
+ * Subclasses must define a static `IDENTIFIER` property.
8
+ */
9
+ export abstract class BaseInstrument {
10
+ static readonly IDENTIFIER: string;
11
+
12
+ protected channel: Channel | null = null;
13
+ constructor(protected readonly dvt: DVTSecureSocketProxyService) {}
14
+
15
+ protected get identifier(): string {
16
+ return (this.constructor as typeof BaseInstrument).IDENTIFIER;
17
+ }
18
+
19
+ /**
20
+ * Initialize the instrument channel.
21
+ */
22
+ async initialize(): Promise<void> {
23
+ if (!this.channel) {
24
+ this.channel = await this.dvt.makeChannel(this.identifier);
25
+ }
26
+ }
27
+ }
@@ -1,8 +1,7 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
2
  import type { ConditionGroup } from '../../../../lib/types.js';
3
- import type { Channel } from '../channel.js';
4
3
  import { MessageAux } from '../dtx-message.js';
5
- import type { DVTSecureSocketProxyService } from '../index.js';
4
+ import { BaseInstrument } from './base-instrument.js';
6
5
 
7
6
  const log = getLogger('ConditionInducer');
8
7
 
@@ -10,24 +9,10 @@ const log = getLogger('ConditionInducer');
10
9
  * Condition Inducer service for simulating various device conditions
11
10
  * such as network conditions, thermal states, etc.
12
11
  */
13
- export class ConditionInducer {
12
+ export class ConditionInducer extends BaseInstrument {
14
13
  static readonly IDENTIFIER =
15
14
  'com.apple.instruments.server.services.ConditionInducer';
16
15
 
17
- private channel: Channel | null = null;
18
-
19
- constructor(private readonly dvt: DVTSecureSocketProxyService) {}
20
-
21
- /**
22
- * Initialize the condition inducer channel
23
- */
24
- async initialize(): Promise<void> {
25
- if (this.channel) {
26
- return;
27
- }
28
- this.channel = await this.dvt.makeChannel(ConditionInducer.IDENTIFIER);
29
- }
30
-
31
16
  /**
32
17
  * List all available condition inducers and their profiles
33
18
  * @returns Array of condition groups with their available profiles
@@ -1,9 +1,8 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
2
  import { parseBinaryPlist } from '../../../../lib/plist/index.js';
3
3
  import type { ProcessInfo } from '../../../../lib/types.js';
4
- import type { Channel } from '../channel.js';
5
4
  import { MessageAux } from '../dtx-message.js';
6
- import type { DVTSecureSocketProxyService } from '../index.js';
5
+ import { BaseInstrument } from './base-instrument.js';
7
6
 
8
7
  const log = getLogger('DeviceInfo');
9
8
 
@@ -25,22 +24,10 @@ const log = getLogger('DeviceInfo');
25
24
  * - nameForUid(uid): Get username for UID
26
25
  * - nameForGid(gid): Get group name for GID
27
26
  */
28
- export class DeviceInfo {
27
+ export class DeviceInfo extends BaseInstrument {
29
28
  static readonly IDENTIFIER =
30
29
  'com.apple.instruments.server.services.deviceinfo';
31
30
 
32
- private channel: Channel | null = null;
33
- constructor(private readonly dvt: DVTSecureSocketProxyService) {}
34
-
35
- /**
36
- * Initialize the device info channel
37
- */
38
- async initialize(): Promise<void> {
39
- if (!this.channel) {
40
- this.channel = await this.dvt.makeChannel(DeviceInfo.IDENTIFIER);
41
- }
42
- }
43
-
44
31
  /**
45
32
  * List directory contents at the specified path.
46
33
  * @param path - The directory path to list
@@ -1,24 +1,13 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
- import type { Channel } from '../channel.js';
3
2
  import { MessageAux } from '../dtx-message.js';
4
- import type { DVTSecureSocketProxyService } from '../index.js';
3
+ import { BaseInstrument } from './base-instrument.js';
5
4
 
6
5
  const log = getLogger('Graphics');
7
6
 
8
- export class Graphics {
7
+ export class Graphics extends BaseInstrument {
9
8
  static readonly IDENTIFIER =
10
9
  'com.apple.instruments.server.services.graphics.opengl';
11
10
 
12
- private channel: Channel | null = null;
13
-
14
- constructor(private readonly dvt: DVTSecureSocketProxyService) {}
15
-
16
- async initialize(): Promise<void> {
17
- if (!this.channel) {
18
- this.channel = await this.dvt.makeChannel(Graphics.IDENTIFIER);
19
- }
20
- }
21
-
22
11
  async start(): Promise<void> {
23
12
  await this.initialize();
24
13
 
@@ -1,7 +1,6 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
- import type { Channel } from '../channel.js';
3
2
  import { MessageAux } from '../dtx-message.js';
4
- import type { DVTSecureSocketProxyService } from '../index.js';
3
+ import { BaseInstrument } from './base-instrument.js';
5
4
 
6
5
  const log = getLogger('LocationSimulation');
7
6
 
@@ -16,25 +15,10 @@ export interface LocationCoordinates {
16
15
  /**
17
16
  * Location simulation service for simulating device GPS location
18
17
  */
19
- export class LocationSimulation {
18
+ export class LocationSimulation extends BaseInstrument {
20
19
  static readonly IDENTIFIER =
21
20
  'com.apple.instruments.server.services.LocationSimulation';
22
21
 
23
- private channel: Channel | null = null;
24
-
25
- constructor(private readonly dvt: DVTSecureSocketProxyService) {}
26
-
27
- /**
28
- * Initialize the location simulation channel
29
- */
30
- async initialize(): Promise<void> {
31
- if (this.channel) {
32
- return;
33
- }
34
-
35
- this.channel = await this.dvt.makeChannel(LocationSimulation.IDENTIFIER);
36
- }
37
-
38
22
  /**
39
23
  * Set the simulated GPS location
40
24
  * @param coordinates The location coordinates
@@ -0,0 +1,144 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import { MessageAux } from '../dtx-message.js';
3
+ import { decodeNSKeyedArchiver } from '../nskeyedarchiver-decoder.js';
4
+ import { BaseInstrument } from './base-instrument.js';
5
+
6
+ const log = getLogger('Notifications');
7
+
8
+ /**
9
+ * Application state notification data structure
10
+ */
11
+ export interface ApplicationStateNotificationData {
12
+ /** High-precision Mach absolute time timestamp */
13
+ mach_absolute_time: bigint;
14
+ /** Full path to the executable (e.g., '/private/var/containers/Bundle/Application/.../MobileCal.app') */
15
+ execName: string;
16
+ /** Short application name (e.g., 'MobileCal') */
17
+ appName: string;
18
+ /** Process ID of the application */
19
+ pid: number;
20
+ /** Application state: 'Foreground' | 'Background' | 'Suspended' | 'Terminated' */
21
+ state_description: string;
22
+ }
23
+
24
+ /**
25
+ * Memory level notification data structure
26
+ */
27
+ export interface MemoryLevelNotificationData {
28
+ /** Memory pressure level code (0=Normal, 1=Warning, 2=Critical, 3=...) */
29
+ code: number;
30
+ /** High-precision Mach absolute time timestamp */
31
+ mach_absolute_time: bigint;
32
+ /** NSDate timestamp object with NS.time property */
33
+ timestamp: number;
34
+ /** Process ID (-1 for system-wide notifications) */
35
+ pid: number;
36
+ }
37
+
38
+ /**
39
+ * Application state notification message
40
+ */
41
+ export interface ApplicationStateNotification {
42
+ selector: 'applicationStateNotification:';
43
+ data: ApplicationStateNotificationData;
44
+ }
45
+
46
+ /**
47
+ * Memory level notification message
48
+ */
49
+ export interface MemoryLevelNotification {
50
+ selector: 'memoryLevelNotification:';
51
+ data: MemoryLevelNotificationData;
52
+ }
53
+
54
+ /**
55
+ * Monitor memory and app notification
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * for await (const msg of notifications.messages()) {
60
+ * if (!msg) continue;
61
+ *
62
+ * if (msg.selector === 'applicationStateNotification:') {
63
+ * const notif = msg.data;
64
+ * console.log(`${notif.appName} is ${notif.state_description}`);
65
+ * }
66
+ * }
67
+ * ```
68
+ */
69
+ export type NotificationMessage =
70
+ | ApplicationStateNotification
71
+ | MemoryLevelNotification;
72
+
73
+ /**
74
+ * Notifications service for monitoring iOS system events
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * for await (const msg of dvtService.notifications.messages()) {
79
+ * if (!msg) continue;
80
+ *
81
+ * if (msg.selector === 'applicationStateNotification:') {
82
+ * const app = msg.data;
83
+ * console.log(`${app.appName}: ${app.state_description}`);
84
+ * }
85
+ * }
86
+ * ```
87
+ */
88
+ export class Notifications extends BaseInstrument {
89
+ /** DTX service identifier for mobile notifications */
90
+ static readonly IDENTIFIER =
91
+ 'com.apple.instruments.server.services.mobilenotifications';
92
+
93
+ async start(): Promise<void> {
94
+ await this.initialize();
95
+ const args = new MessageAux().appendObj(true);
96
+ await this.channel!.call('setApplicationStateNotificationsEnabled_')(args);
97
+ await this.channel!.call('setMemoryNotificationsEnabled_')(args);
98
+ }
99
+
100
+ async stop(): Promise<void> {
101
+ const args = new MessageAux().appendObj(false);
102
+ await this.channel!.call('setApplicationStateNotificationsEnabled_')(args);
103
+ await this.channel!.call('setMemoryNotificationsEnabled_')(args);
104
+ }
105
+
106
+ /**
107
+ * Yields notification messages from the iOS device
108
+ */
109
+ async *messages(): AsyncGenerator<NotificationMessage, void, undefined> {
110
+ log.debug('Network monitoring has started');
111
+ await this.start();
112
+
113
+ try {
114
+ while (true) {
115
+ const [selector, auxiliaries] =
116
+ await this.channel!.receivePlistWithAux();
117
+
118
+ // Decode NSKeyedArchiver format in auxiliaries first index
119
+ const decodedData = auxiliaries[0];
120
+ if (
121
+ decodedData &&
122
+ typeof decodedData === 'object' &&
123
+ decodedData.$archiver === 'NSKeyedArchiver'
124
+ ) {
125
+ try {
126
+ const data = decodeNSKeyedArchiver(decodedData);
127
+ yield {
128
+ selector: selector as
129
+ | 'applicationStateNotification:'
130
+ | 'memoryLevelNotification:',
131
+ data,
132
+ } as NotificationMessage;
133
+ } catch (error) {
134
+ log.warn('Failed to decode NSKeyedArchiver data:', error);
135
+ await this.stop();
136
+ }
137
+ }
138
+ }
139
+ } finally {
140
+ log.debug('Network monitoring has ended');
141
+ await this.stop();
142
+ }
143
+ }
144
+ }
@@ -1,26 +1,15 @@
1
1
  import { getLogger } from '../../../../lib/logger.js';
2
- import type { Channel } from '../channel.js';
3
- import type { DVTSecureSocketProxyService } from '../index.js';
2
+ import { BaseInstrument } from './base-instrument.js';
4
3
 
5
4
  const log = getLogger('Screenshot');
6
5
 
7
6
  /**
8
7
  * Screenshot service for capturing device screenshots
9
8
  */
10
- export class Screenshot {
9
+ export class Screenshot extends BaseInstrument {
11
10
  static readonly IDENTIFIER =
12
11
  'com.apple.instruments.server.services.screenshot';
13
12
 
14
- private channel: Channel | null = null;
15
-
16
- constructor(private readonly dvt: DVTSecureSocketProxyService) {}
17
-
18
- async initialize(): Promise<void> {
19
- if (!this.channel) {
20
- this.channel = await this.dvt.makeChannel(Screenshot.IDENTIFIER);
21
- }
22
- }
23
-
24
13
  /**
25
14
  * Capture a screenshot from the device
26
15
  * @returns The screenshot data as a Buffer