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.
- package/CHANGELOG.md +6 -0
- package/build/src/index.d.ts +2 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +1 -0
- package/build/src/index.js.map +1 -1
- package/build/src/lib/plist/binary-plist-parser.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-parser.js +17 -0
- package/build/src/lib/plist/binary-plist-parser.js.map +1 -1
- package/build/src/lib/types.d.ts +144 -0
- package/build/src/lib/types.d.ts.map +1 -1
- package/build/src/services/ios/dvt/instruments/device-info.d.ts +15 -0
- package/build/src/services/ios/dvt/instruments/device-info.d.ts.map +1 -1
- package/build/src/services/ios/dvt/instruments/device-info.js +30 -0
- package/build/src/services/ios/dvt/instruments/device-info.js.map +1 -1
- package/build/src/services/ios/dvt/instruments/sysmontap.d.ts +115 -0
- package/build/src/services/ios/dvt/instruments/sysmontap.d.ts.map +1 -0
- package/build/src/services/ios/dvt/instruments/sysmontap.js +259 -0
- package/build/src/services/ios/dvt/instruments/sysmontap.js.map +1 -0
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -1
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +7 -2
- package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js.map +1 -1
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +2 -0
- package/build/src/services.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +6 -0
- package/src/lib/plist/binary-plist-parser.ts +19 -0
- package/src/lib/types.ts +158 -0
- package/src/services/ios/dvt/instruments/device-info.ts +46 -0
- package/src/services/ios/dvt/instruments/sysmontap.ts +308 -0
- package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +9 -2
- 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
|
-
|
|
240
|
-
|
|
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
|
|