From a4e743b420807ca728736b05ff8cfd25a75e0fdc Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Thu, 27 Aug 2026 17:00:12 -0700 Subject: [PATCH 1/5] [build-tools] install mitmproxy and pass --network-capture when a session requests it --- CHANGELOG.md | 1 + .../startAgentDeviceRemoteSession.ts | 7 ++ .../functions/startAppiumRemoteSession.ts | 7 ++ .../functions/startArgentRemoteSession.ts | 7 ++ .../functions/startServeSimRemoteSession.ts | 7 ++ .../__tests__/remoteDeviceRunSession.test.ts | 74 +++++++++++++++++++ .../src/steps/utils/remoteDeviceRunSession.ts | 51 ++++++++++++- 7 files changed, 153 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e07b8953e3..63bf27f9f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Cache CocoaPods dependencies between iOS builds. ([#4266](https://github.com/expo/eas-cli/pull/4266) by [@AbbanMustafa](https://github.com/AbbanMustafa)) - [build-tools] Support `EAS_BUN_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `bun install --filter`. ([#4292](https://github.com/expo/eas-cli/pull/4292) by [@konrad-armatys](https://github.com/konrad-armatys)) - [build-tools] Support `EAS_PNPM_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `pnpm install --filter`. ([#4302](https://github.com/expo/eas-cli/pull/4302) by [@robingullo](https://github.com/robingullo)) +- [build-tools] Install mitmproxy and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4300](https://github.com/expo/eas-cli/pull/4300) by [@gwdp](https://github.com/gwdp)) ### 🐛 Bug fixes diff --git a/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts b/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts index dbdd7b960e..ab73c428ed 100644 --- a/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts @@ -55,6 +55,11 @@ export function createStartAgentDeviceRemoteSessionBuildFunction( required: false, allowedValueTypeName: BuildStepInputValueTypeName.STRING, }), + BuildStepInput.createProvider({ + id: 'network_capture', + required: false, + allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, + }), BuildStepInput.createProvider({ id: 'max_idle_time_minutes', required: false, @@ -76,6 +81,7 @@ export function createStartAgentDeviceRemoteSessionBuildFunction( const ngrokAuthtoken = getNgrokAuthtokenOrThrow(env); const packageVersion = inputs.package_version.value as string | undefined; + const networkCapture = inputs.network_capture?.value === true; // A missing or non-positive value disables the idle timeout (opt-in feature). const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value as number | undefined; const maxDurationSeconds = inputs.max_duration_seconds?.value as number | undefined; @@ -120,6 +126,7 @@ export function createStartAgentDeviceRemoteSessionBuildFunction( env, logger, timeoutMs: STARTUP_TIMEOUT_MS, + networkCapture, }); logger.info(`Web preview URL: ${serveSim.previewUrl}`); } diff --git a/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts b/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts index f43ad2b12a..902b0d8bf4 100644 --- a/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts @@ -56,6 +56,11 @@ export function createStartAppiumRemoteSessionBuildFunction( required: false, allowedValueTypeName: BuildStepInputValueTypeName.STRING, }), + BuildStepInput.createProvider({ + id: 'network_capture', + required: false, + allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, + }), BuildStepInput.createProvider({ id: 'max_idle_time_minutes', required: false, @@ -67,6 +72,7 @@ export function createStartAppiumRemoteSessionBuildFunction( const ngrokTunnelDomain = getNgrokTunnelDomainOrThrow(env); const ngrokAuthtoken = getNgrokAuthtokenOrThrow(env); const packageVersion = inputs.package_version.value as string | undefined; + const networkCapture = inputs.network_capture?.value === true; const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value as number | undefined; const { runtimePlatform } = global; const versionSpec = resolveAppium3VersionSpec(packageVersion); @@ -135,6 +141,7 @@ export function createStartAppiumRemoteSessionBuildFunction( env, logger, timeoutMs: APPIUM_STARTUP_TIMEOUT_MS, + networkCapture, }); break; case BuildRuntimePlatform.LINUX: diff --git a/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts b/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts index eced65a17b..5c385293d4 100644 --- a/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts @@ -68,6 +68,11 @@ export function createStartArgentRemoteSessionBuildFunction( required: false, allowedValueTypeName: BuildStepInputValueTypeName.STRING, }), + BuildStepInput.createProvider({ + id: 'network_capture', + required: false, + allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, + }), BuildStepInput.createProvider({ id: 'max_idle_time_minutes', required: false, @@ -89,6 +94,7 @@ export function createStartArgentRemoteSessionBuildFunction( const ngrokAuthtoken = getNgrokAuthtokenOrThrow(env); const packageVersion = inputs.package_version.value as string | undefined; + const networkCapture = inputs.network_capture?.value === true; // A missing or non-positive value disables the idle timeout (opt-in feature). const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value as number | undefined; const maxDurationSeconds = inputs.max_duration_seconds?.value as number | undefined; @@ -207,6 +213,7 @@ export function createStartArgentRemoteSessionBuildFunction( env, logger, timeoutMs: STARTUP_TIMEOUT_MS, + networkCapture, }); webPreviewUrl = serveSim.previewUrl; logger.info(`Web preview URL: ${webPreviewUrl}`); diff --git a/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts b/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts index f9ed85ca26..1d5b1170b7 100644 --- a/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts @@ -37,12 +37,18 @@ export function createStartServeSimRemoteSessionBuildFunction( required: false, allowedValueTypeName: BuildStepInputValueTypeName.NUMBER, }), + BuildStepInput.createProvider({ + id: 'network_capture', + required: false, + allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, + }), ], fn: async ({ logger }, { inputs, env, signal }) => { const deviceRunSessionId = getDeviceRunSessionIdOrThrow(env); const ngrokTunnelDomain = getNgrokTunnelDomainOrThrow(env); const maxDurationSeconds = inputs.max_duration_seconds?.value as number | undefined; const packageVersion = inputs.package_version?.value as string | undefined; + const networkCapture = inputs.network_capture?.value === true; logger.info('Starting serve-sim remote session.'); @@ -54,6 +60,7 @@ export function createStartServeSimRemoteSessionBuildFunction( logger, timeoutMs: STARTUP_TIMEOUT_MS, packageVersion, + networkCapture, }); logger.info(`Preview URL: ${serveSim.previewUrl}`); diff --git a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts index 9e1e3be0c9..37edc79725 100644 --- a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts +++ b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts @@ -1,3 +1,4 @@ +import { SystemError } from '@expo/eas-build-job'; import { bunyan } from '@expo/logger'; import { BuildRuntimePlatform, BuildStepEnv } from '@expo/steps'; import spawn from '@expo/turtle-spawn'; @@ -15,6 +16,7 @@ import { sleepAsync } from '../../../utils/retry'; import { createServeSimArgs, ensureFfmpegInstalledAsync, + ensureMitmproxyInstalledAsync, fetchServeSimTurnArgsAsync, metricsCorsOriginToServeSimArgs, startNgrokTunnelAsync, @@ -171,6 +173,17 @@ describe(createServeSimArgs, () => { '@expo/serve-sim@next', ]); }); + + it('leaves network capture off unless it is asked for', () => { + expect(createServeSimArgs({ port: 4321 })).not.toContain('--network-capture'); + expect(createServeSimArgs({ port: 4321, networkCapture: false })).not.toContain( + '--network-capture' + ); + }); + + it('turns network capture on when requested', () => { + expect(createServeSimArgs({ port: 4321, networkCapture: true })).toContain('--network-capture'); + }); }); describe(metricsCorsOriginToServeSimArgs, () => { @@ -628,6 +641,67 @@ describe(waitForDeviceRunSessionStoppedAsync, () => { }); }); +describe(ensureMitmproxyInstalledAsync, () => { + const spawnMock = jest.mocked(spawn); + + function spawnResolved(): ReturnType { + return Promise.resolve({}) as unknown as ReturnType; + } + + function spawnRejected(): ReturnType { + return Promise.reject(new Error('boom')) as unknown as ReturnType; + } + + beforeEach(() => { + spawnMock.mockReset(); + }); + + it('does not install when mitmdump is on PATH', async () => { + spawnMock.mockReturnValueOnce(spawnResolved()); + + await ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }); + + expect(spawnMock).toHaveBeenCalledTimes(1); + expect(spawnMock).toHaveBeenCalledWith('mitmdump', ['--version'], expect.anything()); + }); + + it('installs mitmproxy with Homebrew when it is missing', async () => { + spawnMock + .mockReturnValueOnce(spawnRejected()) + .mockReturnValueOnce(spawnResolved()) + .mockReturnValueOnce(spawnResolved()); + + await ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }); + + expect(spawnMock).toHaveBeenCalledWith( + 'brew', + ['install', 'mitmproxy'], + expect.objectContaining({ + env: expect.objectContaining({ HOMEBREW_NO_AUTO_UPDATE: '1' }), + }) + ); + }); + + it('throws when the install fails, rather than starting a session that cannot capture', async () => { + spawnMock.mockReturnValueOnce(spawnRejected()).mockReturnValueOnce(spawnRejected()); + + await expect( + ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }) + ).rejects.toThrow(SystemError); + }); + + it('throws when brew succeeds but mitmdump still does not run', async () => { + spawnMock + .mockReturnValueOnce(spawnRejected()) + .mockReturnValueOnce(spawnResolved()) + .mockReturnValueOnce(spawnRejected()); + + await expect( + ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }) + ).rejects.toThrow(/still not runnable/); + }); +}); + describe(ensureFfmpegInstalledAsync, () => { const spawnMock = jest.mocked(spawn); diff --git a/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts b/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts index 176299e78f..2ea5003f88 100644 --- a/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts +++ b/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts @@ -14,6 +14,7 @@ import { clearTimeout, setTimeout } from 'node:timers'; import { setTimeout as setTimeoutAsync } from 'node:timers/promises'; import { CustomBuildContext } from '../../customBuildContext'; +import { Datadog } from '../../datadog'; import { Sentry } from '../../sentry'; import { sleepAsync } from '../../utils/retry'; import { turtleFetch } from '../../utils/turtleFetch'; @@ -344,6 +345,45 @@ async function installFfmpegWithAptAsync({ * `spawn` is not an async function and can throw synchronously, which * `asyncResult` cannot catch — it only wraps an already-created promise. */ +async function isMitmproxyAvailableAsync(env: BuildStepEnv): Promise { + return (await asyncResult(spawn('mitmdump', ['--version'], { env }))).ok; +} + +export async function ensureMitmproxyInstalledAsync({ + env, + logger, +}: { + env: BuildStepEnv; + logger: bunyan; +}): Promise { + if (await isMitmproxyAvailableAsync(env)) { + logger.info('mitmproxy is already installed.'); + return; + } + + logger.info('mitmproxy is not installed, installing it with Homebrew for network capture.'); + const startedAt = Date.now(); + try { + await spawn('brew', ['install', 'mitmproxy'], { + env: { ...env, HOMEBREW_NO_AUTO_UPDATE: '1' }, + logger, + }); + } catch (err) { + throw new SystemError('Could not install mitmproxy, which network capture needs.', { + cause: err, + }); + } + + if (!(await isMitmproxyAvailableAsync(env))) { + throw new SystemError( + 'Installed mitmproxy but mitmdump is still not runnable. Try `brew link mitmproxy` on the image.' + ); + } + + logger.info('Installed mitmproxy.'); + Datadog.distribution('eas.mitmproxy.install_duration', Date.now() - startedAt); +} + export async function ensureFfmpegInstalledAsync({ runtimePlatform, env, @@ -605,11 +645,13 @@ export function createServeSimArgs({ turnArgs = [], metricsCorsArgs = [], packageVersion, + networkCapture = false, }: { port: number; turnArgs?: string[]; metricsCorsArgs?: string[]; packageVersion?: string; + networkCapture?: boolean; }): string[] { return [ '--yes', @@ -632,6 +674,7 @@ export function createServeSimArgs({ SERVE_SIM_VIDEO_FPS, ...turnArgs, ...metricsCorsArgs, + ...(networkCapture ? ['--network-capture'] : []), ]; } @@ -707,14 +750,20 @@ export async function startServeSimWithTunnelAsync( logger, timeoutMs, packageVersion, + networkCapture = false, }: { baseDomain: string; env: BuildStepEnv; logger: bunyan; timeoutMs: number; packageVersion?: string; + networkCapture?: boolean; } ): Promise { + if (networkCapture) { + await ensureMitmproxyInstalledAsync({ env, logger }); + } + const port = await findAvailablePortAsync(); logger.info( `Launching ${createServeSimPackageSpec(packageVersion)} on ${SERVE_SIM_HOST}:${port}.` @@ -723,7 +772,7 @@ export async function startServeSimWithTunnelAsync( const metricsCorsArgs = metricsCorsOriginToServeSimArgs(env); const serveSim = spawnDetached({ command: 'npx', - args: createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion }), + args: createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion, networkCapture }), env, }); From b7b702cc7c135f1f6470d6af154c314d0a51f5f0 Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Fri, 28 Aug 2026 17:58:30 -0700 Subject: [PATCH 2/5] [build-tools] fix changelog pr link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bf27f9f6..a888d96a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Cache CocoaPods dependencies between iOS builds. ([#4266](https://github.com/expo/eas-cli/pull/4266) by [@AbbanMustafa](https://github.com/AbbanMustafa)) - [build-tools] Support `EAS_BUN_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `bun install --filter`. ([#4292](https://github.com/expo/eas-cli/pull/4292) by [@konrad-armatys](https://github.com/konrad-armatys)) - [build-tools] Support `EAS_PNPM_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `pnpm install --filter`. ([#4302](https://github.com/expo/eas-cli/pull/4302) by [@robingullo](https://github.com/robingullo)) -- [build-tools] Install mitmproxy and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4300](https://github.com/expo/eas-cli/pull/4300) by [@gwdp](https://github.com/gwdp)) +- [build-tools] Install mitmproxy and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4300](https://github.com/expo/eas-cli/pull/4307) by [@gwdp](https://github.com/gwdp)) ### 🐛 Bug fixes From 21917550c4361f0d68358ce5fdedb46715be3a2e Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Fri, 28 Aug 2026 17:59:57 -0700 Subject: [PATCH 3/5] [build-tools] fix changelog pr number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a888d96a31..88776ee999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Cache CocoaPods dependencies between iOS builds. ([#4266](https://github.com/expo/eas-cli/pull/4266) by [@AbbanMustafa](https://github.com/AbbanMustafa)) - [build-tools] Support `EAS_BUN_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `bun install --filter`. ([#4292](https://github.com/expo/eas-cli/pull/4292) by [@konrad-armatys](https://github.com/konrad-armatys)) - [build-tools] Support `EAS_PNPM_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `pnpm install --filter`. ([#4302](https://github.com/expo/eas-cli/pull/4302) by [@robingullo](https://github.com/robingullo)) -- [build-tools] Install mitmproxy and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4300](https://github.com/expo/eas-cli/pull/4307) by [@gwdp](https://github.com/gwdp)) +- [build-tools] Install mitmproxy and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4307](https://github.com/expo/eas-cli/pull/4307) by [@gwdp](https://github.com/gwdp)) ### 🐛 Bug fixes From c2d9336a3f23c5763eb5bc4c0d908611140ef313 Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Fri, 28 Aug 2026 18:52:49 -0700 Subject: [PATCH 4/5] [build-tools] address self review on mitmproxy install --- .../startAgentDeviceRemoteSession.ts | 2 +- .../functions/startAppiumRemoteSession.ts | 2 +- .../functions/startArgentRemoteSession.ts | 2 +- .../functions/startServeSimRemoteSession.ts | 10 +-- .../__tests__/remoteDeviceRunSession.test.ts | 38 ++++----- .../src/steps/utils/remoteDeviceRunSession.ts | 80 +++++++++---------- 6 files changed, 64 insertions(+), 70 deletions(-) diff --git a/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts b/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts index ab73c428ed..bca49dfe41 100644 --- a/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startAgentDeviceRemoteSession.ts @@ -81,7 +81,7 @@ export function createStartAgentDeviceRemoteSessionBuildFunction( const ngrokAuthtoken = getNgrokAuthtokenOrThrow(env); const packageVersion = inputs.package_version.value as string | undefined; - const networkCapture = inputs.network_capture?.value === true; + const networkCapture = inputs.network_capture?.value as boolean | undefined; // A missing or non-positive value disables the idle timeout (opt-in feature). const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value as number | undefined; const maxDurationSeconds = inputs.max_duration_seconds?.value as number | undefined; diff --git a/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts b/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts index 902b0d8bf4..e6411260fb 100644 --- a/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts @@ -72,7 +72,7 @@ export function createStartAppiumRemoteSessionBuildFunction( const ngrokTunnelDomain = getNgrokTunnelDomainOrThrow(env); const ngrokAuthtoken = getNgrokAuthtokenOrThrow(env); const packageVersion = inputs.package_version.value as string | undefined; - const networkCapture = inputs.network_capture?.value === true; + const networkCapture = inputs.network_capture?.value as boolean | undefined; const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value as number | undefined; const { runtimePlatform } = global; const versionSpec = resolveAppium3VersionSpec(packageVersion); diff --git a/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts b/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts index 5c385293d4..614bbe16ad 100644 --- a/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startArgentRemoteSession.ts @@ -94,7 +94,7 @@ export function createStartArgentRemoteSessionBuildFunction( const ngrokAuthtoken = getNgrokAuthtokenOrThrow(env); const packageVersion = inputs.package_version.value as string | undefined; - const networkCapture = inputs.network_capture?.value === true; + const networkCapture = inputs.network_capture?.value as boolean | undefined; // A missing or non-positive value disables the idle timeout (opt-in feature). const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value as number | undefined; const maxDurationSeconds = inputs.max_duration_seconds?.value as number | undefined; diff --git a/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts b/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts index 1d5b1170b7..cc039b375f 100644 --- a/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startServeSimRemoteSession.ts @@ -33,14 +33,14 @@ export function createStartServeSimRemoteSessionBuildFunction( allowedValueTypeName: BuildStepInputValueTypeName.STRING, }), BuildStepInput.createProvider({ - id: 'max_duration_seconds', + id: 'network_capture', required: false, - allowedValueTypeName: BuildStepInputValueTypeName.NUMBER, + allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, }), BuildStepInput.createProvider({ - id: 'network_capture', + id: 'max_duration_seconds', required: false, - allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN, + allowedValueTypeName: BuildStepInputValueTypeName.NUMBER, }), ], fn: async ({ logger }, { inputs, env, signal }) => { @@ -48,7 +48,7 @@ export function createStartServeSimRemoteSessionBuildFunction( const ngrokTunnelDomain = getNgrokTunnelDomainOrThrow(env); const maxDurationSeconds = inputs.max_duration_seconds?.value as number | undefined; const packageVersion = inputs.package_version?.value as string | undefined; - const networkCapture = inputs.network_capture?.value === true; + const networkCapture = inputs.network_capture?.value as boolean | undefined; logger.info('Starting serve-sim remote session.'); diff --git a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts index 37edc79725..13f16b9f9d 100644 --- a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts +++ b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts @@ -1,4 +1,3 @@ -import { SystemError } from '@expo/eas-build-job'; import { bunyan } from '@expo/logger'; import { BuildRuntimePlatform, BuildStepEnv } from '@expo/steps'; import spawn from '@expo/turtle-spawn'; @@ -115,6 +114,14 @@ function createEnvMock(): BuildStepEnv { return { DEVICE_RUN_SESSION_ID: 'drs-id' } as unknown as BuildStepEnv; } +function spawnResolved(): ReturnType { + return Promise.resolve({}) as unknown as ReturnType; +} + +function spawnRejected(): ReturnType { + return Promise.reject(new Error('boom')) as unknown as ReturnType; +} + describe(createServeSimArgs, () => { it('uses the latest Expo package and applies the EAS streaming policy', () => { expect( @@ -174,14 +181,14 @@ describe(createServeSimArgs, () => { ]); }); - it('leaves network capture off unless it is asked for', () => { + it('omits --network-capture by default', () => { expect(createServeSimArgs({ port: 4321 })).not.toContain('--network-capture'); expect(createServeSimArgs({ port: 4321, networkCapture: false })).not.toContain( '--network-capture' ); }); - it('turns network capture on when requested', () => { + it('appends --network-capture when enabled', () => { expect(createServeSimArgs({ port: 4321, networkCapture: true })).toContain('--network-capture'); }); }); @@ -644,14 +651,6 @@ describe(waitForDeviceRunSessionStoppedAsync, () => { describe(ensureMitmproxyInstalledAsync, () => { const spawnMock = jest.mocked(spawn); - function spawnResolved(): ReturnType { - return Promise.resolve({}) as unknown as ReturnType; - } - - function spawnRejected(): ReturnType { - return Promise.reject(new Error('boom')) as unknown as ReturnType; - } - beforeEach(() => { spawnMock.mockReset(); }); @@ -673,21 +672,24 @@ describe(ensureMitmproxyInstalledAsync, () => { await ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }); - expect(spawnMock).toHaveBeenCalledWith( + expect(spawnMock).toHaveBeenCalledTimes(3); + expect(spawnMock).toHaveBeenNthCalledWith( + 2, 'brew', ['install', 'mitmproxy'], expect.objectContaining({ env: expect.objectContaining({ HOMEBREW_NO_AUTO_UPDATE: '1' }), }) ); + expect(spawnMock).toHaveBeenLastCalledWith('mitmdump', ['--version'], expect.anything()); }); - it('throws when the install fails, rather than starting a session that cannot capture', async () => { + it('throws when the install fails', async () => { spawnMock.mockReturnValueOnce(spawnRejected()).mockReturnValueOnce(spawnRejected()); await expect( ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }) - ).rejects.toThrow(SystemError); + ).rejects.toThrow(/Could not install mitmproxy/); }); it('throws when brew succeeds but mitmdump still does not run', async () => { @@ -705,14 +707,6 @@ describe(ensureMitmproxyInstalledAsync, () => { describe(ensureFfmpegInstalledAsync, () => { const spawnMock = jest.mocked(spawn); - function spawnResolved(): ReturnType { - return Promise.resolve({}) as unknown as ReturnType; - } - - function spawnRejected(): ReturnType { - return Promise.reject(new Error('boom')) as unknown as ReturnType; - } - beforeEach(() => { spawnMock.mockReset(); jest.mocked(Sentry).capture.mockReset(); diff --git a/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts b/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts index 2ea5003f88..fe120c4af5 100644 --- a/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts +++ b/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts @@ -14,7 +14,6 @@ import { clearTimeout, setTimeout } from 'node:timers'; import { setTimeout as setTimeoutAsync } from 'node:timers/promises'; import { CustomBuildContext } from '../../customBuildContext'; -import { Datadog } from '../../datadog'; import { Sentry } from '../../sentry'; import { sleepAsync } from '../../utils/retry'; import { turtleFetch } from '../../utils/turtleFetch'; @@ -345,45 +344,6 @@ async function installFfmpegWithAptAsync({ * `spawn` is not an async function and can throw synchronously, which * `asyncResult` cannot catch — it only wraps an already-created promise. */ -async function isMitmproxyAvailableAsync(env: BuildStepEnv): Promise { - return (await asyncResult(spawn('mitmdump', ['--version'], { env }))).ok; -} - -export async function ensureMitmproxyInstalledAsync({ - env, - logger, -}: { - env: BuildStepEnv; - logger: bunyan; -}): Promise { - if (await isMitmproxyAvailableAsync(env)) { - logger.info('mitmproxy is already installed.'); - return; - } - - logger.info('mitmproxy is not installed, installing it with Homebrew for network capture.'); - const startedAt = Date.now(); - try { - await spawn('brew', ['install', 'mitmproxy'], { - env: { ...env, HOMEBREW_NO_AUTO_UPDATE: '1' }, - logger, - }); - } catch (err) { - throw new SystemError('Could not install mitmproxy, which network capture needs.', { - cause: err, - }); - } - - if (!(await isMitmproxyAvailableAsync(env))) { - throw new SystemError( - 'Installed mitmproxy but mitmdump is still not runnable. Try `brew link mitmproxy` on the image.' - ); - } - - logger.info('Installed mitmproxy.'); - Datadog.distribution('eas.mitmproxy.install_duration', Date.now() - startedAt); -} - export async function ensureFfmpegInstalledAsync({ runtimePlatform, env, @@ -423,6 +383,46 @@ export async function ensureFfmpegInstalledAsync({ } } +const MITMPROXY_INSTALL_TIMEOUT_MS = 5 * 60 * 1000; + +async function isMitmproxyAvailableAsync(env: BuildStepEnv): Promise { + return (await asyncResult(spawn('mitmdump', ['--version'], { env }))).ok; +} + +export async function ensureMitmproxyInstalledAsync({ + env, + logger, +}: { + env: BuildStepEnv; + logger: bunyan; +}): Promise { + if (await isMitmproxyAvailableAsync(env)) { + logger.info('mitmproxy is already installed.'); + return; + } + + logger.info('mitmproxy is not installed, installing it with Homebrew for network capture.'); + try { + await spawn('brew', ['install', 'mitmproxy'], { + env: { ...env, HOMEBREW_NO_AUTO_UPDATE: '1' }, + logger, + timeout: MITMPROXY_INSTALL_TIMEOUT_MS, + }); + } catch (err) { + throw new SystemError('Could not install mitmproxy for network capture.', { + cause: err, + }); + } + + if (!(await isMitmproxyAvailableAsync(env))) { + throw new SystemError( + 'Installed mitmproxy but mitmdump is still not runnable. Try `brew reinstall --cask mitmproxy` on the image.' + ); + } + + logger.info('Installed mitmproxy.'); +} + const TurnIceServersResponseSchema = z.object({ data: z.object({ iceServers: TurnIceServersSchema, From aad96c41eee2a7f836ddc6f9d8aa8163f403af86 Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Tue, 1 Sep 2026 21:43:43 -0700 Subject: [PATCH 5/5] [build-tools] install mitmproxy from the turtle-v2 artifact in its own step --- CHANGELOG.md | 2 +- .../build-tools/src/steps/easFunctions.ts | 2 + .../__tests__/installMitmproxy.test.ts | 124 ++++++++++++++++++ .../src/steps/functions/installMitmproxy.ts | 95 ++++++++++++++ .../__tests__/remoteDeviceRunSession.test.ts | 57 -------- .../src/steps/utils/remoteDeviceRunSession.ts | 44 ------- 6 files changed, 222 insertions(+), 102 deletions(-) create mode 100644 packages/build-tools/src/steps/functions/__tests__/installMitmproxy.test.ts create mode 100644 packages/build-tools/src/steps/functions/installMitmproxy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 88776ee999..2fcfeeaaba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Cache CocoaPods dependencies between iOS builds. ([#4266](https://github.com/expo/eas-cli/pull/4266) by [@AbbanMustafa](https://github.com/AbbanMustafa)) - [build-tools] Support `EAS_BUN_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `bun install --filter`. ([#4292](https://github.com/expo/eas-cli/pull/4292) by [@konrad-armatys](https://github.com/konrad-armatys)) - [build-tools] Support `EAS_PNPM_FILTER_WORKSPACE` to install only the selected workspace's dependencies with `pnpm install --filter`. ([#4302](https://github.com/expo/eas-cli/pull/4302) by [@robingullo](https://github.com/robingullo)) -- [build-tools] Install mitmproxy and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4307](https://github.com/expo/eas-cli/pull/4307) by [@gwdp](https://github.com/gwdp)) +- [build-tools] Add the `eas/install_mitmproxy` step and start serve-sim with `--network-capture` when a simulator session requests network capture. ([#4307](https://github.com/expo/eas-cli/pull/4307) by [@gwdp](https://github.com/gwdp)) ### 🐛 Bug fixes diff --git a/packages/build-tools/src/steps/easFunctions.ts b/packages/build-tools/src/steps/easFunctions.ts index 6ba0d71e89..cc6f539950 100644 --- a/packages/build-tools/src/steps/easFunctions.ts +++ b/packages/build-tools/src/steps/easFunctions.ts @@ -19,6 +19,7 @@ import { generateGymfileFromTemplateFunction } from './functions/generateGymfile import { createGetCredentialsForBuildTriggeredByGithubIntegration } from './functions/getCredentialsForBuildTriggeredByGitHubIntegration'; import { injectAndroidCredentialsFunction } from './functions/injectAndroidCredentials'; import { createInstallMaestroBuildFunction } from './functions/installMaestro'; +import { createInstallMitmproxyBuildFunction } from './functions/installMitmproxy'; import { createInstallBuildFunction } from './functions/installBuild'; import { createInstallNodeModulesBuildFunction } from './functions/installNodeModules'; import { createInstallPodsBuildFunction } from './functions/installPods'; @@ -110,6 +111,7 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] { createStartServeSimMetricsBuildFunction(), createCollectServeSimMetricsBuildFunction(ctx), createInstallMaestroBuildFunction(), + createInstallMitmproxyBuildFunction(), createInstallPodsBuildFunction(), createSendSlackMessageFunction(), diff --git a/packages/build-tools/src/steps/functions/__tests__/installMitmproxy.test.ts b/packages/build-tools/src/steps/functions/__tests__/installMitmproxy.test.ts new file mode 100644 index 0000000000..48c26ebcfc --- /dev/null +++ b/packages/build-tools/src/steps/functions/__tests__/installMitmproxy.test.ts @@ -0,0 +1,124 @@ +import { BuildRuntimePlatform } from '@expo/steps'; +import spawn from '@expo/turtle-spawn'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { createGlobalContextMock } from '../../../__tests__/utils/context'; +import { decompressTarAsync } from '../../../utils/files'; +import { createInstallMitmproxyBuildFunction } from '../installMitmproxy'; + +jest.mock('@expo/turtle-spawn', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('../../../utils/files', () => ({ + decompressTarAsync: jest.fn(), +})); + +const mockedSpawn = jest.mocked(spawn); +const mockedDecompressTarAsync = jest.mocked(decompressTarAsync); + +function spawnResolved(): ReturnType { + return Promise.resolve({}) as unknown as ReturnType; +} + +function spawnRejected(): ReturnType { + return Promise.reject(new Error('not found')) as unknown as ReturnType; +} + +describe('createInstallMitmproxyBuildFunction', () => { + let homeDirectory: string; + + beforeEach(async () => { + jest.clearAllMocks(); + mockedDecompressTarAsync.mockResolvedValue(undefined); + homeDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'install-mitmproxy-test')); + }); + + afterEach(async () => { + await fs.promises.rm(homeDirectory, { force: true, recursive: true }); + }); + + function createStep( + env: Record + ): ReturnType< + ReturnType['createBuildStepFromFunctionCall'] + > { + const globalCtx = createGlobalContextMock({ runtimePlatform: BuildRuntimePlatform.DARWIN }); + globalCtx.updateEnv({ ...globalCtx.env, HOME: homeDirectory, ...env }); + return createInstallMitmproxyBuildFunction().createBuildStepFromFunctionCall(globalCtx, { + callInputs: {}, + }); + } + + it('does not download when mitmdump is already on PATH', async () => { + mockedSpawn.mockReturnValueOnce(spawnResolved()); + + await createStep({ EAS_BUILD_RUNNER: 'eas-build' }).executeAsync(); + + expect(mockedSpawn).toHaveBeenCalledTimes(1); + expect(mockedDecompressTarAsync).not.toHaveBeenCalled(); + }); + + it('does not touch the machine outside EAS Build VMs', async () => { + mockedSpawn.mockReturnValueOnce(spawnRejected()); + + await createStep({}).executeAsync(); + + expect(mockedSpawn).toHaveBeenCalledTimes(1); + expect(mockedDecompressTarAsync).not.toHaveBeenCalled(); + }); + + it('downloads the pinned artifact from the turtle-v2 bucket and extracts it', async () => { + mockedSpawn + .mockReturnValueOnce(spawnRejected()) + .mockReturnValueOnce(spawnResolved()) + .mockReturnValueOnce(spawnResolved()); + + await createStep({ EAS_BUILD_RUNNER: 'eas-build' }).executeAsync(); + + expect(mockedSpawn).toHaveBeenNthCalledWith( + 2, + 'curl', + [ + '--fail', + '--location', + '--output', + expect.stringContaining('mitmproxy.tar.gz'), + 'https://storage.googleapis.com/turtle-v2/mitmproxy-12.2.3-macos-arm64.tar.gz', + ], + expect.anything() + ); + expect(mockedDecompressTarAsync).toHaveBeenCalledWith({ + archivePath: expect.stringContaining('mitmproxy.tar.gz'), + destinationDirectory: path.join(homeDirectory, '.eas-mitmproxy'), + }); + }); + + it('puts the extracted binaries on PATH for later steps', async () => { + mockedSpawn + .mockReturnValueOnce(spawnRejected()) + .mockReturnValueOnce(spawnResolved()) + .mockReturnValueOnce(spawnResolved()); + + const step = createStep({ EAS_BUILD_RUNNER: 'eas-build' }); + await step.executeAsync(); + + expect(step.ctx.global.env.PATH).toContain( + path.join(homeDirectory, '.eas-mitmproxy', 'mitmproxy.app', 'Contents', 'MacOS') + ); + }); + + it('throws when mitmdump is still not runnable after the install', async () => { + mockedSpawn + .mockReturnValueOnce(spawnRejected()) + .mockReturnValueOnce(spawnResolved()) + .mockReturnValueOnce(spawnRejected()); + + await expect(createStep({ EAS_BUILD_RUNNER: 'eas-build' }).executeAsync()).rejects.toThrow( + /mitmdump is still not runnable/ + ); + }); +}); diff --git a/packages/build-tools/src/steps/functions/installMitmproxy.ts b/packages/build-tools/src/steps/functions/installMitmproxy.ts new file mode 100644 index 0000000000..3d8c8c193b --- /dev/null +++ b/packages/build-tools/src/steps/functions/installMitmproxy.ts @@ -0,0 +1,95 @@ +import { bunyan } from '@expo/logger'; +import { asyncResult } from '@expo/results'; +import { + BuildFunction, + BuildRuntimePlatform, + BuildStepEnv, + BuildStepGlobalContext, +} from '@expo/steps'; +import spawn from '@expo/turtle-spawn'; +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { decompressTarAsync } from '../../utils/files'; + +const MITMPROXY_VERSION = '12.2.3'; +const MITMPROXY_DOWNLOAD_URL = `https://storage.googleapis.com/turtle-v2/mitmproxy-${MITMPROXY_VERSION}-macos-arm64.tar.gz`; + +export function createInstallMitmproxyBuildFunction(): BuildFunction { + return new BuildFunction({ + namespace: 'eas', + id: 'install_mitmproxy', + name: 'Install mitmproxy', + supportedRuntimePlatforms: [BuildRuntimePlatform.DARWIN], + fn: async ({ logger, global }, { env }) => { + if (await isMitmproxyAvailableAsync(env)) { + logger.info('mitmproxy is already installed.'); + return; + } + + if (env.EAS_BUILD_RUNNER !== 'eas-build') { + logger.warn( + 'mitmproxy is not installed and network capture needs it. Install it with `brew install mitmproxy` and rerun the job.' + ); + return; + } + + await installMitmproxyFromGcsAsync({ logger, env, global }); + + if (!(await isMitmproxyAvailableAsync(env))) { + throw new Error( + `Installed mitmproxy ${MITMPROXY_VERSION} but mitmdump is still not runnable. The worker image may not match the artifact's macOS version or architecture; check the download and extract logs above.` + ); + } + + logger.info(`Installed mitmproxy ${MITMPROXY_VERSION}.`); + }, + }); +} + +async function isMitmproxyAvailableAsync(env: BuildStepEnv): Promise { + return (await asyncResult(spawn('mitmdump', ['--version'], { env }))).ok; +} + +async function installMitmproxyFromGcsAsync({ + logger, + env, + global, +}: { + logger: bunyan; + env: BuildStepEnv; + global: BuildStepGlobalContext; +}): Promise { + assert( + env.HOME, + 'Failed to infer directory to install mitmproxy in: $HOME environment variable is empty.' + ); + const installDirectory = path.join(env.HOME, '.eas-mitmproxy'); + const tempDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'install_mitmproxy')); + const archivePath = path.join(tempDirectory, 'mitmproxy.tar.gz'); + + try { + logger.info(`Downloading mitmproxy ${MITMPROXY_VERSION}`); + await spawn('curl', ['--fail', '--location', '--output', archivePath, MITMPROXY_DOWNLOAD_URL], { + env, + logger, + }); + + await fs.promises.rm(installDirectory, { force: true, recursive: true }); + await fs.promises.mkdir(installDirectory, { recursive: true }); + logger.info('Extracting mitmproxy'); + await decompressTarAsync({ archivePath, destinationDirectory: installDirectory }); + } finally { + await fs.promises.rm(tempDirectory, { force: true, recursive: true }); + } + + const mitmproxyBinDir = path.join(installDirectory, 'mitmproxy.app', 'Contents', 'MacOS'); + global.updateEnv({ + ...global.env, + PATH: `${global.env.PATH}:${mitmproxyBinDir}`, + }); + env.PATH = `${env.PATH}:${mitmproxyBinDir}`; + process.env.PATH = `${process.env.PATH}:${mitmproxyBinDir}`; +} diff --git a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts index 13f16b9f9d..40d62f3a84 100644 --- a/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts +++ b/packages/build-tools/src/steps/utils/__tests__/remoteDeviceRunSession.test.ts @@ -15,7 +15,6 @@ import { sleepAsync } from '../../../utils/retry'; import { createServeSimArgs, ensureFfmpegInstalledAsync, - ensureMitmproxyInstalledAsync, fetchServeSimTurnArgsAsync, metricsCorsOriginToServeSimArgs, startNgrokTunnelAsync, @@ -648,62 +647,6 @@ describe(waitForDeviceRunSessionStoppedAsync, () => { }); }); -describe(ensureMitmproxyInstalledAsync, () => { - const spawnMock = jest.mocked(spawn); - - beforeEach(() => { - spawnMock.mockReset(); - }); - - it('does not install when mitmdump is on PATH', async () => { - spawnMock.mockReturnValueOnce(spawnResolved()); - - await ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }); - - expect(spawnMock).toHaveBeenCalledTimes(1); - expect(spawnMock).toHaveBeenCalledWith('mitmdump', ['--version'], expect.anything()); - }); - - it('installs mitmproxy with Homebrew when it is missing', async () => { - spawnMock - .mockReturnValueOnce(spawnRejected()) - .mockReturnValueOnce(spawnResolved()) - .mockReturnValueOnce(spawnResolved()); - - await ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }); - - expect(spawnMock).toHaveBeenCalledTimes(3); - expect(spawnMock).toHaveBeenNthCalledWith( - 2, - 'brew', - ['install', 'mitmproxy'], - expect.objectContaining({ - env: expect.objectContaining({ HOMEBREW_NO_AUTO_UPDATE: '1' }), - }) - ); - expect(spawnMock).toHaveBeenLastCalledWith('mitmdump', ['--version'], expect.anything()); - }); - - it('throws when the install fails', async () => { - spawnMock.mockReturnValueOnce(spawnRejected()).mockReturnValueOnce(spawnRejected()); - - await expect( - ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }) - ).rejects.toThrow(/Could not install mitmproxy/); - }); - - it('throws when brew succeeds but mitmdump still does not run', async () => { - spawnMock - .mockReturnValueOnce(spawnRejected()) - .mockReturnValueOnce(spawnResolved()) - .mockReturnValueOnce(spawnRejected()); - - await expect( - ensureMitmproxyInstalledAsync({ env: createEnvMock(), logger: createLoggerMock() }) - ).rejects.toThrow(/still not runnable/); - }); -}); - describe(ensureFfmpegInstalledAsync, () => { const spawnMock = jest.mocked(spawn); diff --git a/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts b/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts index fe120c4af5..e84cb41477 100644 --- a/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts +++ b/packages/build-tools/src/steps/utils/remoteDeviceRunSession.ts @@ -383,46 +383,6 @@ export async function ensureFfmpegInstalledAsync({ } } -const MITMPROXY_INSTALL_TIMEOUT_MS = 5 * 60 * 1000; - -async function isMitmproxyAvailableAsync(env: BuildStepEnv): Promise { - return (await asyncResult(spawn('mitmdump', ['--version'], { env }))).ok; -} - -export async function ensureMitmproxyInstalledAsync({ - env, - logger, -}: { - env: BuildStepEnv; - logger: bunyan; -}): Promise { - if (await isMitmproxyAvailableAsync(env)) { - logger.info('mitmproxy is already installed.'); - return; - } - - logger.info('mitmproxy is not installed, installing it with Homebrew for network capture.'); - try { - await spawn('brew', ['install', 'mitmproxy'], { - env: { ...env, HOMEBREW_NO_AUTO_UPDATE: '1' }, - logger, - timeout: MITMPROXY_INSTALL_TIMEOUT_MS, - }); - } catch (err) { - throw new SystemError('Could not install mitmproxy for network capture.', { - cause: err, - }); - } - - if (!(await isMitmproxyAvailableAsync(env))) { - throw new SystemError( - 'Installed mitmproxy but mitmdump is still not runnable. Try `brew reinstall --cask mitmproxy` on the image.' - ); - } - - logger.info('Installed mitmproxy.'); -} - const TurnIceServersResponseSchema = z.object({ data: z.object({ iceServers: TurnIceServersSchema, @@ -760,10 +720,6 @@ export async function startServeSimWithTunnelAsync( networkCapture?: boolean; } ): Promise { - if (networkCapture) { - await ensureMitmproxyInstalledAsync({ env, logger }); - } - const port = await findAvailablePortAsync(); logger.info( `Launching ${createServeSimPackageSpec(packageVersion)} on ${SERVE_SIM_HOST}:${port}.`