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.
Files changed (30) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/src/services/ios/afc/codec.d.ts +13 -4
  3. package/build/src/services/ios/afc/codec.d.ts.map +1 -1
  4. package/build/src/services/ios/afc/codec.js +57 -32
  5. package/build/src/services/ios/afc/codec.js.map +1 -1
  6. package/build/src/services/ios/afc/constants.d.ts +2 -2
  7. package/build/src/services/ios/afc/constants.d.ts.map +1 -1
  8. package/build/src/services/ios/afc/constants.js +4 -5
  9. package/build/src/services/ios/afc/constants.js.map +1 -1
  10. package/build/src/services/ios/afc/demux.d.ts +40 -0
  11. package/build/src/services/ios/afc/demux.d.ts.map +1 -0
  12. package/build/src/services/ios/afc/demux.js +174 -0
  13. package/build/src/services/ios/afc/demux.js.map +1 -0
  14. package/build/src/services/ios/afc/index.d.ts +3 -5
  15. package/build/src/services/ios/afc/index.d.ts.map +1 -1
  16. package/build/src/services/ios/afc/index.js +34 -40
  17. package/build/src/services/ios/afc/index.js.map +1 -1
  18. package/build/src/services/ios/afc/stream-utils.d.ts +10 -11
  19. package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -1
  20. package/build/src/services/ios/afc/stream-utils.js +7 -8
  21. package/build/src/services/ios/afc/stream-utils.js.map +1 -1
  22. package/build/src/services/ios/testmanagerd/index.js +2 -2
  23. package/build/src/services/ios/testmanagerd/index.js.map +1 -1
  24. package/package.json +2 -1
  25. package/src/services/ios/afc/codec.ts +73 -37
  26. package/src/services/ios/afc/constants.ts +4 -6
  27. package/src/services/ios/afc/demux.ts +225 -0
  28. package/src/services/ios/afc/index.ts +48 -67
  29. package/src/services/ios/afc/stream-utils.ts +21 -24
  30. 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 for the provided operation and payload length.
72
+ * Build an AFC packet header with explicit entire_length and this_length values.
72
73
  */
73
- export function encodeHeader(
74
+ export function encodeHeaderExplicit(
74
75
  op: AfcOpcode,
75
76
  packetNum: bigint,
76
- payloadLen: number,
77
- thisLenOverride?: number,
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
- // entire_length
86
- writeUInt64LE(entireLen).copy(header, 8);
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
- * Send an AFC packet (header and optional payload) to the socket.
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
- payload: Buffer = Buffer.alloc(0),
211
- thisLenOverride?: number,
257
+ headerPayload: Buffer = Buffer.alloc(0),
258
+ content: Buffer = Buffer.alloc(0),
212
259
  ): Promise<void> {
213
- const header = encodeHeader(op, packetNum, payload.length, thisLenOverride);
214
- await new Promise<void>((resolve, reject) => {
215
- socket.write(header, (err) => {
216
- if (err) {
217
- return reject(err);
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 = 4 * 1024 * 1024; // 4 MiB
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 = 15_000;
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
- AFC_FOPEN_TEXTUAL_MODES,
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 packetNum: bigint = 0n;
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
- this._dispatch.bind(this),
233
- this._receive.bind(this),
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._dispatch(
254
+ const { status } = await this._sendAndWait(
266
255
  AfcOpcode.WRITE,
267
- Buffer.concat([writeUInt64LE(handle), chunk]),
268
- AFC_WRITE_THIS_LENGTH,
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._dispatch(op, payload, thisLenOverride);
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';