1c4ef02aeSEvan Bacon/**
2c4ef02aeSEvan Bacon * Copyright (c) 2021 Expo, Inc.
3c4ef02aeSEvan Bacon * Copyright (c) 2018 Drifty Co.
4c4ef02aeSEvan Bacon *
5c4ef02aeSEvan Bacon * This source code is licensed under the MIT license found in the
6c4ef02aeSEvan Bacon * LICENSE file in the root directory of this source tree.
7c4ef02aeSEvan Bacon */
8c4ef02aeSEvan Baconimport Debug from 'debug';
9c4ef02aeSEvan Baconimport * as fs from 'fs';
10c4ef02aeSEvan Baconimport { Socket } from 'net';
11c4ef02aeSEvan Baconimport * as path from 'path';
12c4ef02aeSEvan Baconimport { promisify } from 'util';
13c4ef02aeSEvan Bacon
14*8a424bebSJames Ideimport { ServiceClient } from './ServiceClient';
15c4ef02aeSEvan Baconimport { CommandError } from '../../../../utils/errors';
16c4ef02aeSEvan Baconimport {
17c4ef02aeSEvan Bacon  AFC_FILE_OPEN_FLAGS,
18c4ef02aeSEvan Bacon  AFC_OPS,
19c4ef02aeSEvan Bacon  AFC_STATUS,
20c4ef02aeSEvan Bacon  AFCError,
21c4ef02aeSEvan Bacon  AFCProtocolClient,
22c4ef02aeSEvan Bacon  AFCResponse,
23c4ef02aeSEvan Bacon} from '../protocol/AFCProtocol';
24c4ef02aeSEvan Bacon
25c4ef02aeSEvan Baconconst debug = Debug('expo:apple-device:client:afc');
26c4ef02aeSEvan Baconconst MAX_OPEN_FILES = 240;
27c4ef02aeSEvan Bacon
28c4ef02aeSEvan Baconexport class AFCClient extends ServiceClient<AFCProtocolClient> {
29c4ef02aeSEvan Bacon  constructor(public socket: Socket) {
30c4ef02aeSEvan Bacon    super(socket, new AFCProtocolClient(socket));
31c4ef02aeSEvan Bacon  }
32c4ef02aeSEvan Bacon
33c4ef02aeSEvan Bacon  async getFileInfo(path: string): Promise<string[]> {
34c4ef02aeSEvan Bacon    debug(`getFileInfo: ${path}`);
35c4ef02aeSEvan Bacon
36c4ef02aeSEvan Bacon    const response = await this.protocolClient.sendMessage({
37c4ef02aeSEvan Bacon      operation: AFC_OPS.GET_FILE_INFO,
38c4ef02aeSEvan Bacon      data: toCString(path),
39c4ef02aeSEvan Bacon    });
40c4ef02aeSEvan Bacon    debug(`getFileInfo:response: %O`, response);
41c4ef02aeSEvan Bacon
42c4ef02aeSEvan Bacon    const strings: string[] = [];
43c4ef02aeSEvan Bacon    let currentString = '';
44c4ef02aeSEvan Bacon    const tokens = response.data;
45c4ef02aeSEvan Bacon    tokens.forEach((token) => {
46c4ef02aeSEvan Bacon      if (token === 0) {
47c4ef02aeSEvan Bacon        strings.push(currentString);
48c4ef02aeSEvan Bacon        currentString = '';
49c4ef02aeSEvan Bacon      } else {
50c4ef02aeSEvan Bacon        currentString += String.fromCharCode(token);
51c4ef02aeSEvan Bacon      }
52c4ef02aeSEvan Bacon    });
53c4ef02aeSEvan Bacon    return strings;
54c4ef02aeSEvan Bacon  }
55c4ef02aeSEvan Bacon
56c4ef02aeSEvan Bacon  async writeFile(fd: Buffer, data: Buffer): Promise<AFCResponse> {
57c4ef02aeSEvan Bacon    debug(`writeFile: ${Array.prototype.toString.call(fd)} data size: ${data.length}`);
58c4ef02aeSEvan Bacon
59c4ef02aeSEvan Bacon    const response = await this.protocolClient.sendMessage({
60c4ef02aeSEvan Bacon      operation: AFC_OPS.FILE_WRITE,
61c4ef02aeSEvan Bacon      data: fd,
62c4ef02aeSEvan Bacon      payload: data,
63c4ef02aeSEvan Bacon    });
64c4ef02aeSEvan Bacon
65c4ef02aeSEvan Bacon    debug(`writeFile:response:`, response);
66c4ef02aeSEvan Bacon    return response;
67c4ef02aeSEvan Bacon  }
68c4ef02aeSEvan Bacon
69c4ef02aeSEvan Bacon  protected async openFile(path: string): Promise<Buffer> {
70c4ef02aeSEvan Bacon    debug(`openFile: ${path}`);
71c4ef02aeSEvan Bacon    // mode + path + null terminator
72c4ef02aeSEvan Bacon    const data = Buffer.alloc(8 + path.length + 1);
73c4ef02aeSEvan Bacon    // write mode
74c4ef02aeSEvan Bacon    data.writeUInt32LE(AFC_FILE_OPEN_FLAGS.WRONLY, 0);
75c4ef02aeSEvan Bacon    // then path to file
76c4ef02aeSEvan Bacon    toCString(path).copy(data, 8);
77c4ef02aeSEvan Bacon
78c4ef02aeSEvan Bacon    const response = await this.protocolClient.sendMessage({
79c4ef02aeSEvan Bacon      operation: AFC_OPS.FILE_OPEN,
80c4ef02aeSEvan Bacon      data,
81c4ef02aeSEvan Bacon    });
82c4ef02aeSEvan Bacon
83c4ef02aeSEvan Bacon    // debug(`openFile:response:`, response);
84c4ef02aeSEvan Bacon
85c4ef02aeSEvan Bacon    if (response.operation === AFC_OPS.FILE_OPEN_RES) {
86c4ef02aeSEvan Bacon      return response.data;
87c4ef02aeSEvan Bacon    }
88c4ef02aeSEvan Bacon
89c4ef02aeSEvan Bacon    throw new CommandError(
90c4ef02aeSEvan Bacon      'APPLE_DEVICE_AFC',
91c4ef02aeSEvan Bacon      `There was an unknown error opening file ${path}, response: ${Array.prototype.toString.call(
92c4ef02aeSEvan Bacon        response.data
93c4ef02aeSEvan Bacon      )}`
94c4ef02aeSEvan Bacon    );
95c4ef02aeSEvan Bacon  }
96c4ef02aeSEvan Bacon
97c4ef02aeSEvan Bacon  protected async closeFile(fd: Buffer): Promise<AFCResponse> {
98c4ef02aeSEvan Bacon    debug(`closeFile fd: ${Array.prototype.toString.call(fd)}`);
99c4ef02aeSEvan Bacon    const response = await this.protocolClient.sendMessage({
100c4ef02aeSEvan Bacon      operation: AFC_OPS.FILE_CLOSE,
101c4ef02aeSEvan Bacon      data: fd,
102c4ef02aeSEvan Bacon    });
103c4ef02aeSEvan Bacon
104c4ef02aeSEvan Bacon    debug(`closeFile:response:`, response);
105c4ef02aeSEvan Bacon    return response;
106c4ef02aeSEvan Bacon  }
107c4ef02aeSEvan Bacon
108c4ef02aeSEvan Bacon  protected async uploadFile(srcPath: string, destPath: string): Promise<void> {
109c4ef02aeSEvan Bacon    debug(`uploadFile: ${srcPath}, ${destPath}`);
110c4ef02aeSEvan Bacon
111c4ef02aeSEvan Bacon    // read local file and get fd of destination
112c4ef02aeSEvan Bacon    const [srcFile, destFile] = await Promise.all([
113c4ef02aeSEvan Bacon      await promisify(fs.readFile)(srcPath),
114c4ef02aeSEvan Bacon      await this.openFile(destPath),
115c4ef02aeSEvan Bacon    ]);
116c4ef02aeSEvan Bacon
117c4ef02aeSEvan Bacon    try {
118c4ef02aeSEvan Bacon      await this.writeFile(destFile, srcFile);
119c4ef02aeSEvan Bacon      await this.closeFile(destFile);
120c4ef02aeSEvan Bacon    } catch (err) {
121c4ef02aeSEvan Bacon      await this.closeFile(destFile);
122c4ef02aeSEvan Bacon      throw err;
123c4ef02aeSEvan Bacon    }
124c4ef02aeSEvan Bacon  }
125c4ef02aeSEvan Bacon
126c4ef02aeSEvan Bacon  async makeDirectory(path: string): Promise<AFCResponse> {
127c4ef02aeSEvan Bacon    debug(`makeDirectory: ${path}`);
128c4ef02aeSEvan Bacon
129c4ef02aeSEvan Bacon    const response = await this.protocolClient.sendMessage({
130c4ef02aeSEvan Bacon      operation: AFC_OPS.MAKE_DIR,
131c4ef02aeSEvan Bacon      data: toCString(path),
132c4ef02aeSEvan Bacon    });
133c4ef02aeSEvan Bacon
134c4ef02aeSEvan Bacon    debug(`makeDirectory:response:`, response);
135c4ef02aeSEvan Bacon    return response;
136c4ef02aeSEvan Bacon  }
137c4ef02aeSEvan Bacon
138c4ef02aeSEvan Bacon  async uploadDirectory(srcPath: string, destPath: string): Promise<void> {
139c4ef02aeSEvan Bacon    debug(`uploadDirectory: ${srcPath}`);
140c4ef02aeSEvan Bacon    await this.makeDirectory(destPath);
141c4ef02aeSEvan Bacon
142c4ef02aeSEvan Bacon    // AFC doesn't seem to give out more than 240 file handles,
143c4ef02aeSEvan Bacon    // so we delay any requests that would push us over until more open up
144c4ef02aeSEvan Bacon    let numOpenFiles = 0;
145c4ef02aeSEvan Bacon    const pendingFileUploads: (() => void)[] = [];
146c4ef02aeSEvan Bacon    const _this = this;
147c4ef02aeSEvan Bacon    return uploadDir(srcPath);
148c4ef02aeSEvan Bacon
149c4ef02aeSEvan Bacon    async function uploadDir(dirPath: string): Promise<void> {
150c4ef02aeSEvan Bacon      const promises: Promise<void>[] = [];
151c4ef02aeSEvan Bacon      for (const file of fs.readdirSync(dirPath)) {
152c4ef02aeSEvan Bacon        const filePath = path.join(dirPath, file);
153c4ef02aeSEvan Bacon        const remotePath = path.join(destPath, path.relative(srcPath, filePath));
154c4ef02aeSEvan Bacon        if (fs.lstatSync(filePath).isDirectory()) {
155c4ef02aeSEvan Bacon          promises.push(_this.makeDirectory(remotePath).then(() => uploadDir(filePath)));
156c4ef02aeSEvan Bacon        } else {
157c4ef02aeSEvan Bacon          // Create promise to add to promises array
158c4ef02aeSEvan Bacon          // this way it can be resolved once a pending upload has finished
159c4ef02aeSEvan Bacon          let resolve: (val?: any) => void;
160c4ef02aeSEvan Bacon          let reject: (err: AFCError) => void;
161c4ef02aeSEvan Bacon          const promise = new Promise<void>((res, rej) => {
162c4ef02aeSEvan Bacon            resolve = res;
163c4ef02aeSEvan Bacon            reject = rej;
164c4ef02aeSEvan Bacon          });
165c4ef02aeSEvan Bacon          promises.push(promise);
166c4ef02aeSEvan Bacon
167c4ef02aeSEvan Bacon          // wrap upload in a function in case we need to save it for later
168c4ef02aeSEvan Bacon          const uploadFile = (tries = 0) => {
169c4ef02aeSEvan Bacon            numOpenFiles++;
170c4ef02aeSEvan Bacon            _this
171c4ef02aeSEvan Bacon              .uploadFile(filePath, remotePath)
172c4ef02aeSEvan Bacon              .then(() => {
173c4ef02aeSEvan Bacon                resolve();
174c4ef02aeSEvan Bacon                numOpenFiles--;
175c4ef02aeSEvan Bacon                const fn = pendingFileUploads.pop();
176c4ef02aeSEvan Bacon                if (fn) {
177c4ef02aeSEvan Bacon                  fn();
178c4ef02aeSEvan Bacon                }
179c4ef02aeSEvan Bacon              })
180c4ef02aeSEvan Bacon              .catch((err: AFCError) => {
181c4ef02aeSEvan Bacon                // Couldn't get fd for whatever reason, try again
182c4ef02aeSEvan Bacon                // # of retries is arbitrary and can be adjusted
183c4ef02aeSEvan Bacon                if (err.status === AFC_STATUS.NO_RESOURCES && tries < 10) {
184c4ef02aeSEvan Bacon                  debug(`Received NO_RESOURCES from AFC, retrying ${filePath} upload. ${tries}`);
185c4ef02aeSEvan Bacon                  uploadFile(tries++);
186c4ef02aeSEvan Bacon                } else {
187c4ef02aeSEvan Bacon                  numOpenFiles--;
188c4ef02aeSEvan Bacon                  reject(err);
189c4ef02aeSEvan Bacon                }
190c4ef02aeSEvan Bacon              });
191c4ef02aeSEvan Bacon          };
192c4ef02aeSEvan Bacon
193c4ef02aeSEvan Bacon          if (numOpenFiles < MAX_OPEN_FILES) {
194c4ef02aeSEvan Bacon            uploadFile();
195c4ef02aeSEvan Bacon          } else {
196c4ef02aeSEvan Bacon            debug(
197c4ef02aeSEvan Bacon              `numOpenFiles >= ${MAX_OPEN_FILES}, adding to pending queue. Length: ${pendingFileUploads.length}`
198c4ef02aeSEvan Bacon            );
199c4ef02aeSEvan Bacon            pendingFileUploads.push(uploadFile);
200c4ef02aeSEvan Bacon          }
201c4ef02aeSEvan Bacon        }
202c4ef02aeSEvan Bacon      }
203c4ef02aeSEvan Bacon      await Promise.all(promises);
204c4ef02aeSEvan Bacon    }
205c4ef02aeSEvan Bacon  }
206c4ef02aeSEvan Bacon}
207c4ef02aeSEvan Bacon
208c4ef02aeSEvan Baconfunction toCString(s: string) {
209c4ef02aeSEvan Bacon  const buf = Buffer.alloc(s.length + 1);
210c4ef02aeSEvan Bacon  const len = buf.write(s);
211c4ef02aeSEvan Bacon  buf.writeUInt8(0, len);
212c4ef02aeSEvan Bacon  return buf;
213c4ef02aeSEvan Bacon}
214