appium-ios-remotexpc 0.9.0 → 0.10.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.
@@ -0,0 +1,385 @@
1
+ import { logger } from '@appium/support';
2
+ import fs from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { Readable, Writable } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { buildClosePayload, buildFopenPayload, buildReadPayload, buildRemovePayload, buildRenamePayload, buildStatPayload, nanosecondsToMilliseconds, nextReadChunkSize, parseCStringArray, parseKeyValueNullList, readAfcResponse, rsdHandshakeForRawService, sendAfcPacket, writeUInt64LE, } from './codec.js';
8
+ import { AFC_FOPEN_TEXTUAL_MODES, AFC_WRITE_THIS_LENGTH } from './constants.js';
9
+ import { AfcError, AfcFileMode, AfcOpcode } from './enums.js';
10
+ import { createAfcReadStream, createAfcWriteStream } from './stream-utils.js';
11
+ const log = logger.getLogger('AfcService');
12
+ const NON_LISTABLE_ENTRIES = ['', '.', '..'];
13
+ /**
14
+ * AFC client over RSD (Remote XPC shim).
15
+ * After RSDCheckin, speaks raw AFC protocol on the same socket.
16
+ */
17
+ export class AfcService {
18
+ address;
19
+ static RSD_SERVICE_NAME = 'com.apple.afc.shim.remote';
20
+ socket = null;
21
+ packetNum = 0n;
22
+ silent = false;
23
+ constructor(address, silent) {
24
+ this.address = address;
25
+ this.silent = silent ?? process.env.NODE_ENV !== 'test';
26
+ }
27
+ /**
28
+ * List directory entries. Returned entries do not include '.' and '..'
29
+ */
30
+ async listdir(dirPath) {
31
+ const data = await this._doOperation(AfcOpcode.READ_DIR, buildStatPayload(dirPath));
32
+ const entries = parseCStringArray(data);
33
+ return entries.filter((x) => !NON_LISTABLE_ENTRIES.includes(x));
34
+ }
35
+ async stat(filePath) {
36
+ log.debug(`Getting file info for: ${filePath}`);
37
+ try {
38
+ const data = await this._doOperation(AfcOpcode.GET_FILE_INFO, buildStatPayload(filePath));
39
+ const kv = parseKeyValueNullList(data);
40
+ const out = {
41
+ st_size: BigInt(kv.st_size),
42
+ st_blocks: Number.parseInt(kv.st_blocks, 10),
43
+ st_mtime: new Date(nanosecondsToMilliseconds(kv.st_mtime)),
44
+ st_birthtime: new Date(nanosecondsToMilliseconds(kv.st_birthtime)),
45
+ st_nlink: Number.parseInt(kv.st_nlink, 10),
46
+ };
47
+ for (const [k, v] of Object.entries(kv)) {
48
+ if (!(k in out)) {
49
+ out[k] = v;
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ catch (error) {
55
+ if (!this.silent) {
56
+ log.error(`Failed to stat file '${filePath}':`, error);
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ async isdir(filePath) {
62
+ const st = await this.stat(filePath);
63
+ return st.st_ifmt === AfcFileMode.S_IFDIR;
64
+ }
65
+ async exists(filePath) {
66
+ try {
67
+ await this.stat(filePath);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ async fopen(filePath, mode = 'r') {
75
+ const afcMode = AFC_FOPEN_TEXTUAL_MODES[mode];
76
+ if (!afcMode) {
77
+ const allowedModes = Object.keys(AFC_FOPEN_TEXTUAL_MODES).join(', ');
78
+ if (!this.silent) {
79
+ log.error(`Invalid fopen mode '${mode}'. Allowed modes: ${allowedModes}`);
80
+ }
81
+ throw new Error(`Invalid fopen mode '${mode}'. Allowed: ${allowedModes}`);
82
+ }
83
+ log.debug(`Opening file '${filePath}' with mode '${mode}'`);
84
+ try {
85
+ const data = await this._doOperation(AfcOpcode.FILE_OPEN, buildFopenPayload(afcMode, filePath));
86
+ // Response data contains UInt64LE 'handle'
87
+ const handle = data.readBigUInt64LE(0);
88
+ log.debug(`File opened successfully, handle: ${handle}`);
89
+ return handle;
90
+ }
91
+ catch (error) {
92
+ if (!this.silent) {
93
+ log.error(`Failed to open file '${filePath}' with mode '${mode}':`, error);
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ async fclose(handle) {
99
+ await this._doOperation(AfcOpcode.FILE_CLOSE, buildClosePayload(handle));
100
+ }
101
+ createReadStream(handle, size) {
102
+ return createAfcReadStream(handle, size, this._dispatch.bind(this), this._receive.bind(this));
103
+ }
104
+ createWriteStream(handle, chunkSize) {
105
+ return createAfcWriteStream(handle, this._dispatch.bind(this), this._receive.bind(this), chunkSize);
106
+ }
107
+ async fread(handle, size) {
108
+ log.debug(`Reading ${size} bytes from handle ${handle}`);
109
+ const stream = this.createReadStream(handle, size);
110
+ const chunks = [];
111
+ for await (const chunk of stream) {
112
+ chunks.push(chunk);
113
+ }
114
+ const buffer = Buffer.concat(chunks);
115
+ log.debug(`Successfully read ${buffer.length} bytes`);
116
+ return buffer;
117
+ }
118
+ async fwrite(handle, data, chunkSize = data.length) {
119
+ log.debug(`Writing ${data.length} bytes to handle ${handle}`);
120
+ const effectiveChunkSize = chunkSize;
121
+ let offset = 0;
122
+ let chunkCount = 0;
123
+ while (offset < data.length) {
124
+ const end = Math.min(offset + effectiveChunkSize, data.length);
125
+ const chunk = data.subarray(offset, end);
126
+ chunkCount++;
127
+ await this._dispatch(AfcOpcode.WRITE, Buffer.concat([writeUInt64LE(handle), chunk]), AFC_WRITE_THIS_LENGTH);
128
+ const { status } = await this._receive();
129
+ if (status !== AfcError.SUCCESS) {
130
+ const errorName = AfcError[status] || 'UNKNOWN';
131
+ if (!this.silent) {
132
+ log.error(`Write operation failed at offset ${offset} with status ${errorName} (${status})`);
133
+ }
134
+ throw new Error(`fwrite chunk failed with ${errorName} (${status}) at offset ${offset}`);
135
+ }
136
+ offset = end;
137
+ }
138
+ log.debug(`Successfully wrote ${data.length} bytes in ${chunkCount} chunks`);
139
+ }
140
+ async getFileContents(filePath) {
141
+ log.debug(`Reading file contents: ${filePath}`);
142
+ const resolved = await this._resolvePath(filePath);
143
+ const st = await this.stat(resolved);
144
+ if (st.st_ifmt !== AfcFileMode.S_IFREG) {
145
+ if (!this.silent) {
146
+ log.error(`Path '${resolved}' is not a regular file (type: ${st.st_ifmt})`);
147
+ }
148
+ throw new Error(`'${resolved}' isn't a regular file`);
149
+ }
150
+ const h = await this.fopen(resolved, 'r');
151
+ try {
152
+ const buf = await this.fread(h, st.st_size);
153
+ log.debug(`Successfully read ${buf.length} bytes from ${filePath}`);
154
+ return buf;
155
+ }
156
+ finally {
157
+ await this.fclose(h);
158
+ }
159
+ }
160
+ async setFileContents(filePath, data) {
161
+ log.debug(`Writing ${data.length} bytes to file: ${filePath}`);
162
+ const h = await this.fopen(filePath, 'w');
163
+ try {
164
+ await this.fwrite(h, data);
165
+ log.debug(`Successfully wrote file: ${filePath}`);
166
+ }
167
+ catch (error) {
168
+ await this.rmSingle(filePath, true);
169
+ throw error;
170
+ }
171
+ finally {
172
+ await this.fclose(h);
173
+ }
174
+ }
175
+ async readToStream(filePath) {
176
+ log.debug(`Creating read stream for: ${filePath}`);
177
+ const resolved = await this._resolvePath(filePath);
178
+ const st = await this.stat(resolved);
179
+ if (st.st_ifmt !== AfcFileMode.S_IFREG) {
180
+ throw new Error(`'${resolved}' isn't a regular file`);
181
+ }
182
+ const handle = await this.fopen(resolved, 'r');
183
+ const stream = this.createReadStream(handle, st.st_size);
184
+ stream.once('close', () => this.fclose(handle).catch(() => { }));
185
+ return stream;
186
+ }
187
+ async writeFromStream(filePath, stream) {
188
+ log.debug(`Writing stream to file: ${filePath}`);
189
+ const handle = await this.fopen(filePath, 'w');
190
+ const writeStream = this.createWriteStream(handle);
191
+ try {
192
+ await pipeline(stream, writeStream);
193
+ log.debug(`Successfully wrote file: ${filePath}`);
194
+ }
195
+ catch (error) {
196
+ await this.rmSingle(filePath, true);
197
+ throw error;
198
+ }
199
+ finally {
200
+ await this.fclose(handle);
201
+ }
202
+ }
203
+ async pull(remoteSrc, localDst) {
204
+ log.debug(`Pulling file from '${remoteSrc}' to '${localDst}'`);
205
+ const stream = await this.readToStream(remoteSrc);
206
+ const writeStream = fs.createWriteStream(localDst);
207
+ await pipeline(stream, writeStream);
208
+ log.debug(`Successfully pulled file to '${localDst}'`);
209
+ }
210
+ async rmSingle(filePath, force = false) {
211
+ log.debug(`Removing single path: ${filePath} (force: ${force})`);
212
+ try {
213
+ await this._doOperation(AfcOpcode.REMOVE_PATH, buildRemovePayload(filePath));
214
+ log.debug(`Successfully removed: ${filePath}`);
215
+ return true;
216
+ }
217
+ catch (error) {
218
+ if (force) {
219
+ log.debug(`Failed to remove '${filePath}' (ignored due to force=true):`, error);
220
+ return false;
221
+ }
222
+ if (!this.silent) {
223
+ log.error(`Failed to remove '${filePath}':`, error);
224
+ }
225
+ throw error;
226
+ }
227
+ }
228
+ async rm(filePath, force = false) {
229
+ if (!(await this.exists(filePath))) {
230
+ return force ? [] : [filePath];
231
+ }
232
+ if (!(await this.isdir(filePath))) {
233
+ if (await this.rmSingle(filePath, force)) {
234
+ return [];
235
+ }
236
+ return [filePath];
237
+ }
238
+ const failedPaths = [];
239
+ for (const entry of await this.listdir(filePath)) {
240
+ const cur = path.posix.join(filePath, entry);
241
+ if (await this.isdir(cur)) {
242
+ const sub = await this.rm(cur, true);
243
+ failedPaths.push(...sub);
244
+ }
245
+ else {
246
+ if (!(await this.rmSingle(cur, true))) {
247
+ failedPaths.push(cur);
248
+ }
249
+ }
250
+ }
251
+ try {
252
+ if (!(await this.rmSingle(filePath, force))) {
253
+ failedPaths.push(filePath);
254
+ }
255
+ }
256
+ catch (err) {
257
+ if (failedPaths.length) {
258
+ failedPaths.push(filePath);
259
+ }
260
+ else {
261
+ throw err;
262
+ }
263
+ }
264
+ return failedPaths;
265
+ }
266
+ async rename(src, dst) {
267
+ log.debug(`Renaming '${src}' to '${dst}'`);
268
+ try {
269
+ await this._doOperation(AfcOpcode.RENAME_PATH, buildRenamePayload(src, dst));
270
+ log.debug(`Successfully renamed '${src}' to '${dst}'`);
271
+ }
272
+ catch (error) {
273
+ if (!this.silent) {
274
+ log.error(`Failed to rename '${src}' to '${dst}':`, error);
275
+ }
276
+ throw error;
277
+ }
278
+ }
279
+ async push(localSrc, remoteDst) {
280
+ log.debug(`Pushing file from '${localSrc}' to '${remoteDst}'`);
281
+ const readStream = fs.createReadStream(localSrc);
282
+ await this.writeFromStream(remoteDst, readStream);
283
+ log.debug(`Successfully pushed file to '${remoteDst}'`);
284
+ }
285
+ async walk(root) {
286
+ const out = [];
287
+ const entries = await this.listdir(root);
288
+ const dirs = [];
289
+ const files = [];
290
+ for (const e of entries) {
291
+ const p = path.posix.join(root, e);
292
+ if (await this.isdir(p)) {
293
+ dirs.push(e);
294
+ }
295
+ else {
296
+ files.push(e);
297
+ }
298
+ }
299
+ out.push({ dir: root, dirs, files });
300
+ for (const d of dirs) {
301
+ out.push(...(await this.walk(path.posix.join(root, d))));
302
+ }
303
+ return out;
304
+ }
305
+ /**
306
+ * Close the underlying socket
307
+ */
308
+ close() {
309
+ log.debug('Closing AFC service connection');
310
+ try {
311
+ this.socket?.end();
312
+ }
313
+ catch (error) {
314
+ log.debug('Error while closing socket (ignored):', error);
315
+ }
316
+ this.socket = null;
317
+ }
318
+ /**
319
+ * Connect to RSD port and perform RSDCheckin.
320
+ * Keeps the underlying socket for raw AFC I/O.
321
+ */
322
+ async _connect() {
323
+ if (this.socket && !this.socket.destroyed) {
324
+ return this.socket;
325
+ }
326
+ const [host, rsdPort] = this.address;
327
+ this.socket = await new Promise((resolve, reject) => {
328
+ const s = net.createConnection({ host, port: rsdPort }, () => {
329
+ s.setTimeout(0);
330
+ s.setKeepAlive(true);
331
+ resolve(s);
332
+ });
333
+ s.once('error', reject);
334
+ s.setTimeout(30000, () => {
335
+ s.destroy();
336
+ reject(new Error('AFC connect timed out'));
337
+ });
338
+ });
339
+ await rsdHandshakeForRawService(this.socket);
340
+ log.debug('RSD handshake complete; switching to raw AFC');
341
+ return this.socket;
342
+ }
343
+ async _resolvePath(filePath) {
344
+ const info = await this.stat(filePath);
345
+ if (info.st_ifmt === AfcFileMode.S_IFLNK && info.LinkTarget) {
346
+ const target = info.LinkTarget;
347
+ if (target.startsWith('/')) {
348
+ return target;
349
+ }
350
+ return path.posix.join(path.posix.dirname(filePath), target);
351
+ }
352
+ return filePath;
353
+ }
354
+ async _dispatch(op, payload = Buffer.alloc(0), thisLenOverride) {
355
+ const sock = await this._connect();
356
+ await sendAfcPacket(sock, op, this.packetNum++, payload, thisLenOverride);
357
+ }
358
+ async _receive() {
359
+ const sock = await this._connect();
360
+ const res = await readAfcResponse(sock);
361
+ return { status: res.status, data: res.data };
362
+ }
363
+ /**
364
+ * Send a single-operation request and parse result.
365
+ * Throws if status != SUCCESS.
366
+ * Returns response DATA buffer when applicable.
367
+ */
368
+ async _doOperation(op, payload = Buffer.alloc(0), thisLenOverride) {
369
+ await this._dispatch(op, payload, thisLenOverride);
370
+ const { status, data } = await this._receive();
371
+ if (status !== AfcError.SUCCESS) {
372
+ const errorName = AfcError[status] || 'UNKNOWN';
373
+ const opName = AfcOpcode[op] || op.toString();
374
+ if (status === AfcError.OBJECT_NOT_FOUND) {
375
+ throw new Error(`AFC error: OBJECT_NOT_FOUND for operation ${opName}`);
376
+ }
377
+ if (!this.silent) {
378
+ log.error(`AFC operation ${opName} failed with status ${errorName} (${status})`);
379
+ }
380
+ throw new Error(`AFC operation ${opName} failed with ${errorName} (${status})`);
381
+ }
382
+ return data;
383
+ }
384
+ }
385
+ export default AfcService;
@@ -0,0 +1,14 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ import { AfcError, AfcOpcode } from './enums.js';
3
+ type AfcDispatcher = (op: AfcOpcode, payload: Buffer) => Promise<void>;
4
+ type AfcWriteDispatcher = (op: AfcOpcode, payload: Buffer, thisLenOverride?: number) => Promise<void>;
5
+ export declare function createAfcReadStream(handle: bigint, size: bigint, dispatch: AfcDispatcher, receive: () => Promise<{
6
+ status: AfcError;
7
+ data: Buffer;
8
+ }>): Readable;
9
+ export declare function createAfcWriteStream(handle: bigint, dispatch: AfcWriteDispatcher, receive: () => Promise<{
10
+ status: AfcError;
11
+ data: Buffer;
12
+ }>, chunkSize?: number): Writable;
13
+ export {};
14
+ //# sourceMappingURL=stream-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../../../../src/services/ios/afc/stream-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEjD,KAAK,aAAa,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE,KAAK,kBAAkB,GAAG,CACxB,EAAE,EAAE,SAAS,EACb,OAAO,EAAE,MAAM,EACf,eAAe,CAAC,EAAE,MAAM,KACrB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,MAAM,OAAO,CAAC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,QAAQ,CAmCV;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,MAAM,OAAO,CAAC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC1D,SAAS,CAAC,EAAE,MAAM,GACjB,QAAQ,CAwCV"}
@@ -0,0 +1,60 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ import { buildReadPayload, nextReadChunkSize, writeUInt64LE } from './codec.js';
3
+ import { AFC_WRITE_THIS_LENGTH } from './constants.js';
4
+ import { AfcError, AfcOpcode } from './enums.js';
5
+ export function createAfcReadStream(handle, size, dispatch, receive) {
6
+ let left = size;
7
+ let totalRead = 0n;
8
+ return new Readable({
9
+ async read() {
10
+ try {
11
+ if (left <= 0n) {
12
+ this.push(null);
13
+ return;
14
+ }
15
+ const toRead = nextReadChunkSize(left);
16
+ await dispatch(AfcOpcode.READ, buildReadPayload(handle, toRead));
17
+ const { status, data } = await receive();
18
+ if (status !== AfcError.SUCCESS) {
19
+ const errorName = AfcError[status] || 'UNKNOWN';
20
+ this.destroy(new Error(`fread error: ${errorName} (${status})`));
21
+ return;
22
+ }
23
+ totalRead += BigInt(data.length);
24
+ left -= BigInt(data.length);
25
+ this.push(data);
26
+ if (BigInt(data.length) < toRead) {
27
+ this.push(null);
28
+ }
29
+ }
30
+ catch (error) {
31
+ this.destroy(error);
32
+ }
33
+ },
34
+ });
35
+ }
36
+ export function createAfcWriteStream(handle, dispatch, receive, chunkSize) {
37
+ return new Writable({
38
+ async write(chunk, encoding, callback) {
39
+ try {
40
+ let offset = 0;
41
+ while (offset < chunk.length) {
42
+ const end = Math.min(offset + (chunkSize ?? chunk.length), chunk.length);
43
+ const subchunk = chunk.subarray(offset, end);
44
+ await dispatch(AfcOpcode.WRITE, Buffer.concat([writeUInt64LE(handle), subchunk]), AFC_WRITE_THIS_LENGTH);
45
+ const { status } = await receive();
46
+ if (status !== AfcError.SUCCESS) {
47
+ const errorName = AfcError[status] || 'UNKNOWN';
48
+ callback(new Error(`fwrite chunk failed with ${errorName} (${status}) at offset ${offset}`));
49
+ return;
50
+ }
51
+ offset = end;
52
+ }
53
+ callback();
54
+ }
55
+ catch (error) {
56
+ callback(error);
57
+ }
58
+ },
59
+ });
60
+ }
@@ -1,5 +1,6 @@
1
1
  import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
2
2
  import type { DiagnosticsServiceWithConnection, MobileConfigServiceWithConnection, MobileImageMounterServiceWithConnection, NotificationProxyServiceWithConnection, PowerAssertionServiceWithConnection, SpringboardServiceWithConnection, SyslogService as SyslogServiceType, WebInspectorServiceWithConnection } from './lib/types.js';
3
+ import AfcService from './services/ios/afc/index.js';
3
4
  export declare function startDiagnosticsService(udid: string): Promise<DiagnosticsServiceWithConnection>;
4
5
  export declare function startNotificationProxyService(udid: string): Promise<NotificationProxyServiceWithConnection>;
5
6
  export declare function startMobileConfigService(udid: string): Promise<MobileConfigServiceWithConnection>;
@@ -7,6 +8,11 @@ export declare function startMobileImageMounterService(udid: string): Promise<Mo
7
8
  export declare function startSpringboardService(udid: string): Promise<SpringboardServiceWithConnection>;
8
9
  export declare function startPowerAssertionService(udid: string): Promise<PowerAssertionServiceWithConnection>;
9
10
  export declare function startSyslogService(udid: string): Promise<SyslogServiceType>;
11
+ /**
12
+ * Start AFC service over RemoteXPC shim.
13
+ * Resolves the AFC service port via RemoteXPC and returns a ready-to-use AfcService instance.
14
+ */
15
+ export declare function startAfcService(udid: string): Promise<AfcService>;
10
16
  export declare function startWebInspectorService(udid: string): Promise<WebInspectorServiceWithConnection>;
11
17
  export declare function createRemoteXPCConnection(udid: string): Promise<{
12
18
  remoteXPC: RemoteXpcConnection;
@@ -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,gCAAgC,EAChC,iCAAiC,EACjC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,gCAAgC,EAChC,aAAa,IAAI,iBAAiB,EAClC,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AAaxB,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;AACD,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,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,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;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,gCAAgC,EAChC,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;AAarD,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,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,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
@@ -2,6 +2,7 @@ import { strongbox } from '@appium/strongbox';
2
2
  import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
3
3
  import { TunnelManager } from './lib/tunnel/index.js';
4
4
  import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
5
+ import AfcService from './services/ios/afc/index.js';
5
6
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
6
7
  import { MobileConfigService } from './services/ios/mobile-config/index.js';
7
8
  import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
@@ -82,6 +83,18 @@ export async function startSyslogService(udid) {
82
83
  const { tunnelConnection } = await createRemoteXPCConnection(udid);
83
84
  return new SyslogService([tunnelConnection.host, tunnelConnection.port]);
84
85
  }
86
+ /**
87
+ * Start AFC service over RemoteXPC shim.
88
+ * Resolves the AFC service port via RemoteXPC and returns a ready-to-use AfcService instance.
89
+ */
90
+ export async function startAfcService(udid) {
91
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
92
+ const afcDescriptor = remoteXPC.findService(AfcService.RSD_SERVICE_NAME);
93
+ return new AfcService([
94
+ tunnelConnection.host,
95
+ parseInt(afcDescriptor.port, 10),
96
+ ]);
97
+ }
85
98
  export async function startWebInspectorService(udid) {
86
99
  const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
87
100
  const webInspectorService = remoteXPC.findService(WebInspectorService.RSD_SERVICE_NAME);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-remotexpc",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "main": "build/src/index.js",
5
5
  "types": "build/src/index.d.ts",
6
6
  "type": "module",
@@ -33,6 +33,7 @@
33
33
  "test:mobile-config": "mocha test/integration/mobile-config-test.ts --exit --timeout 1m",
34
34
  "test:springboard": "mocha test/integration/springboard-service-test.ts --exit --timeout 1m",
35
35
  "test:webinspector": "mocha test/integration/webinspector-test.ts --exit --timeout 1m",
36
+ "test:afc": "mocha test/integration/afc-test.ts --exit --timeout 1m",
36
37
  "test:power-assertion": "mocha test/integration/power-assertion-test.ts --exit --timeout 1m",
37
38
  "test:unit": "mocha 'test/unit/**/*.ts' --exit --timeout 2m",
38
39
  "test:tunnel-creation": "sudo tsx scripts/test-tunnel-creation.ts",
@@ -2,6 +2,7 @@ import {
2
2
  TunnelRegistryServer,
3
3
  startTunnelRegistryServer,
4
4
  } from '../lib/tunnel/tunnel-registry-server.js';
5
+ import * as afc from './ios/afc/index.js';
5
6
  import * as diagnostics from './ios/diagnostic-service/index.js';
6
7
  import * as mobileImageMounter from './ios/mobile-image-mounter/index.js';
7
8
  import * as powerAssertion from './ios/power-assertion/index.js';
@@ -15,6 +16,7 @@ export {
15
16
  powerAssertion,
16
17
  syslog,
17
18
  tunnel,
19
+ afc,
18
20
  webinspector,
19
21
  TunnelRegistryServer,
20
22
  startTunnelRegistryServer,