appium-ios-remotexpc 5.2.2 → 5.3.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 (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/src/index.d.ts +2 -1
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/index.js +1 -0
  5. package/build/src/index.js.map +1 -1
  6. package/build/src/lib/plist/binary-plist-parser.d.ts.map +1 -1
  7. package/build/src/lib/plist/binary-plist-parser.js +17 -0
  8. package/build/src/lib/plist/binary-plist-parser.js.map +1 -1
  9. package/build/src/lib/types.d.ts +144 -0
  10. package/build/src/lib/types.d.ts.map +1 -1
  11. package/build/src/services/ios/dvt/instruments/device-info.d.ts +15 -0
  12. package/build/src/services/ios/dvt/instruments/device-info.d.ts.map +1 -1
  13. package/build/src/services/ios/dvt/instruments/device-info.js +30 -0
  14. package/build/src/services/ios/dvt/instruments/device-info.js.map +1 -1
  15. package/build/src/services/ios/dvt/instruments/sysmontap.d.ts +115 -0
  16. package/build/src/services/ios/dvt/instruments/sysmontap.d.ts.map +1 -0
  17. package/build/src/services/ios/dvt/instruments/sysmontap.js +259 -0
  18. package/build/src/services/ios/dvt/instruments/sysmontap.js.map +1 -0
  19. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -1
  20. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +7 -2
  21. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js.map +1 -1
  22. package/build/src/services.d.ts.map +1 -1
  23. package/build/src/services.js +2 -0
  24. package/build/src/services.js.map +1 -1
  25. package/package.json +2 -1
  26. package/src/index.ts +6 -0
  27. package/src/lib/plist/binary-plist-parser.ts +19 -0
  28. package/src/lib/types.ts +158 -0
  29. package/src/services/ios/dvt/instruments/device-info.ts +46 -0
  30. package/src/services/ios/dvt/instruments/sysmontap.ts +308 -0
  31. package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +9 -2
  32. package/src/services.ts +2 -0
@@ -189,6 +189,52 @@ export class DeviceInfo extends BaseInstrument {
189
189
  );
190
190
  }
191
191
 
