From e9ae39d0ad057b1715c7f2e9013ba5c98730e328 Mon Sep 17 00:00:00 2001 From: EmmaYuan1015 Date: Fri, 18 Sep 2026 09:40:53 +0800 Subject: [PATCH 1/4] Fix Metro process termination error logging --- .../commands/cleanRestartPackager.ts | 60 ++++++++++++------- .../commands/cleanRestartPackager.test.ts | 52 ++++++++++++++++ 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/extension/commands/cleanRestartPackager.ts b/src/extension/commands/cleanRestartPackager.ts index dae1b2baa..ebfbbb4d4 100644 --- a/src/extension/commands/cleanRestartPackager.ts +++ b/src/extension/commands/cleanRestartPackager.ts @@ -57,45 +57,59 @@ export class CleanRestartPackager extends ReactNativeCommand { if (platformId === HostPlatformId.WINDOWS) { // Windows: Use netstat and taskkill + let outcome: string; try { const netstatResult = await childProcess.exec( `netstat -ano | findstr :${port}`, ); - const outcome = await netstatResult.outcome; - - if (outcome) { - // Extract PID from netstat output - const lines = outcome.split("\n"); - for (const line of lines) { - const match = line.match(/\s+LISTENING\s+(\d+)/); - if (match && match[1]) { - const pid = match[1]; - logger.info(`Found Metro process with PID: ${pid}`); - await childProcess - .exec(`taskkill /PID ${pid} /F /T`) - .then(killResult => killResult.outcome); - logger.info(`Successfully terminated process ${pid}`); - } - } - } + outcome = await netstatResult.outcome; } catch (error) { logger.info(`No Metro process found on port ${port}`); + return; + } + + // Extract PID from netstat output + const lines = outcome.split("\n"); + for (const line of lines) { + const match = line.match(/\s+LISTENING\s+(\d+)/); + if (match && match[1]) { + const pid = match[1]; + logger.info(`Found Metro process with PID: ${pid}`); + try { + await childProcess + .exec(`taskkill /PID ${pid} /F /T`) + .then(killResult => killResult.outcome); + logger.info(`Successfully terminated process ${pid}`); + } catch (error) { + logger.warning( + `Failed to terminate Metro process ${pid}: ${String(error)}`, + ); + } + } } } else { // macOS/Linux: Use lsof and kill + let outcome: string; try { const lsofResult = await childProcess.exec(`lsof -ti:${port}`); - const outcome = await lsofResult.outcome; + outcome = await lsofResult.outcome; + } catch (error) { + logger.info(`No Metro process found on port ${port}`); + return; + } - if (outcome && outcome.trim()) { - const pid = outcome.trim(); - logger.info(`Found Metro process with PID: ${pid}`); + if (outcome && outcome.trim()) { + const pid = outcome.trim(); + logger.info(`Found Metro process with PID: ${pid}`); + try { const killResult = await childProcess.exec(`kill -9 ${pid}`); await killResult.outcome; logger.info(`Successfully terminated process ${pid}`); + } catch (error) { + logger.warning( + `Failed to terminate Metro process ${pid}: ${String(error)}`, + ); } - } catch (error) { - logger.info(`No Metro process found on port ${port}`); } } } catch (error) { diff --git a/test/extension/commands/cleanRestartPackager.test.ts b/test/extension/commands/cleanRestartPackager.test.ts index 62225e366..ba83b4534 100644 --- a/test/extension/commands/cleanRestartPackager.test.ts +++ b/test/extension/commands/cleanRestartPackager.test.ts @@ -176,6 +176,33 @@ suite("cleanRestartPackagerCommand", function () { assert.strictEqual(restartStub.calledOnce, true); }); + test("should warn when terminating a Metro process fails on Windows", async function () { + const execStub = Sinon.stub(); + execStub + .withArgs("netstat -ano | findstr :9090") + .returns(createExecResult("TCP 127.0.0.1:9090 0.0.0.0:0 LISTENING 12345")); + execStub + .withArgs("taskkill /PID 12345 /F /T") + .returns(createRejectedExecResult(new Error("access denied"))); + execStub.withArgs("watchman watch-del-all").returns(createExecResult("")); + const restartStub = Sinon.stub().returns(Promise.resolve()); + const { CleanRestartPackager, logger } = createCommandModule( + HostPlatformId.WINDOWS, + execStub, + ); + + await runCommand(CleanRestartPackager, tempDir, restartStub); + + assert.strictEqual( + logger.warning.calledWith( + "Failed to terminate Metro process 12345: Error: access denied", + ), + true, + ); + assert.strictEqual(logger.info.calledWith("No Metro process found on port 9090"), false); + assert.strictEqual(restartStub.calledOnce, true); + }); + test("should kill Metro process on macOS and restart packager", async function () { const execStub = Sinon.stub(); execStub.withArgs("lsof -ti:9090").returns(createExecResult("23456\n")); @@ -193,6 +220,31 @@ suite("cleanRestartPackagerCommand", function () { assert.strictEqual(restartStub.calledWithExactly(9090), true); }); + test("should warn when terminating a Metro process fails on Unix", async function () { + const execStub = Sinon.stub(); + execStub.withArgs("lsof -ti:9090").returns(createExecResult("23456\n")); + execStub + .withArgs("kill -9 23456") + .returns(createRejectedExecResult(new Error("operation not permitted"))); + execStub.withArgs("watchman watch-del-all").returns(createExecResult("")); + const restartStub = Sinon.stub().returns(Promise.resolve()); + const { CleanRestartPackager, logger } = createCommandModule( + HostPlatformId.LINUX, + execStub, + ); + + await runCommand(CleanRestartPackager, tempDir, restartStub); + + assert.strictEqual( + logger.warning.calledWith( + "Failed to terminate Metro process 23456: Error: operation not permitted", + ), + true, + ); + assert.strictEqual(logger.info.calledWith("No Metro process found on port 9090"), false); + assert.strictEqual(restartStub.calledOnce, true); + }); + test("should remove Metro cache recursively before restarting packager", async function () { const metroCachePath = path.join(tempDir, "node_modules", ".cache", "metro"); const nestedCachePath = path.join(metroCachePath, "nested"); From 8289dacf860d4c027f4131eb4e328b513da9b1a7 Mon Sep 17 00:00:00 2001 From: EmmaYuan1015 Date: Sun, 20 Sep 2026 15:21:35 +0800 Subject: [PATCH 2/4] Preserve Expo Doctor output on failure --- src/extension/commands/expoDoctor.ts | 23 ++++++++-- test/extension/commands/expoDoctor.test.ts | 50 ++++++++++++++-------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/extension/commands/expoDoctor.ts b/src/extension/commands/expoDoctor.ts index b78e951b8..df119b9b2 100644 --- a/src/extension/commands/expoDoctor.ts +++ b/src/extension/commands/expoDoctor.ts @@ -17,9 +17,26 @@ export class expoDoctor extends ReactNativeCommand { async baseFn(): Promise { assert(this.project); const projectRootPath = this.project.getPackager().getProjectPath(); - const res = await new ChildProcess().exec("npx expo-doctor", { cwd: projectRootPath }); + const result = new ChildProcess().spawn("npx", ["expo-doctor"], { + cwd: projectRootPath, + }); + const stdoutChunks: string[] = []; + const stderrChunks: string[] = []; + result.stdout.on("data", data => stdoutChunks.push(data.toString())); + result.stderr.on("data", data => stderrChunks.push(data.toString())); + logger.info("Running diagnostics..."); - const outcome = await res.outcome; - logger.info(outcome); + try { + await result.outcome; + } finally { + const stdout = stdoutChunks.join(""); + const stderr = stderrChunks.join(""); + if (stdout) { + logger.info(stdout); + } + if (stderr) { + logger.error(stderr); + } + } } } diff --git a/test/extension/commands/expoDoctor.test.ts b/test/extension/commands/expoDoctor.test.ts index e3b794eb3..9163dd3e7 100644 --- a/test/extension/commands/expoDoctor.test.ts +++ b/test/extension/commands/expoDoctor.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. import assert = require("assert"); +import { EventEmitter } from "events"; import Sinon = require("sinon"); import proxyquire = require("proxyquire"); @@ -9,7 +10,7 @@ suite("expoDoctorCommand", function () { const projectRootPath = "test-project"; function createCommandModule( - execStub: Sinon.SinonStub, + spawnStub: Sinon.SinonStub, logger = { info: Sinon.stub(), error: Sinon.stub(), @@ -22,7 +23,7 @@ suite("expoDoctorCommand", function () { } class FakeChildProcess { - public exec = execStub; + public spawn = spawnStub; } const module = proxyquire.noCallThru()("../../../src/extension/commands/expoDoctor", { @@ -55,11 +56,22 @@ suite("expoDoctorCommand", function () { }; } - function createExecResult(outcome: Promise): any { - return Promise.resolve({ - process: {}, - outcome, + function createSpawnResult(stdout: string, stderr: string, error?: Error): any { + const stdoutStream = new EventEmitter(); + const stderrStream = new EventEmitter(); + const outcome = new Promise((resolve, reject) => { + setImmediate(() => { + stdoutStream.emit("data", Buffer.from(stdout)); + stderrStream.emit("data", Buffer.from(stderr)); + error ? reject(error) : resolve(); + }); }); + + return { + stdout: stdoutStream, + stderr: stderrStream, + outcome, + }; } async function runCommand( @@ -76,13 +88,13 @@ suite("expoDoctorCommand", function () { } test("should run expo doctor from the project root and log the outcome", async function () { - const execStub = Sinon.stub().returns(createExecResult(Promise.resolve("No issues found"))); - const { expoDoctor, logger } = createCommandModule(execStub); + const spawnStub = Sinon.stub().returns(createSpawnResult("No issues found", "")); + const { expoDoctor, logger } = createCommandModule(spawnStub); await runCommand(expoDoctor); assert.strictEqual( - execStub.calledWithExactly("npx expo-doctor", { cwd: projectRootPath }), + spawnStub.calledWithExactly("npx", ["expo-doctor"], { cwd: projectRootPath }), true, ); assert.strictEqual(logger.info.calledWithExactly("Running diagnostics..."), true); @@ -91,28 +103,32 @@ suite("expoDoctorCommand", function () { test("should propagate an error when starting expo doctor fails", async function () { const error = new Error("could not start expo doctor"); - const execStub = Sinon.stub().returns(Promise.reject(error)); - const { expoDoctor, logger } = createCommandModule(execStub); + const spawnStub = Sinon.stub().throws(error); + const { expoDoctor, logger } = createCommandModule(spawnStub); await assert.rejects(() => runCommand(expoDoctor), error); assert.strictEqual(logger.info.called, false); }); - test("should propagate an error when expo doctor exits unsuccessfully", async function () { + test("should log output and propagate an error when expo doctor exits unsuccessfully", async function () { const error = new Error("expo doctor failed"); - const execStub = Sinon.stub().returns(createExecResult(Promise.reject(error))); - const { expoDoctor, logger } = createCommandModule(execStub); + const spawnStub = Sinon.stub().returns( + createSpawnResult("Issues found", "Dependency mismatch", error), + ); + const { expoDoctor, logger } = createCommandModule(spawnStub); await assert.rejects(() => runCommand(expoDoctor), error); assert.strictEqual(logger.info.calledWithExactly("Running diagnostics..."), true); + assert.strictEqual(logger.info.calledWithExactly("Issues found"), true); + assert.strictEqual(logger.error.calledWithExactly("Dependency mismatch"), true); }); test("should require a project before running expo doctor", async function () { - const execStub = Sinon.stub().returns(createExecResult(Promise.resolve(""))); - const { expoDoctor } = createCommandModule(execStub); + const spawnStub = Sinon.stub().returns(createSpawnResult("", "")); + const { expoDoctor } = createCommandModule(spawnStub); const command = expoDoctor.formInstance(); await assert.rejects(() => command.baseFn(), assert.AssertionError); - assert.strictEqual(execStub.called, false); + assert.strictEqual(spawnStub.called, false); }); }); From 5cdb85fe92b249d80401d39990dc17bf37e80534 Mon Sep 17 00:00:00 2001 From: Zhi yan Jia Date: Tue, 20 Oct 2026 17:07:37 +0800 Subject: [PATCH 3/4] Fix Hermes target response reading under Node Inspector Collect HTTP response bytes and decode them as UTF-8 after the response ends instead of using setEncoding(). This avoids a compatibility issue with Inspector network inspection during Hermes target discovery. Add regression coverage for response reading and split UTF-8 characters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cdp-proxy/debuggerEndpointHelper.ts | 11 ++++--- test/cdp-proxy/debuggerEndpointHelper.test.ts | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/cdp-proxy/debuggerEndpointHelper.ts b/src/cdp-proxy/debuggerEndpointHelper.ts index 397ac22c8..1ea8e9767 100644 --- a/src/cdp-proxy/debuggerEndpointHelper.ts +++ b/src/cdp-proxy/debuggerEndpointHelper.ts @@ -186,12 +186,13 @@ export class DebuggerEndpointHelper { } const request = driver.get(url, requestOptions, response => { - let data = ""; - response.setEncoding("utf8"); - response.on("data", (chunk: string) => { - data += chunk; + const chunks: Uint8Array[] = []; + response.on("data", (chunk: Buffer) => { + chunks.push(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)); + }); + response.on("end", () => { + fulfill(Buffer.concat(chunks).toString("utf8")); }); - response.on("end", () => fulfill(data)); response.on("error", reject); }); diff --git a/test/cdp-proxy/debuggerEndpointHelper.test.ts b/test/cdp-proxy/debuggerEndpointHelper.test.ts index 98fe082a6..3e9c456ef 100644 --- a/test/cdp-proxy/debuggerEndpointHelper.test.ts +++ b/test/cdp-proxy/debuggerEndpointHelper.test.ts @@ -3,6 +3,8 @@ import assert = require("assert"); import * as sinon from "sinon"; +import * as http from "http"; +import { AddressInfo } from "net"; import { DebuggerEndpointHelper } from "../../src/cdp-proxy/debuggerEndpointHelper"; import { PromiseUtil } from "../../src/common/node/promise"; @@ -15,6 +17,37 @@ suite("debuggerEndpointHelper", function () { } }); + test("getWSEndpoint should preserve Buffer responses and decode split UTF-8 characters", async function () { + const endpoint = "ws://localhost:8081/\u4e2d\u6587"; + const body = Buffer.from(JSON.stringify({ webSocketDebuggerUrl: endpoint }), "utf8"); + const splitIndex = body.indexOf("\u4e2d") + 1; + const setEncodingSpy = sinon.spy(http.IncomingMessage.prototype, "setEncoding"); + const server = http.createServer((_request, response) => { + response.setHeader("Content-Type", "application/json; charset=utf-8"); + response.write(body.subarray(0, splitIndex)); + setImmediate(() => response.end(body.subarray(splitIndex))); + }); + + try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address() as AddressInfo; + const helper = new DebuggerEndpointHelper(); + + const result = await helper.getWSEndpoint(`http://127.0.0.1:${address.port}`); + + assert.strictEqual(result, endpoint); + assert.strictEqual(setEncodingSpy.called, false); + } finally { + setEncodingSpy.restore(); + await new Promise((resolve, reject) => { + server.close(error => (error ? reject(error) : resolve())); + }); + } + }); + test("getWSEndpoint should return /json/version webSocketDebuggerUrl when present", async function () { const helper = new DebuggerEndpointHelper(); const fetchJsonStub = sinon.stub(helper as any, "fetchJson"); From efc9e195f6252a12026bd3ca3430d14e286f723c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:29:00 +0000 Subject: [PATCH 4/4] Bump markdown-it from 14.3.2 to 14.3.3 Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.3.2 to 14.3.3. - [Changelog](https://github.com/markdown-it/markdown-it/blob/14.3.3/CHANGELOG.md) - [Commits](https://github.com/markdown-it/markdown-it/compare/14.3.2...14.3.3) --- updated-dependencies: - dependency-name: markdown-it dependency-version: 14.3.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba9c32ed2..83f4cb2e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8928,9 +8928,9 @@ "dev": true }, "node_modules/markdown-it": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", - "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", + "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", "dev": true, "funding": [ { @@ -20222,9 +20222,9 @@ "dev": true }, "markdown-it": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", - "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", + "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", "dev": true, "requires": { "argparse": "^2.0.1",