From 472eb9de6dcb29a89f558cd579c9c0d7b5c81f91 Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 21 Sep 2026 16:39:05 +0200 Subject: [PATCH 1/2] fix(android): configure ADB timeout through environment Use NATIVE_RUN_ADB_TIMEOUT env variable for all standard ADB operations while retaining ADB_TIMEOUT as a legacy fallback, and document the setting in Android CLI help. This prevents ADB timeout errors on setups with high latency. --- src/android/help.ts | 6 +++++ src/android/utils/__tests__/adb.ts | 39 ++++++++++++++++++++++++++++++ src/android/utils/adb.ts | 26 ++++++++++++-------- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/android/help.ts b/src/android/help.ts index d8a7e5a..fae38bd 100644 --- a/src/android/help.ts +++ b/src/android/help.ts @@ -28,6 +28,12 @@ const help = ` --target ........... Use a specific target --connect ............... Tie process to app process --forward ... Forward a port from device to host + + ADB operations use a default timeout of 5000 milliseconds. Set + NATIVE_RUN_ADB_TIMEOUT to change it for all ADB operations. + The legacy ADB_TIMEOUT variable is also supported, but + NATIVE_RUN_ADB_TIMEOUT takes precedence when both are set. + Invalid or non-positive values fall back to the 5000 millisecond default. `; export async function run(args: readonly string[]): Promise { diff --git a/src/android/utils/__tests__/adb.ts b/src/android/utils/__tests__/adb.ts index 663160c..24bebb7 100644 --- a/src/android/utils/__tests__/adb.ts +++ b/src/android/utils/__tests__/adb.ts @@ -3,6 +3,45 @@ import * as os from 'os'; import type * as adb from '../adb'; describe('android/utils/adb', () => { + describe('getAdbTimeout', () => { + let adbUtils: typeof adb; + + beforeEach(async () => { + jest.resetModules(); + adbUtils = await import('../adb'); + }); + + afterEach(() => { + delete process.env.NATIVE_RUN_ADB_TIMEOUT; + delete process.env.ADB_TIMEOUT; + }); + + it('should read the timeout from NATIVE_RUN_ADB_TIMEOUT', () => { + process.env.NATIVE_RUN_ADB_TIMEOUT = '12345'; + + expect(adbUtils.getAdbTimeout()).toBe(12345); + }); + + it('should prefer NATIVE_RUN_ADB_TIMEOUT over the legacy ADB_TIMEOUT', () => { + process.env.NATIVE_RUN_ADB_TIMEOUT = '12345'; + process.env.ADB_TIMEOUT = '67890'; + + expect(adbUtils.getAdbTimeout()).toBe(12345); + }); + + it('should support the legacy ADB_TIMEOUT variable', () => { + process.env.ADB_TIMEOUT = '12345'; + + expect(adbUtils.getAdbTimeout()).toBe(12345); + }); + + it('should fall back to 5000 when the timeout is invalid', () => { + process.env.ADB_TIMEOUT = 'nope'; + + expect(adbUtils.getAdbTimeout()).toBe(5000); + }); + }); + describe('parseAdbDevices', () => { let adbUtils: typeof adb; diff --git a/src/android/utils/adb.ts b/src/android/utils/adb.ts index 3d0a81f..c25e7d9 100644 --- a/src/android/utils/adb.ts +++ b/src/android/utils/adb.ts @@ -57,7 +57,7 @@ export async function getDevices(sdk: SDK): Promise { const args = ['devices', '-l']; debug('Invoking adb with args: %O', args); - const stdout = await execAdb(sdk, args, { timeout: 5000 }); + const stdout = await execAdb(sdk, args); const devices = parseAdbDevices(stdout); @@ -85,7 +85,7 @@ export async function getDeviceProperty(sdk: SDK, device: Device, property: stri const args = ['-s', device.serial, 'shell', 'getprop', property]; debug('Invoking adb with args: %O', args); - const stdout = await execAdb(sdk, args, { timeout: 5000 }); + const stdout = await execAdb(sdk, args); return stdout.trim(); } @@ -95,7 +95,7 @@ export async function getDeviceProperties(sdk: SDK, device: Device): Promise { @@ -348,7 +348,12 @@ export async function unforwardPorts(sdk: SDK, device: Device, ports: Ports): Pr const args = ['-s', device.serial, 'reverse', '--remove', `tcp:${ports.device}`]; debug('Invoking adb with args: %O', args); - await execAdb(sdk, args, { timeout: 5000 }); + await execAdb(sdk, args); +} + +export function getAdbTimeout(): number { + const timeout = Number.parseFloat(process.env.NATIVE_RUN_ADB_TIMEOUT || process.env.ADB_TIMEOUT || '5000'); + return Number.isFinite(timeout) && timeout > 0 ? timeout : 5000; } export interface ExecADBOptions { @@ -358,16 +363,17 @@ export interface ExecADBOptions { export async function execAdb(sdk: SDK, args: string[], options: ExecADBOptions = {}): Promise { const debug = Debug(`${modulePrefix}:${execAdb.name}`); let timer: NodeJS.Timer | undefined; + const timeout = typeof options.timeout === 'number' ? options.timeout : getAdbTimeout(); const retry = async () => { - const msg = `ADBs is unresponsive after ${options.timeout}ms, killing server and retrying...\n`; + const msg = `ADBs is unresponsive after ${timeout}ms, killing server and retrying...\n`; if (process.argv.includes('--json')) { debug(msg); } else { process.stderr.write(msg); } - debug('ADB timeout of %O reached, killing server and retrying...', options.timeout); + debug('ADB timeout of %O reached, killing server and retrying...', timeout); debug('Invoking adb with args: %O', ['kill-server']); await execAdb(sdk, ['kill-server']); debug('Invoking adb with args: %O', ['start-server']); @@ -392,8 +398,8 @@ export async function execAdb(sdk: SDK, args: string[], options: ExecADBOptions }; return new Promise((resolve, reject) => { - if (options.timeout) { - timer = setTimeout(() => retry().then(resolve, reject), options.timeout); + if (timeout) { + timer = setTimeout(() => retry().then(resolve, reject), timeout); } run().then(resolve, (err) => { From 59b1132f5908c1136e5341f4de9e035ae5736508 Mon Sep 17 00:00:00 2001 From: luisbytes <137983537+luisbytes@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:53:00 -0500 Subject: [PATCH 2/2] feat: support device hub for Xcode 27+ (#408) --- src/errors.ts | 10 ++-------- src/ios/utils/simulator.ts | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index bae1fc4..cb122ec 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -78,10 +78,7 @@ export type AVDExceptionCode = typeof ERR_UNSUITABLE_API_INSTALLATION; export class AVDException extends AndroidException {} export type EmulatorExceptionCode = - | typeof ERR_ALREADY_RUNNING - | typeof ERR_AVD_HOME_NOT_FOUND - | typeof ERR_NON_ZERO_EXIT - | typeof ERR_UNKNOWN_AVD; + typeof ERR_ALREADY_RUNNING | typeof ERR_AVD_HOME_NOT_FOUND | typeof ERR_NON_ZERO_EXIT | typeof ERR_UNKNOWN_AVD; export class EmulatorException extends AndroidException {} @@ -98,10 +95,7 @@ export type SDKExceptionCode = export class SDKException extends AndroidException {} export type IOSRunExceptionCode = - | typeof ERR_TARGET_NOT_FOUND - | typeof ERR_NO_DEVICE - | typeof ERR_NO_TARGET - | typeof ERR_DEVICE_LOCKED; + typeof ERR_TARGET_NOT_FOUND | typeof ERR_NO_DEVICE | typeof ERR_NO_TARGET | typeof ERR_DEVICE_LOCKED; export class IOSRunException extends Exception {} diff --git a/src/ios/utils/simulator.ts b/src/ios/utils/simulator.ts index ebe8a4c..cc15c74 100644 --- a/src/ios/utils/simulator.ts +++ b/src/ios/utils/simulator.ts @@ -1,3 +1,4 @@ +import { pathExists } from '@ionic/utils-fs'; import { spawnSync } from 'child_process'; // TODO: need cross-spawn for windows? import * as Debug from 'debug'; @@ -85,11 +86,22 @@ export async function runOnSimulator(udid: string, appPath: string, bundleId: st const xCodePath = await getXCodePath(); debug(`Running simulator ${udid}`); - const openResult = spawnSync( - 'open', - [`${xCodePath}/Applications/Simulator.app`, '--args', '-CurrentDeviceUDID', udid], - { encoding: 'utf8' }, - ); + + const simulatorApp = `${xCodePath}/Applications/Simulator.app`; + const deviceHubApp = `${xCodePath}/../Applications/DeviceHub.app`; + + let openArgs: string[]; + + if (await pathExists(simulatorApp)) { + openArgs = [simulatorApp, '--args', '-CurrentDeviceUDID', udid]; + } else if (await pathExists(deviceHubApp)) { + openArgs = [`devices://device/open?id=${udid}`]; + } else { + throw new Exception(`Unable to find Simulator.app or DeviceHub.app in ${xCodePath}`); + } + + const openResult = spawnSync('open', openArgs, { encoding: 'utf8' }); + if (openResult.status) { throw new Exception(`There was an error opening simulator: ${openResult.stderr}`); }