Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions src/cdp-proxy/debuggerEndpointHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,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);
});

Expand Down
62 changes: 38 additions & 24 deletions src/extension/commands/cleanRestartPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,46 +57,60 @@ 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}`);
// eslint-disable-next-line no-await-in-loop -- kill PIDs one at a time; taskkill /T kills process trees, so later PIDs may already be gone
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 {
// eslint-disable-next-line no-await-in-loop -- kill PIDs one at a time; taskkill /T kills process trees, so later PIDs may already be gone
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) {
Expand Down
4 changes: 2 additions & 2 deletions src/extension/commands/enableHermes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ export class EnableHermes extends Command {

const gradleFileContent = fs.readFileSync(gradleFilePath, "utf-8");
const hermesMatches = gradleFileContent.match(
/^([ \t]*)(#\s*)?hermesEnabled\s*=\s*\w*/m,
/^([\t ]*)(#\s*)?hermesEnabled\s*=\s*\w*/m,
);
if (hermesMatches && !hermesMatches[2]) {
const updatedHermes = gradleFileContent.replace(
/^([ \t]*)hermesEnabled\s*=\s*\w*/m,
/^([\t ]*)hermesEnabled\s*=\s*\w*/m,
`$1hermesEnabled=${isHermesEnabled}`,
);
await this.nodeFileSystem.writeFile(gradleFilePath, updatedHermes);
Expand Down
23 changes: 20 additions & 3 deletions src/extension/commands/expoDoctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,26 @@ export class expoDoctor extends ReactNativeCommand {
async baseFn(): Promise<void> {
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);
}
}
}
}
33 changes: 33 additions & 0 deletions test/cdp-proxy/debuggerEndpointHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<void>((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<void>((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");
Expand Down
52 changes: 52 additions & 0 deletions test/extension/commands/cleanRestartPackager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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");
Expand Down
50 changes: 33 additions & 17 deletions test/extension/commands/expoDoctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
// 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");

suite("expoDoctorCommand", function () {
const projectRootPath = "test-project";

function createCommandModule(
execStub: Sinon.SinonStub,
spawnStub: Sinon.SinonStub,
logger = {
info: Sinon.stub(),
error: Sinon.stub(),
Expand All @@ -22,7 +23,7 @@ suite("expoDoctorCommand", function () {
}

class FakeChildProcess {
public exec = execStub;
public spawn = spawnStub;
}

const module = proxyquire.noCallThru()("../../../src/extension/commands/expoDoctor", {
Expand Down Expand Up @@ -55,11 +56,22 @@ suite("expoDoctorCommand", function () {
};
}

function createExecResult(outcome: Promise<string>): 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<void>((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(
Expand All @@ -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);
Expand All @@ -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);
});
});
Loading