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