192
+ /**
193
+ * Get the list of process attribute names supported by the sysmontap
194
+ * instrument. The returned order matches the per-process value tuples
195
+ * emitted by the sysmontap service, so it is used to label those values.
196
+ * @returns Array of process attribute names (e.g. 'pid', 'name', 'cpuUsage')
197
+ */
198
+ async sysmonProcessAttributes(): Promise<string[]> {
199
+ return this.expectStringArrayResult(
200
+ await this.requestInformation('sysmonProcessAttributes'),
201
+ 'sysmonProcessAttributes',
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Get the list of system attribute names supported by the sysmontap
207
+ * instrument. The returned order matches the system value tuple emitted by
208
+ * the sysmontap service, so it is used to label those values.
209
+ * @returns Array of system attribute names (e.g. 'vmPageSize', 'physMemSize')
210
+ */
211
+ async sysmonSystemAttributes(): Promise<string[]> {
212
+ return this.expectStringArrayResult(
213
+ await this.requestInformation('sysmonSystemAttributes'),
214
+ 'sysmonSystemAttributes',
215
+ );
216
+ }
217
+
218
+ private expectStringArrayResult(result: unknown, context: string): string[] {
219
+ if (
220
+ Array.isArray(result) &&
221
+ result.every((item) => typeof item === 'string')
222
+ ) {
223
+ return result;
224
+ }
225
+
226
+ if (hasNSErrorIndicators(result)) {
227
+ const description =
228
+ (result as { NSUserInfo?: { NSLocalizedDescription?: string } })
229
+ .NSUserInfo?.NSLocalizedDescription ?? JSON.stringify(result);
230
+ throw new Error(`${context}: ${description}`);
231
+ }
232
+
233
+ throw new Error(
234
+ `${context}: expected string array, got ${typeof result} (${JSON.stringify(result)})`,
235
+ );
236
+ }
237
+
192
238
  private expectStringResult(result: unknown, context: string): string {
193
239
  if (typeof result === 'string') {
194
240
  return result;
@@ -0,0 +1,308 @@
1
+ import { util } from '@appium/support';
2
+
3
+ import { getLogger } from '../../../../lib/logger.js';
4
+ import type {
5
+ SysmonProcessInfo,
6
+ SysmonSample,
7
+ SysmonSystemInfo,
8
+ SysmontapOptions,
9
+ } from '../../../../lib/types.js';
10
+ import { MessageAux } from '../dtx-message.js';
11
+ import { BaseInstrument } from './base-instrument.js';
12
+ import { DeviceInfo } from './device-info.js';
13
+
14
+ const log = getLogger('Sysmontap');
15
+
16
+ /**
17
+ * Sysmontap provides real-time sampling of per-process and system resource
18
+ * usage (CPU, memory, disk, etc.) on iOS devices.
19
+ *
20
+ * The instrument is configured with a set of process and system attribute
21
+ * names (queried from the device via {@link DeviceInfo} unless overridden) and
22
+ * a sampling interval. Once started, the device streams samples; each sample
23
+ * carries the raw value tuples that are mapped back to the attribute names.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const { sysmontap, dvtService } = await Services.startDVTService(udid);
28
+ * try {
29
+ * await sysmontap.configure({ intervalMs: 1000 });
30
+ * for await (const processes of sysmontap.iterProcesses()) {
31
+ * console.log(processes.length, 'processes');
32
+ * break;
33
+ * }
34
+ * } finally {
35
+ * await sysmontap.stop();
36
+ * await dvtService.close();
37
+ * }
38
+ * ```
39
+ */
40
+ export class Sysmontap extends BaseInstrument {
41
+ static readonly IDENTIFIER =
42
+ 'com.apple.instruments.server.services.sysmontap';
43
+
44
+ /** Default sampling interval in milliseconds. */
45
+ static readonly DEFAULT_INTERVAL_MS = 500;
46
+
47
+ /** Minimum permitted sampling interval in milliseconds. */
48
+ static readonly MINIMUM_INTERVAL_MS = 1;
49
+
50
+ private processAttributes: string[] = [];
51
+ private systemAttributes: string[] = [];
52
+ private builtConfig: Record<string, unknown> | null = null;
53
+ private started = false;
54
+ private stopRequested = false;
55
+ private receiveAbortController: AbortController | null = null;
56
+
57
+ /**
58
+ * The process attribute names currently in effect. Empty until
59
+ * {@link configure} (or {@link start}) has run.
60
+ */
61
+ getProcessAttributes(): string[] {
62
+ return [...this.processAttributes];
63
+ }
64
+
65
+ /**
66
+ * The system attribute names currently in effect. Empty until
67
+ * {@link configure} (or {@link start}) has run.
68
+ */
69
+ getSystemAttributes(): string[] {
70
+ return [...this.systemAttributes];
71
+ }
72
+
73
+ /**
74
+ * Resolve the sampling attributes (querying the device when not overridden)
75
+ * and build the sampling configuration. This does not start sampling; the
76
+ * configuration is (re)applied to the device by {@link start}. Safe to call
77
+ * multiple times; the latest configuration wins.
78
+ * @param options Optional sampling configuration
79
+ */
80
+ async configure(options: SysmontapOptions = {}): Promise<void> {
81
+ if (options.processAttributes && options.systemAttributes) {
82
+ this.processAttributes = options.processAttributes;
83
+ this.systemAttributes = options.systemAttributes;
84
+ } else {
85
+ const deviceInfo = new DeviceInfo(this.dvt);
86
+ this.processAttributes =
87
+ options.processAttributes ??
88
+ (await deviceInfo.sysmonProcessAttributes());
89
+ this.systemAttributes =
90
+ options.systemAttributes ?? (await deviceInfo.sysmonSystemAttributes());
91
+ }
92
+
93
+ const intervalMs = Math.max(
94
+ options.intervalMs ?? Sysmontap.DEFAULT_INTERVAL_MS,
95
+ Sysmontap.MINIMUM_INTERVAL_MS,
96
+ );
97
+
98
+ this.builtConfig = {
99
+ ur: Sysmontap.MINIMUM_INTERVAL_MS, // Output frequency (ms)
100
+ bm: 0,
101
+ procAttrs: this.processAttributes,
102
+ sysAttrs: this.systemAttributes,
103
+ cpuUsage: true,
104
+ physFootprint: true, // Include physical memory footprint
105
+ sampleInterval: intervalMs * 1_000_000, // Sample interval in nanoseconds
106
+ };
107
+
108
+ log.debug(
109
+ `sysmontap configured: interval=${intervalMs}ms, ` +
110
+ `${util.pluralize('process attribute', this.processAttributes.length, true)}, ` +
111
+ `${util.pluralize('system attribute', this.systemAttributes.length, true)}`,
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Begin sampling. Resolves the configuration first (with defaults) when
117
+ * {@link configure} has not been called.
118
+ *
119
+ * A sysmontap instance supports a single sampling session per DVT
120
+ * connection: once a stream has been {@link stop}ped, the device does not
121
+ * resume it on the same connection. To sample again, start a new DVT
122
+ * connection (e.g. via `Services.startDVTService`).
123
+ */
124
+ async start(): Promise<void> {
125
+ if (this.started) {
126
+ log.debug('sysmontap already sampling; start() is a no-op');
127
+ return;
128
+ }
129
+ if (!this.builtConfig) {
130
+ await this.configure();
131
+ }
132
+ await this.initialize();
133
+ const channel = this.requireChannel();
134
+
135
+ // setConfig must precede start so the device knows which attributes to
136
+ // sample.
137
+ const args = new MessageAux().appendObj(this.builtConfig);
138
+ await channel.call('setConfig_')(args, false);
139
+
140
+ this.stopRequested = false;
141
+ await channel.call('start')(undefined, false);
142
+ this.started = true;
143
+ log.debug('sysmontap sampling started');
144
+ }
145
+
146
+ /**
147
+ * Stop sampling and unblock any in-flight iterator.
148
+ */
149
+ async stop(): Promise<void> {
150
+ this.stopRequested = true;
151
+ this.receiveAbortController?.abort();
152
+
153
+ if (!this.started) {
154
+ return;
155
+ }
156
+ this.started = false;
157
+
158
+ if (this.channel) {
159
+ try {
160
+ await this.requireChannel().call('stop')(undefined, false);
161
+ } catch (error) {
162
+ log.debug(
163
+ 'sysmontap stop() could not notify the device:',
164
+ error instanceof Error ? error.message : error,
165
+ );
166
+ }
167
+ }
168
+ log.debug('sysmontap sampling stopped');
169
+ }
170
+
171
+ /**
172
+ * Async iterator that yields raw sysmontap samples as they arrive. The
173
+ * device interleaves two kinds of samples: system samples (with `System`,
174
+ * `SystemAttributes`, CPU usage) and process samples (with `Processes`).
175
+ * Internal control/heartbeat frames are filtered out.
176
+ *
177
+ * Sampling starts automatically on first iteration and stops when iteration
178
+ * terminates (via break or return), when {@link stop} is called, or when the
179
+ * underlying DVT connection is closed. The iterator never throws on a read
180
+ * failure: it ends the stream instead, so a consumer's `for await` does not
181
+ * have to guard against connection teardown.
182
+ */
183
+ async *messages(): AsyncGenerator<SysmonSample, void, unknown> {
184
+ await this.start();
185
+
186
+ try {
187
+ while (!this.stopRequested) {
188
+ const channel = this.requireChannel();
189
+ const receiveAbortController = new AbortController();
190
+ this.receiveAbortController = receiveAbortController;
191
+
192
+ let plist: unknown;
193
+ try {
194
+ plist = await channel.receivePlist(receiveAbortController.signal);
195
+ } catch (err) {
196
+ // End the stream rather than throwing out of the async iterator
197
+ if (
198
+ !this.stopRequested &&
199
+ !this.isAbortError(err) &&
200
+ !this.isConnectionClosedError(err)
201
+ ) {
202
+ log.warn('sysmontap stream ended due to an unexpected error:', err);
203
+ }
204
+ break;
205
+ } finally {
206
+ if (this.receiveAbortController === receiveAbortController) {
207
+ this.receiveAbortController = null;
208
+ }
209
+ }
210
+
211
+ // Data samples arrive as an array of one or more sample dictionaries.
212
+ // Other payloads (e.g. `{ DTTapMessagePlist: { k: 8, heart } }` control
213
+ // and heartbeat frames, or a null parse failure) are not samples and
214
+ // are skipped.
215
+ if (!Array.isArray(plist)) {
216
+ continue;
217
+ }
218
+
219
+ for (const row of plist) {
220
+ if (row && typeof row === 'object') {
221
+ yield row as SysmonSample;
222
+ }
223
+ }
224
+ }
225
+ } finally {
226
+ await this.stop();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Async iterator that yields labelled per-process snapshots. Each yielded
232
+ * value is the list of processes contained in a single sample, with the raw
233
+ * per-process value tuples mapped to objects keyed by the configured process
234
+ * attribute names.
235
+ *
236
+ * Note: the first emitted snapshot typically contains uninitialised
237
+ * `cpuUsage` values and is commonly skipped by consumers.
238
+ */
239
+ async *iterProcesses(): AsyncGenerator<SysmonProcessInfo[], void, unknown> {
240
+ for await (const sample of this.messages()) {
241
+ const processes = sample.Processes;
242
+ if (!processes || typeof processes !== 'object') {
243
+ continue;
244
+ }
245
+
246
+ const entries: SysmonProcessInfo[] = [];
247
+ for (const values of Object.values(processes)) {
248
+ if (Array.isArray(values)) {
249
+ entries.push(this.zipAttributes(this.processAttributes, values));
250
+ }
251
+ }
252
+ yield entries;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Async iterator that yields labelled system snapshots. Each yielded value is
258
+ * the device-wide metrics from a single system sample, with the raw value
259
+ * tuple mapped to an object keyed by the configured system attribute names.
260
+ *
261
+ * This is the system-sample counterpart to {@link iterProcesses}.
262
+ */
263
+ async *iterSystem(): AsyncGenerator<SysmonSystemInfo, void, unknown> {
264
+ for await (const sample of this.messages()) {
265
+ const system = sample.System;
266
+ if (!Array.isArray(system)) {
267
+ continue;
268
+ }
269
+ yield this.zipAttributes(this.systemAttributes, system);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Zip an ordered list of attribute names with a raw value tuple.
275
+ */
276
+ private zipAttributes(
277
+ attributes: string[],
278
+ values: unknown[],
279
+ ): Record<string, unknown> {
280
+ const result: Record<string, unknown> = {};
281
+ for (let i = 0; i < attributes.length; i++) {
282
+ result[attributes[i]] = values[i];
283
+ }
284
+ return result;
285
+ }
286
+
287
+ private isAbortError(err: unknown): boolean {
288
+ return (
289
+ err instanceof DOMException ||
290
+ (err instanceof Error && err.name === 'AbortError')
291
+ );
292
+ }
293
+
294
+ /**
295
+ * Whether an error indicates the underlying socket/DVT connection was closed,
296
+ * destroyed, or otherwise torn down (an expected way for the stream to end).
297
+ * Closing the DVT connection clears its per-channel fragmenters, so a pending
298
+ * read can surface as "No fragmenter for channel" or "Not connected".
299
+ */
300
+ private isConnectionClosedError(err: unknown): boolean {
301
+ return (
302
+ err instanceof Error &&
303
+ /socket|destroyed|closed|not initialized|not available|not connected|no fragmenter|EPIPE|ECONNRESET/i.test(
304
+ err.message,
305
+ )
306
+ );
307
+ }
308
+ }
@@ -236,8 +236,15 @@ export class NSKeyedArchiverDecoder {
236
236
  const key = this.decodeObject(keyRefs[i], visited, depth + 1);
237
237
  const value = this.decodeObject(valueRefs[i], visited, depth + 1);
238
238
 
239
- if (typeof key === 'string') {
240
- result[key] = value;
239
+ // Object keys are strings in JS. Coerce primitive keys (e.g. the integer
240
+ // pids used by the sysmontap `Processes` map) so they are not dropped.
241
+ if (
242
+ typeof key === 'string' ||
243
+ typeof key === 'number' ||
244
+ typeof key === 'bigint' ||
245
+ typeof key === 'boolean'
246
+ ) {
247
+ result[String(key)] = value;
241
248
  }
242
249
  }
243
250
 
package/src/services.ts CHANGED
@@ -22,6 +22,7 @@ import { NetworkMonitor } from './services/ios/dvt/instruments/network-monitor.j
22
22
  import { Notifications } from './services/ios/dvt/instruments/notifications.js';
23
23
  import { ProcessControl } from './services/ios/dvt/instruments/process-control.js';
24
24
  import { Screenshot } from './services/ios/dvt/instruments/screenshot.js';
25
+ import { Sysmontap } from './services/ios/dvt/instruments/sysmontap.js';
25
26
  import { HidIndigoService } from './services/ios/hid-indigo/index.js';
26
27
  import { HouseArrestService } from './services/ios/house-arrest/index.js';
27
28
  import { InstallationProxyService } from './services/ios/installation-proxy/index.js';
@@ -245,6 +246,7 @@ export async function startDVTService(udid: string): Promise<DVTInstruments> {
245
246
  notification: new Notifications(dvtService),
246
247
  networkMonitor: new NetworkMonitor(dvtService),
247
248
  processControl: new ProcessControl(dvtService),
249
+ sysmontap: new Sysmontap(dvtService),
248
250
  };
249
251
  }
250
252