1/** 2 * Copyright (c) 2021 Expo, Inc. 3 * Copyright (c) 2018 Drifty Co. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8import Debug from 'debug'; 9import { Socket } from 'net'; 10 11import { ResponseError, ServiceClient } from './ServiceClient'; 12import { LockdownProtocolClient } from '../protocol/LockdownProtocol'; 13import type { LockdownCommand, LockdownResponse } from '../protocol/LockdownProtocol'; 14 15const debug = Debug('expo:apple-device:client:installation_proxy'); 16 17export type OnInstallProgressCallback = (props: { 18 status: string; 19 isComplete: boolean; 20 // copiedFiles: number; 21 progress: number; 22}) => void; 23 24interface IPOptions { 25 ApplicationsType?: 'Any'; 26 PackageType?: 'Developer'; 27 CFBundleIdentifier?: string; 28 29 ReturnAttributes?: ( 30 | 'CFBundleIdentifier' 31 | 'ApplicationDSID' 32 | 'ApplicationType' 33 | 'CFBundleExecutable' 34 | 'CFBundleDisplayName' 35 | 'CFBundleIconFile' 36 | 'CFBundleName' 37 | 'CFBundleShortVersionString' 38 | 'CFBundleSupportedPlatforms' 39 | 'CFBundleURLTypes' 40 | 'CodeInfoIdentifier' 41 | 'Container' 42 | 'Entitlements' 43 | 'HasSettingsBundle' 44 | 'IsUpgradeable' 45 | 'MinimumOSVersion' 46 | 'Path' 47 | 'SignerIdentity' 48 | 'UIDeviceFamily' 49 | 'UIFileSharingEnabled' 50 | 'UIStatusBarHidden' 51 | 'UISupportedInterfaceOrientations' 52 )[]; 53 BundleIDs?: string[]; 54 [key: string]: undefined | string | string[]; 55} 56 57interface IPInstallPercentCompleteResponseItem extends LockdownResponse { 58 PercentComplete: number; 59} 60 61interface IPInstallCFBundleIdentifierResponseItem { 62 CFBundleIdentifier: string; 63} 64 65interface IPInstallCompleteResponseItem extends LockdownResponse { 66 Status: 'Complete'; 67} 68/* 69 * [{ "PercentComplete": 5, "Status": "CreatingStagingDirectory" }] 70 * ... 71 * [{ "PercentComplete": 90, "Status": "GeneratingApplicationMap" }] 72 * [{ "CFBundleIdentifier": "my.company.app" }] 73 * [{ "Status": "Complete" }] 74 */ 75type IPInstallPercentCompleteResponse = IPInstallPercentCompleteResponseItem[]; 76type IPInstallCFBundleIdentifierResponse = IPInstallCFBundleIdentifierResponseItem[]; 77type IPInstallCompleteResponse = IPInstallCompleteResponseItem[]; 78 79interface IPMessage extends LockdownCommand { 80 Command: string; 81 ClientOptions: IPOptions; 82} 83 84interface IPLookupResponseItem extends LockdownResponse { 85 LookupResult: IPLookupResult; 86} 87/* 88 * [{ 89 * LookupResult: IPLookupResult, 90 * Status: "Complete" 91 * }] 92 */ 93type IPLookupResponse = IPLookupResponseItem[]; 94 95export interface IPLookupResult { 96 // BundleId 97 [key: string]: { 98 Container: string; 99 CFBundleIdentifier: string; 100 CFBundleExecutable: string; 101 Path: string; 102 }; 103} 104 105function isIPLookupResponse(resp: any): resp is IPLookupResponse { 106 return resp.length && resp[0].LookupResult !== undefined; 107} 108 109function isIPInstallPercentCompleteResponse(resp: any): resp is IPInstallPercentCompleteResponse { 110 return resp.length && resp[0].PercentComplete !== undefined; 111} 112 113function isIPInstallCFBundleIdentifierResponse( 114 resp: any 115): resp is IPInstallCFBundleIdentifierResponse { 116 return resp.length && resp[0].CFBundleIdentifier !== undefined; 117} 118 119function isIPInstallCompleteResponse(resp: any): resp is IPInstallCompleteResponse { 120 return resp.length && resp[0].Status === 'Complete'; 121} 122 123export class InstallationProxyClient extends ServiceClient<LockdownProtocolClient<IPMessage>> { 124 constructor(public socket: Socket) { 125 super(socket, new LockdownProtocolClient(socket)); 126 } 127 128 async lookupApp( 129 bundleIds: string[], 130 options: IPOptions = { 131 ReturnAttributes: ['Path', 'Container', 'CFBundleExecutable', 'CFBundleIdentifier'], 132 ApplicationsType: 'Any', 133 } 134 ) { 135 debug(`lookupApp, options: ${JSON.stringify(options)}`); 136 137 let resp = await this.protocolClient.sendMessage({ 138 Command: 'Lookup', 139 ClientOptions: { 140 BundleIDs: bundleIds, 141 ...options, 142 }, 143 }); 144 if (resp && !Array.isArray(resp)) resp = [resp]; 145 if (isIPLookupResponse(resp)) { 146 return resp[0].LookupResult; 147 } else { 148 throw new ResponseError(`There was an error looking up app`, resp); 149 } 150 } 151 152 async installApp( 153 packagePath: string, 154 bundleId: string, 155 options: IPOptions = { 156 ApplicationsType: 'Any', 157 PackageType: 'Developer', 158 }, 159 onProgress: OnInstallProgressCallback 160 ) { 161 debug(`installApp, packagePath: ${packagePath}, bundleId: ${bundleId}`); 162 163 return this.protocolClient.sendMessage( 164 { 165 Command: 'Install', 166 PackagePath: packagePath, 167 ClientOptions: { 168 CFBundleIdentifier: bundleId, 169 ...options, 170 }, 171 }, 172 (resp, resolve, reject) => { 173 if (resp && !Array.isArray(resp)) resp = [resp]; 174 175 if (isIPInstallCompleteResponse(resp)) { 176 onProgress({ 177 isComplete: true, 178 progress: 100, 179 status: resp[0].Status, 180 }); 181 resolve(); 182 } else if (isIPInstallPercentCompleteResponse(resp)) { 183 onProgress({ 184 isComplete: false, 185 progress: resp[0].PercentComplete, 186 status: resp[0].Status, 187 }); 188 debug(`Installation status: ${resp[0].Status}, %${resp[0].PercentComplete}`); 189 } else if (isIPInstallCFBundleIdentifierResponse(resp)) { 190 debug(`Installed app: ${resp[0].CFBundleIdentifier}`); 191 } else { 192 reject( 193 new ResponseError( 194 'There was an error installing app: ' + require('util').inspect(resp), 195 resp 196 ) 197 ); 198 } 199 } 200 ); 201 } 202} 203