appium-ios-remotexpc 2.2.1 → 2.2.2
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/services/ios/afc/codec.d.ts +13 -4
- package/build/src/services/ios/afc/codec.d.ts.map +1 -1
- package/build/src/services/ios/afc/codec.js +57 -32
- package/build/src/services/ios/afc/codec.js.map +1 -1
- package/build/src/services/ios/afc/constants.d.ts +2 -2
- package/build/src/services/ios/afc/constants.d.ts.map +1 -1
- package/build/src/services/ios/afc/constants.js +4 -5
- package/build/src/services/ios/afc/constants.js.map +1 -1
- package/build/src/services/ios/afc/demux.d.ts +40 -0
- package/build/src/services/ios/afc/demux.d.ts.map +1 -0
- package/build/src/services/ios/afc/demux.js +174 -0
- package/build/src/services/ios/afc/demux.js.map +1 -0
- package/build/src/services/ios/afc/index.d.ts +3 -5
- package/build/src/services/ios/afc/index.d.ts.map +1 -1
- package/build/src/services/ios/afc/index.js +34 -40
- package/build/src/services/ios/afc/index.js.map +1 -1
- package/build/src/services/ios/afc/stream-utils.d.ts +10 -11
- package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -1
- package/build/src/services/ios/afc/stream-utils.js +7 -8
- package/build/src/services/ios/afc/stream-utils.js.map +1 -1
- package/build/src/services/ios/testmanagerd/index.js +2 -2
- package/build/src/services/ios/testmanagerd/index.js.map +1 -1
- package/package.json +2 -1
- package/src/services/ios/afc/codec.ts +73 -37
- package/src/services/ios/afc/constants.ts +4 -6
- package/src/services/ios/afc/demux.ts +225 -0
- package/src/services/ios/afc/index.ts +48 -67
- package/src/services/ios/afc/stream-utils.ts +21 -24
- package/src/services/ios/testmanagerd/index.ts +2 -2
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
AFCMAGIC,
|
|
7
7
|
AFC_HEADER_SIZE,
|
|
8
8
|
AFC_OPERATION_TIMEOUT_MS,
|
|
9
|
+
MAXIMUM_READ_SIZE,
|
|
9
10
|
NULL_BYTE,
|
|
10
11
|
} from './constants.js';
|
|
11
12
|
import { AfcError, AfcFopenMode, AfcOpcode } from './enums.js';
|
|
@@ -68,32 +69,35 @@ export function cstr(str: string): Buffer {
|
|
|
68
69
|
return Buffer.concat([s, NULL_BYTE]);
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* Build an AFC packet header
|
|
72
|
+
* Build an AFC packet header with explicit entire_length and this_length values.
|
|
72
73
|
*/
|
|
73
|
-
export function
|
|
74
|
+
export function encodeHeaderExplicit(
|
|
74
75
|
op: AfcOpcode,
|
|
75
76
|
packetNum: bigint,
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
entireLen: number,
|
|
78
|
+
thisLen: number,
|
|
78
79
|
): Buffer {
|
|
79
|
-
const entireLen = BigInt(AFC_HEADER_SIZE + payloadLen);
|
|
80
|
-
const thisLen = BigInt(thisLenOverride ?? AFC_HEADER_SIZE + payloadLen);
|
|
81
|
-
|
|
82
80
|
const header = Buffer.alloc(AFC_HEADER_SIZE);
|
|
83
|
-
// magic
|
|
84
81
|
AFCMAGIC.copy(header, 0);
|
|
85
|
-
|
|
86
|
-
writeUInt64LE(
|
|
87
|
-
// this_length
|
|
88
|
-
writeUInt64LE(thisLen).copy(header, 16);
|
|
89
|
-
// packet_num
|
|
82
|
+
writeUInt64LE(BigInt(entireLen)).copy(header, 8);
|
|
83
|
+
writeUInt64LE(BigInt(thisLen)).copy(header, 16);
|
|
90
84
|
writeUInt64LE(packetNum).copy(header, 24);
|
|
91
|
-
// operation
|
|
92
85
|
writeUInt64LE(BigInt(op)).copy(header, 32);
|
|
93
|
-
|
|
94
86
|
return header;
|
|
95
87
|
}
|
|
96
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Build an AFC packet header for a single contiguous payload after the header.
|
|
91
|
+
*/
|
|
92
|
+
export function encodeHeader(
|
|
93
|
+
op: AfcOpcode,
|
|
94
|
+
packetNum: bigint,
|
|
95
|
+
payloadLen: number,
|
|
96
|
+
): Buffer {
|
|
97
|
+
const entireLen = AFC_HEADER_SIZE + payloadLen;
|
|
98
|
+
return encodeHeaderExplicit(op, packetNum, entireLen, entireLen);
|
|
99
|
+
}
|
|
100
|
+
|
|
97
101
|
const SOCKET_STATES = new WeakMap<net.Socket, SocketState>();
|
|
98
102
|
const FATAL_SOCKETS = new WeakSet<net.Socket>();
|
|
99
103
|
|
|
@@ -201,33 +205,64 @@ export async function readAfcResponse(
|
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
/**
|
|
204
|
-
*
|
|
208
|
+
* Write a buffer to the socket, waiting for drain only when the kernel buffer is full.
|
|
209
|
+
*/
|
|
210
|
+
export async function writeBufferToSocket(
|
|
211
|
+
socket: net.Socket,
|
|
212
|
+
data: Buffer,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
assertSocketReadable(socket);
|
|
215
|
+
if (!data.length) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (socket.write(data)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await new Promise<void>((resolve, reject) => {
|
|
222
|
+
const onDrain = () => {
|
|
223
|
+
cleanup();
|
|
224
|
+
resolve();
|
|
225
|
+
};
|
|
226
|
+
const onError = (err: Error) => {
|
|
227
|
+
cleanup();
|
|
228
|
+
reject(err);
|
|
229
|
+
};
|
|
230
|
+
const onClosed = () => {
|
|
231
|
+
cleanup();
|
|
232
|
+
reject(
|
|
233
|
+
new AfcConnectionError('AFC socket closed while waiting for drain'),
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
const cleanup = () => {
|
|
237
|
+
socket.off('drain', onDrain);
|
|
238
|
+
socket.off('error', onError);
|
|
239
|
+
socket.off('close', onClosed);
|
|
240
|
+
socket.off('end', onClosed);
|
|
241
|
+
};
|
|
242
|
+
socket.once('drain', onDrain);
|
|
243
|
+
socket.once('error', onError);
|
|
244
|
+
socket.once('close', onClosed);
|
|
245
|
+
socket.once('end', onClosed);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Send an AFC packet as three socket writes: header, optional header payload, optional body.
|
|
251
|
+
* For FILE_WRITE, headerPayload is the 8-byte handle and content is file bytes (no memcpy).
|
|
205
252
|
*/
|
|
206
253
|
export async function sendAfcPacket(
|
|
207
254
|
socket: net.Socket,
|
|
208
255
|
op: AfcOpcode,
|
|
209
256
|
packetNum: bigint,
|
|
210
|
-
|
|
211
|
-
|
|
257
|
+
headerPayload: Buffer = Buffer.alloc(0),
|
|
258
|
+
content: Buffer = Buffer.alloc(0),
|
|
212
259
|
): Promise<void> {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (payload.length) {
|
|
220
|
-
socket.write(payload, (err2) => {
|
|
221
|
-
if (err2) {
|
|
222
|
-
return reject(err2);
|
|
223
|
-
}
|
|
224
|
-
resolve();
|
|
225
|
-
});
|
|
226
|
-
} else {
|
|
227
|
-
resolve();
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
});
|
|
260
|
+
const thisLen = AFC_HEADER_SIZE + headerPayload.length;
|
|
261
|
+
const entireLen = thisLen + content.length;
|
|
262
|
+
const header = encodeHeaderExplicit(op, packetNum, entireLen, thisLen);
|
|
263
|
+
await writeBufferToSocket(socket, header);
|
|
264
|
+
await writeBufferToSocket(socket, headerPayload);
|
|
265
|
+
await writeBufferToSocket(socket, content);
|
|
231
266
|
}
|
|
232
267
|
|
|
233
268
|
/**
|
|
@@ -391,6 +426,7 @@ export async function createRawServiceSocket(
|
|
|
391
426
|
const socket = await new Promise<net.Socket>((resolve, reject) => {
|
|
392
427
|
const conn = net.createConnection({ host, port }, () => {
|
|
393
428
|
conn.setKeepAlive(true);
|
|
429
|
+
conn.setNoDelay(true);
|
|
394
430
|
resolve(conn);
|
|
395
431
|
});
|
|
396
432
|
conn.setTimeout(timeoutMs, () => {
|
|
@@ -415,7 +451,7 @@ export async function createRawServiceSocket(
|
|
|
415
451
|
*/
|
|
416
452
|
export function nextReadChunkSize(left: bigint | number): number {
|
|
417
453
|
const leftNum = typeof left === 'bigint' ? Number(left) : left;
|
|
418
|
-
return leftNum;
|
|
454
|
+
return Math.min(leftNum, MAXIMUM_READ_SIZE);
|
|
419
455
|
}
|
|
420
456
|
|
|
421
457
|
/**
|
|
@@ -7,8 +7,9 @@ import { AfcFopenMode } from './enums.js';
|
|
|
7
7
|
// Magic bytes at start of every AFC header
|
|
8
8
|
export const AFCMAGIC = Buffer.from('CFA6LPAA', 'ascii');
|
|
9
9
|
|
|
10
|
-
// IO chunk sizes
|
|
11
|
-
export const MAXIMUM_READ_SIZE =
|
|
10
|
+
// IO chunk sizes (split writes avoid memcpy)
|
|
11
|
+
export const MAXIMUM_READ_SIZE = 1024 * 1024;
|
|
12
|
+
export const MAXIMUM_WRITE_SIZE = 1024 * 1024;
|
|
12
13
|
|
|
13
14
|
// Mapping of textual fopen modes to AFC modes
|
|
14
15
|
export const AFC_FOPEN_TEXTUAL_MODES: Record<string, AfcFopenMode> = {
|
|
@@ -23,10 +24,7 @@ export const AFC_FOPEN_TEXTUAL_MODES: Record<string, AfcFopenMode> = {
|
|
|
23
24
|
// Header size: magic (8) + entire_length (8) + this_length (8) + packet_num (8) + operation (8)
|
|
24
25
|
export const AFC_HEADER_SIZE = 40;
|
|
25
26
|
|
|
26
|
-
export const AFC_OPERATION_TIMEOUT_MS =
|
|
27
|
-
|
|
28
|
-
// Override for WRITE packets' this_length
|
|
29
|
-
export const AFC_WRITE_THIS_LENGTH = 48;
|
|
27
|
+
export const AFC_OPERATION_TIMEOUT_MS = 30_000;
|
|
30
28
|
|
|
31
29
|
export const NULL_BYTE = Buffer.from([0]);
|
|
32
30
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import AsyncLock from 'async-lock';
|
|
2
|
+
import type net from 'node:net';
|
|
3
|
+
|
|
4
|
+
import { getLogger } from '../../../lib/logger.js';
|
|
5
|
+
import {
|
|
6
|
+
fatalizeAfcSocket,
|
|
7
|
+
readAfcHeader,
|
|
8
|
+
readExact,
|
|
9
|
+
readUInt64LE,
|
|
10
|
+
sendAfcPacket,
|
|
11
|
+
} from './codec.js';
|
|
12
|
+
import { AFC_HEADER_SIZE, AFC_OPERATION_TIMEOUT_MS } from './constants.js';
|
|
13
|
+
import { AfcError, AfcOpcode } from './enums.js';
|
|
14
|
+
import { AfcConnectionError } from './errors.js';
|
|
15
|
+
|
|
16
|
+
const log = getLogger('AfcService');
|
|
17
|
+
|
|
18
|
+
type PendingResponse = {
|
|
19
|
+
resolve: (value: { status: AfcError; data: Buffer }) => void;
|
|
20
|
+
reject: (err: Error) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Routes inbound AFC packets to the matching request by packet_num.
|
|
25
|
+
* A background reader task demultiplexes responses so callers can overlap
|
|
26
|
+
* sends while each operation is matched to its reply.
|
|
27
|
+
*/
|
|
28
|
+
export class AfcPacketDemux {
|
|
29
|
+
private readonly pending = new Map<bigint, PendingResponse>();
|
|
30
|
+
private packetNum = 0n;
|
|
31
|
+
private readerSocket: net.Socket | null = null;
|
|
32
|
+
private readerActive = false;
|
|
33
|
+
private readonly sendLock = new AsyncLock();
|
|
34
|
+
private stopped = false;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly getSocket: () => Promise<net.Socket>,
|
|
38
|
+
private readonly onFatalError: (err: Error) => void,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
ensureReaderStarted(socket: net.Socket): void {
|
|
42
|
+
if (this.stopped) {
|
|
43
|
+
throw new AfcConnectionError('AFC demux is stopped');
|
|
44
|
+
}
|
|
45
|
+
if (this.readerActive && this.readerSocket === socket) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.readerSocket = socket;
|
|
49
|
+
this.readerActive = true;
|
|
50
|
+
void this._runReaderLoop(socket);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register a waiter, send one packet, then await its response.
|
|
55
|
+
* packet_num assignment, waiter registration, and send are serialized;
|
|
56
|
+
* awaiting the response happens outside the send lock.
|
|
57
|
+
*/
|
|
58
|
+
async sendAndWait(
|
|
59
|
+
op: AfcOpcode,
|
|
60
|
+
headerPayload: Buffer = Buffer.alloc(0),
|
|
61
|
+
content: Buffer = Buffer.alloc(0),
|
|
62
|
+
timeoutMs = AFC_OPERATION_TIMEOUT_MS,
|
|
63
|
+
): Promise<{ status: AfcError; data: Buffer }> {
|
|
64
|
+
if (this.stopped) {
|
|
65
|
+
throw new AfcConnectionError('AFC demux is stopped');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const socket = await this.getSocket();
|
|
69
|
+
this.ensureReaderStarted(socket);
|
|
70
|
+
|
|
71
|
+
const responsePromise = await this._sendLocked(async () => {
|
|
72
|
+
const num = this.packetNum++;
|
|
73
|
+
const waiter = this._registerPending(num, timeoutMs, op);
|
|
74
|
+
try {
|
|
75
|
+
await sendAfcPacket(socket, op, num, headerPayload, content);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
this._clearPending(num);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
return waiter;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return await responsePromise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resetForNewSocket(): void {
|
|
87
|
+
this._failPending(new AfcConnectionError('AFC socket replaced'), false);
|
|
88
|
+
this.readerSocket = null;
|
|
89
|
+
this.readerActive = false;
|
|
90
|
+
this.stopped = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Graceful shutdown; does not notify the owning service. */
|
|
94
|
+
stop(): void {
|
|
95
|
+
this.stopped = true;
|
|
96
|
+
this._failPending(new AfcConnectionError('AFC demux stopped'), false);
|
|
97
|
+
this.readerSocket = null;
|
|
98
|
+
this.readerActive = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async _runReaderLoop(socket: net.Socket): Promise<void> {
|
|
102
|
+
try {
|
|
103
|
+
await this._readerLoop(socket);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (!this._isCurrentReader(socket)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this._failPending(
|
|
109
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
110
|
+
true,
|
|
111
|
+
);
|
|
112
|
+
} finally {
|
|
113
|
+
if (this.readerSocket === socket) {
|
|
114
|
+
this.readerActive = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private _sendLocked<T>(fn: () => Promise<T>): Promise<T> {
|
|
120
|
+
return this.sendLock.acquire('afc-send', fn);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private _registerPending(
|
|
124
|
+
pktNum: bigint,
|
|
125
|
+
timeoutMs: number,
|
|
126
|
+
op: AfcOpcode,
|
|
127
|
+
): Promise<{ status: AfcError; data: Buffer }> {
|
|
128
|
+
return new Promise<{ status: AfcError; data: Buffer }>(
|
|
129
|
+
(resolve, reject) => {
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
if (!this.pending.has(pktNum)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this._clearPending(pktNum);
|
|
135
|
+
reject(
|
|
136
|
+
new AfcConnectionError(
|
|
137
|
+
`AFC operation ${AfcOpcode[op] ?? op} timed out after ${timeoutMs}ms (packet ${pktNum})`,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
|
|
142
|
+
this.pending.set(pktNum, {
|
|
143
|
+
resolve: (value) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
resolve(value);
|
|
146
|
+
},
|
|
147
|
+
reject: (err) => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
reject(err);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private _clearPending(pktNum: bigint): void {
|
|
157
|
+
this.pending.delete(pktNum);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async _readerLoop(socket: net.Socket): Promise<void> {
|
|
161
|
+
try {
|
|
162
|
+
while (!this.stopped && !socket.destroyed) {
|
|
163
|
+
const header = await readAfcHeader(socket, AFC_OPERATION_TIMEOUT_MS);
|
|
164
|
+
const payloadLen = Number(
|
|
165
|
+
header.entireLength - BigInt(AFC_HEADER_SIZE),
|
|
166
|
+
);
|
|
167
|
+
const payload =
|
|
168
|
+
payloadLen > 0
|
|
169
|
+
? await readExact(socket, payloadLen, AFC_OPERATION_TIMEOUT_MS)
|
|
170
|
+
: Buffer.alloc(0);
|
|
171
|
+
|
|
172
|
+
let status = AfcError.SUCCESS;
|
|
173
|
+
let data = payload;
|
|
174
|
+
const op = Number(header.operation) as AfcOpcode;
|
|
175
|
+
|
|
176
|
+
if (op === AfcOpcode.STATUS) {
|
|
177
|
+
if (payloadLen !== 8) {
|
|
178
|
+
log.error(`AFC STATUS response length != 8 (${payloadLen})`);
|
|
179
|
+
}
|
|
180
|
+
status = Number(readUInt64LE(payload.subarray(0, 8))) as AfcError;
|
|
181
|
+
data = Buffer.alloc(0);
|
|
182
|
+
} else if (op !== AfcOpcode.DATA) {
|
|
183
|
+
log.debug(
|
|
184
|
+
`Unexpected AFC response opcode ${op} for packet ${header.packetNum}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const waiter = this.pending.get(header.packetNum);
|
|
189
|
+
if (waiter) {
|
|
190
|
+
this.pending.delete(header.packetNum);
|
|
191
|
+
waiter.resolve({ status, data });
|
|
192
|
+
} else {
|
|
193
|
+
log.warn(
|
|
194
|
+
`AFC response with no waiter (packet ${header.packetNum}, op ${op})`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (!this._isCurrentReader(socket)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const error =
|
|
203
|
+
err instanceof Error ? err : new AfcConnectionError(String(err));
|
|
204
|
+
if (!socket.destroyed) {
|
|
205
|
+
fatalizeAfcSocket(socket, error);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** True while this socket owns the background reader task. */
|
|
212
|
+
private _isCurrentReader(socket: net.Socket): boolean {
|
|
213
|
+
return !this.stopped && this.readerSocket === socket;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private _failPending(err: Error, notifyService: boolean): void {
|
|
217
|
+
for (const [, waiter] of this.pending) {
|
|
218
|
+
waiter.reject(err);
|
|
219
|
+
}
|
|
220
|
+
this.pending.clear();
|
|
221
|
+
if (notifyService) {
|
|
222
|
+
this.onFatalError(err);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -20,16 +20,10 @@ import {
|
|
|
20
20
|
nanosecondsToMilliseconds,
|
|
21
21
|
parseCStringArray,
|
|
22
22
|
parseKeyValueNullList,
|
|
23
|
-
readAfcResponse,
|
|
24
|
-
sendAfcPacket,
|
|
25
23
|
writeUInt64LE,
|
|
26
24
|
} from './codec.js';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
AFC_LOCK_EX,
|
|
30
|
-
AFC_LOCK_UN,
|
|
31
|
-
AFC_WRITE_THIS_LENGTH,
|
|
32
|
-
} from './constants.js';
|
|
25
|
+
import { AFC_FOPEN_TEXTUAL_MODES, MAXIMUM_WRITE_SIZE } from './constants.js';
|
|
26
|
+
import { AfcPacketDemux } from './demux.js';
|
|
33
27
|
import { AfcError, AfcFileMode, AfcOpcode } from './enums.js';
|
|
34
28
|
import { AfcConnectionError } from './errors.js';
|
|
35
29
|
import { PullLocalNameAllocator } from './pull-local-name-allocator.js';
|
|
@@ -95,7 +89,7 @@ export class AfcService {
|
|
|
95
89
|
static readonly RSD_SERVICE_NAME = 'com.apple.afc.shim.remote';
|
|
96
90
|
|
|
97
91
|
private socket: net.Socket | null = null;
|
|
98
|
-
private
|
|
92
|
+
private demux: AfcPacketDemux | null = null;
|
|
99
93
|
private silent: boolean = false;
|
|
100
94
|
/** Set when the AFC byte stream is no longer safe to reuse on this instance. */
|
|
101
95
|
private connectionError: Error | null = null;
|
|
@@ -218,19 +212,14 @@ export class AfcService {
|
|
|
218
212
|
}
|
|
219
213
|
|
|
220
214
|
createReadStream(handle: bigint, size: bigint): Readable {
|
|
221
|
-
return createAfcReadStream(
|
|
222
|
-
handle,
|
|
223
|
-
size,
|
|
224
|
-
this._dispatch.bind(this),
|
|
225
|
-
this._receive.bind(this),
|
|
226
|
-
);
|
|
215
|
+
return createAfcReadStream(handle, size, this._sendAndWait.bind(this));
|
|
227
216
|
}
|
|
228
217
|
|
|
229
218
|
createWriteStream(handle: bigint, chunkSize?: number): Writable {
|
|
230
219
|
return createAfcWriteStream(
|
|
231
220
|
handle,
|
|
232
|
-
|
|
233
|
-
|
|
221
|
+
(handlePayload, content) =>
|
|
222
|
+
this._sendAndWait(AfcOpcode.WRITE, handlePayload, content),
|
|
234
223
|
chunkSize,
|
|
235
224
|
);
|
|
236
225
|
}
|
|
@@ -262,12 +251,11 @@ export class AfcService {
|
|
|
262
251
|
const chunk = data.subarray(offset, end);
|
|
263
252
|
chunkCount++;
|
|
264
253
|
|
|
265
|
-
await this.
|
|
254
|
+
const { status } = await this._sendAndWait(
|
|
266
255
|
AfcOpcode.WRITE,
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
writeUInt64LE(handle),
|
|
257
|
+
chunk,
|
|
269
258
|
);
|
|
270
|
-
const { status } = await this._receive();
|
|
271
259
|
if (status !== AfcError.SUCCESS) {
|
|
272
260
|
const errorName = AfcError[status] || 'UNKNOWN';
|
|
273
261
|
if (!this.silent) {
|
|
@@ -312,15 +300,13 @@ export class AfcService {
|
|
|
312
300
|
async setFileContents(filePath: string, data: Buffer): Promise<void> {
|
|
313
301
|
log.debug(`Writing ${data.length} bytes to file: ${filePath}`);
|
|
314
302
|
const h = await this.fopen(filePath, 'w');
|
|
315
|
-
await this._lockFile(h);
|
|
316
303
|
try {
|
|
317
|
-
await this.fwrite(h, data);
|
|
304
|
+
await this.fwrite(h, data, MAXIMUM_WRITE_SIZE);
|
|
318
305
|
log.debug(`Successfully wrote file: ${filePath}`);
|
|
319
306
|
} catch (error) {
|
|
320
307
|
await this.rmSingle(filePath, true);
|
|
321
308
|
throw error;
|
|
322
309
|
} finally {
|
|
323
|
-
await this._unlockFile(h);
|
|
324
310
|
await this.fclose(h);
|
|
325
311
|
}
|
|
326
312
|
}
|
|
@@ -345,7 +331,6 @@ export class AfcService {
|
|
|
345
331
|
async writeFromStream(filePath: string, stream: Readable): Promise<void> {
|
|
346
332
|
log.debug(`Writing stream to file: ${filePath}`);
|
|
347
333
|
const handle = await this.fopen(filePath, 'w');
|
|
348
|
-
await this._lockFile(handle);
|
|
349
334
|
const writeStream = this.createWriteStream(handle);
|
|
350
335
|
try {
|
|
351
336
|
await pipeline(stream, writeStream);
|
|
@@ -354,7 +339,6 @@ export class AfcService {
|
|
|
354
339
|
await this.rmSingle(filePath, true);
|
|
355
340
|
throw error;
|
|
356
341
|
} finally {
|
|
357
|
-
await this._unlockFile(handle);
|
|
358
342
|
await this.fclose(handle);
|
|
359
343
|
}
|
|
360
344
|
}
|
|
@@ -548,7 +532,9 @@ export class AfcService {
|
|
|
548
532
|
|
|
549
533
|
async push(localSrc: string, remoteDst: string): Promise<void> {
|
|
550
534
|
log.debug(`Pushing file from '${localSrc}' to '${remoteDst}'`);
|
|
551
|
-
const readStream = fs.createReadStream(localSrc
|
|
535
|
+
const readStream = fs.createReadStream(localSrc, {
|
|
536
|
+
highWaterMark: MAXIMUM_WRITE_SIZE,
|
|
537
|
+
});
|
|
552
538
|
await this.writeFromStream(remoteDst, readStream);
|
|
553
539
|
log.debug(`Successfully pushed file to '${remoteDst}'`);
|
|
554
540
|
}
|
|
@@ -580,6 +566,8 @@ export class AfcService {
|
|
|
580
566
|
*/
|
|
581
567
|
close(): void {
|
|
582
568
|
log.debug('Closing AFC service connection');
|
|
569
|
+
this.demux?.stop();
|
|
570
|
+
this.demux = null;
|
|
583
571
|
const socket = this.socket;
|
|
584
572
|
this.socket = null;
|
|
585
573
|
this.connectionError = null;
|
|
@@ -791,16 +779,48 @@ export class AfcService {
|
|
|
791
779
|
}
|
|
792
780
|
}
|
|
793
781
|
|
|
782
|
+
private _getDemux(): AfcPacketDemux {
|
|
783
|
+
if (!this.demux) {
|
|
784
|
+
this.demux = new AfcPacketDemux(
|
|
785
|
+
async () => await this._connect(),
|
|
786
|
+
(err) => this._markConnectionDead(err),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return this.demux;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private async _sendAndWait(
|
|
793
|
+
op: AfcOpcode,
|
|
794
|
+
headerPayload: Buffer = Buffer.alloc(0),
|
|
795
|
+
content: Buffer = Buffer.alloc(0),
|
|
796
|
+
): Promise<{ status: AfcError; data: Buffer }> {
|
|
797
|
+
return await this._withAfcConnection(async () =>
|
|
798
|
+
this._getDemux().sendAndWait(op, headerPayload, content),
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
794
802
|
private async _connect(): Promise<net.Socket> {
|
|
795
803
|
this._assertConnectionAlive();
|
|
796
804
|
if (this.socket && !this.socket.destroyed) {
|
|
797
805
|
return this.socket;
|
|
798
806
|
}
|
|
807
|
+
|
|
808
|
+
const previousSocket = this.socket;
|
|
809
|
+
if (previousSocket) {
|
|
810
|
+
if (!previousSocket.destroyed) {
|
|
811
|
+
cleanupServiceSocket(previousSocket);
|
|
812
|
+
previousSocket.destroy();
|
|
813
|
+
}
|
|
814
|
+
this.demux?.resetForNewSocket();
|
|
815
|
+
}
|
|
816
|
+
this.socket = null;
|
|
817
|
+
|
|
799
818
|
const [host, rsdPort] = this.address;
|
|
800
819
|
|
|
801
820
|
this.socket = await createRawServiceSocket(host, rsdPort, {
|
|
802
821
|
timeoutMs: 30000,
|
|
803
822
|
});
|
|
823
|
+
this._getDemux().ensureReaderStarted(this.socket);
|
|
804
824
|
log.debug('RSD handshake complete; switching to raw AFC');
|
|
805
825
|
|
|
806
826
|
return this.socket;
|
|
@@ -818,43 +838,6 @@ export class AfcService {
|
|
|
818
838
|
return filePath;
|
|
819
839
|
}
|
|
820
840
|
|
|
821
|
-
private async _lockFile(handle: bigint): Promise<void> {
|
|
822
|
-
await this._doOperation(
|
|
823
|
-
AfcOpcode.FILE_LOCK,
|
|
824
|
-
Buffer.concat([writeUInt64LE(handle), writeUInt64LE(AFC_LOCK_EX)]),
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
private async _unlockFile(handle: bigint): Promise<void> {
|
|
829
|
-
try {
|
|
830
|
-
await this._doOperation(
|
|
831
|
-
AfcOpcode.FILE_LOCK,
|
|
832
|
-
Buffer.concat([writeUInt64LE(handle), writeUInt64LE(AFC_LOCK_UN)]),
|
|
833
|
-
);
|
|
834
|
-
} catch (error) {
|
|
835
|
-
log.warn(`Failed to unlock file handle ${handle}:`, error);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
private async _dispatch(
|
|
840
|
-
op: AfcOpcode,
|
|
841
|
-
payload: Buffer = Buffer.alloc(0),
|
|
842
|
-
thisLenOverride?: number,
|
|
843
|
-
): Promise<void> {
|
|
844
|
-
return await this._withAfcConnection(async () => {
|
|
845
|
-
const sock = await this._connect();
|
|
846
|
-
await sendAfcPacket(sock, op, this.packetNum++, payload, thisLenOverride);
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
private async _receive(): Promise<{ status: AfcError; data: Buffer }> {
|
|
851
|
-
return await this._withAfcConnection(async () => {
|
|
852
|
-
const sock = await this._connect();
|
|
853
|
-
const res = await readAfcResponse(sock);
|
|
854
|
-
return { status: res.status, data: res.data };
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
841
|
/**
|
|
859
842
|
* Send a single-operation request and parse result.
|
|
860
843
|
* Throws if status != SUCCESS.
|
|
@@ -863,10 +846,8 @@ export class AfcService {
|
|
|
863
846
|
private async _doOperation(
|
|
864
847
|
op: AfcOpcode,
|
|
865
848
|
payload: Buffer = Buffer.alloc(0),
|
|
866
|
-
thisLenOverride?: number,
|
|
867
849
|
): Promise<Buffer> {
|
|
868
|
-
await this.
|
|
869
|
-
const { status, data } = await this._receive();
|
|
850
|
+
const { status, data } = await this._sendAndWait(op, payload);
|
|
870
851
|
|
|
871
852
|
if (status !== AfcError.SUCCESS) {
|
|
872
853
|
const errorName = AfcError[status] || 'UNKNOWN';
|