// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. /* eslint-disable */ /* eslint-enable prettier/prettier*/ import * as fs from "fs"; import * as path from "path"; import { logger } from "@vscode/debugadapter/lib/logger"; import { ChildProcess } from "../../common/node/childProcess"; import { PromiseUtil } from "../../common/node/promise"; import { OutputChannelLogger } from "../log/OutputChannelLogger"; import { IDebuggableMobileTarget } from "../mobileTarget"; /** * @preserve * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/c2848df7f210c363113797c0f2e3db8c5d4fd49f/desktop/app/src/server/devices/ios/iOSContainerUtility.tsx * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ export const idbPath = "/usr/local/bin/idb"; // Use debug to get helpful logs when idb fails const idbLogLevel = "DEBUG"; type IdbTarget = { name: string; udid: string; state: string; type: string; target_type?: string; os_version: string; architecture: string; }; export type DeviceTarget = IDebuggableMobileTarget; export const isIdbAvailable = PromiseUtil.promiseCacheDecorator(isAvailable); function isAvailable(): Promise { if (!idbPath) { return Promise.resolve(false); } return fs.promises .access(idbPath, fs.constants.X_OK) .then(() => true) .catch(() => false); } export async function isXcodeDetected(): Promise { return new ChildProcess() .execToString("xcode-select -p") .then(stdout => { return fs.existsSync(stdout.trim()); }) .catch(_ => false); } async function queryTargetsWithoutXcodeDependency( idbCompanionPath: string, isAvailableFunc: (idbPath: string) => Promise, ): Promise> { if (await isAvailableFunc(idbCompanionPath)) { return new ChildProcess() .execToString(`${idbCompanionPath} --list 1 --only device`) .then(stdout => parseIdbTargets(stdout)) .catch((e: Error) => { logger.warn( `Failed to query idb_companion --list 1 --only device for physical targets: ${e}`, ); return []; }); } else { logger.warn( `Unable to locate idb_companion in ${idbCompanionPath}. Try running sudo yum install -y fb-idb`, ); return []; } } function parseIdbTargets(lines: string): Array { return lines .trim() .split("\n") .map(line => line.trim()) .filter(Boolean) .map(line => JSON.parse(line)) .filter(({ state }: IdbTarget) => state.toLocaleLowerCase() === "booted") .map(({ type, target_type, ...rest }: IdbTarget) => ({ type: (type || target_type) === "simulator" ? "emulator" : "physical", ...rest, })) .map((target: IdbTarget) => ({ id: target.udid, isVirtualTarget: target.type === "emulator", name: target.name, isOnline: true, })); } export async function idbListTargets(idbPath: string): Promise> { return new ChildProcess() .execToString(`${idbPath} list-targets --json`) .then(stdout => // See above. parseIdbTargets(stdout), ) .catch((e: Error) => { logger.warn(`Failed to query idb for targets: ${e}`); return []; }); } async function targets(): Promise> { if (process.platform !== "darwin") { return []; } const isXcodeInstalled = await isXcodeDetected(); if (!isXcodeInstalled) { const idbCompanionPath = path.dirname(idbPath) + "/idb_companion"; return queryTargetsWithoutXcodeDependency(idbCompanionPath, isAvailable); } // Not all users have idb installed because you can still use // Flipper with Simulators without it. // But idb is MUCH more CPU efficient than xcrun, so // when installed, use it. This still holds true // with the move from instruments to xcrun. // TODO: Move idb availability check up. return (await isIdbAvailable()) ? await idbListTargets(idbPath) : new ChildProcess() .execToString("xcrun xctrace list devices") .then(stdout => { const targets: DeviceTarget[] = []; const lines = stdout .split("\n") .map(line => line.trim()) .filter(line => !!line); const firstDevicesIndex = lines.indexOf("== Devices ==") + 1; const lastDevicesIndex = lines.indexOf("== Simulators ==") - 1; for (let i = firstDevicesIndex; i <= lastDevicesIndex; i++) { const line = lines[i]; const params = line .split(" ") .map(el => el.trim()) .filter(el => !!el); // Add only devices with system version if ( params[params.length - 1].match(/\(.+\)/) && params[params.length - 2].match(/\(.+\)/) ) { targets.push({ id: params[params.length - 1].replace(/\(|\)/g, "").trim(), name: params.slice(0, params.length - 2).join(" "), isVirtualTarget: false, isOnline: true, }); } } return targets; }) .catch(e => { logger.warn(`Failed to query for devices using xctrace: ${e}`); return []; }); } async function push( udid: string, src: string, bundleId: string, dst: string, logger?: OutputChannelLogger, ): Promise { const cp = new ChildProcess(); await checkIdbIsInstalled(); return wrapWithErrorMessage( cp .execFileToString(idbPath, [ "--log", idbLogLevel, "file", "push", "--udid", udid, "--bundle-id", bundleId, src, dst, ]) .then(() => { return; }) .catch(e => handleMissingIdb(e)), logger, ); } async function pull( udid: string, src: string, bundleId: string, dst: string, logger?: OutputChannelLogger, ): Promise { const cp = new ChildProcess(); await checkIdbIsInstalled(); return wrapWithErrorMessage( cp .execFileToString(idbPath, [ "--log", idbLogLevel, "file", "pull", "--udid", udid, "--bundle-id", bundleId, src, dst, ]) .then(() => { return; }) .catch(e => handleMissingIdb(e)), logger, ); } export async function checkIdbIsInstalled(): Promise { const isInstalled = await isIdbAvailable(); if (!isInstalled) { throw new Error( `idb is required to use iOS devices. Please install it with instructions from https://github.com/facebook/idb.`, ); } } // The fb-internal idb binary is a shim that downloads the proper one on first run. It requires sudo to do so. // If we detect this, Tell the user how to fix it. function handleMissingIdb(e: Error): void { if (e.message && e.message.includes("sudo: no tty present and no askpass program specified")) { throw new Error( `idb doesn't appear to be installed. Run "${idbPath} list-targets" to fix this.`, ); } throw e; } function wrapWithErrorMessage(p: Promise, logger?: OutputChannelLogger): Promise { return p.catch((e: Error) => { logger?.error(e.message); // Give the user instructions. Don't embed the error because it's unique per invocation so won't be deduped. throw new Error( "A problem with idb has ocurred. Please run `sudo rm -rf /tmp/idb*` and `sudo yum install -y fb-idb` to update it, if that doesn't fix it, post in https://github.com/microsoft/vscode-react-native.", ); }); } export default { isAvailable, targets, push, pull, }; /** * @preserve * End region: https://github.com/facebook/flipper/blob/c2848df7f210c363113797c0f2e3db8c5d4fd49f/desktop/app/src/server/devices/ios/iOSContainerUtility.tsx */