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