From 04af7621428b1f9e2c6674906fe73a0712da9a42 Mon Sep 17 00:00:00 2001
From: SurviveM <254925152+SurviveM@users.noreply.github.com>
Date: Mon, 6 Apr 2026 08:20:14 +0800
Subject: [PATCH 1/3] fix: release default web run port and align codex
defaults
---
cli.js | 127 ++++++++++++++-
tests/e2e/test-config.js | 4 +
tests/unit/web-run-host.test.mjs | 148 ++++++++++++++++++
tests/unit/web-ui-behavior-parity.test.mjs | 2 +-
web-ui/app.js | 2 +-
web-ui/modules/app.methods.startup-claude.mjs | 2 +-
web-ui/partials/index/panel-config-codex.html | 4 +-
7 files changed, 283 insertions(+), 6 deletions(-)
diff --git a/cli.js b/cli.js
index 039e7a1..b6bcfbe 100644
--- a/cli.js
+++ b/cli.js
@@ -223,6 +223,131 @@ function resolveWebPort() {
return parsed;
}
+// #region releaseRunPortIfNeeded
+function releaseRunPortIfNeeded(port, deps = {}) {
+ const numericPort = parseInt(String(port), 10);
+ if (numericPort !== DEFAULT_WEB_PORT) {
+ return { attempted: false, released: false, pids: [], reason: 'non-default-port' };
+ }
+
+ const processRef = deps.process || process;
+ const runSpawnSync = deps.spawnSync || spawnSync;
+ const logger = deps.logger || console;
+ const killProcess = typeof deps.kill === 'function'
+ ? deps.kill
+ : (typeof processRef.kill === 'function' ? processRef.kill.bind(processRef) : null);
+ const seenPids = new Set();
+ const currentPid = Number(processRef.pid);
+ let released = false;
+
+ const addPidsFromText = (text) => {
+ const lines = String(text || '').split(/\r?\n/);
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!/^\d+$/.test(trimmed)) {
+ continue;
+ }
+ seenPids.add(Number(trimmed));
+ }
+ };
+
+ const runCommand = (command, args) => {
+ const result = runSpawnSync(command, args, { encoding: 'utf8' });
+ if (result && result.stdout) addPidsFromText(result.stdout);
+ if (result && result.stderr) addPidsFromText(result.stderr);
+ return result || {};
+ };
+
+ const addManagedRunPidsFromPs = (text) => {
+ const lines = String(text || '').split(/\r?\n/);
+ for (const line of lines) {
+ const normalizedLine = ` ${line.replace(/\s+/g, ' ').trim()} `;
+ if (!/(^|[\/\s])codexmate run(\s|$)/.test(normalizedLine) && !/(^|[\/\s])cli\.js run(\s|$)/.test(normalizedLine)) {
+ continue;
+ }
+ const pidMatch = line.match(/^\S+\s+(\d+)\s+/);
+ if (!pidMatch) {
+ continue;
+ }
+ const pid = Number(pidMatch[1]);
+ if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) {
+ continue;
+ }
+ seenPids.add(pid);
+ }
+ };
+
+ if (processRef.platform === 'win32') {
+ const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp']);
+ if (!(netstatResult && netstatResult.error)) {
+ const lines = String(netstatResult.stdout || '').split(/\r?\n/);
+ for (const line of lines) {
+ const parts = line.trim().split(/\s+/);
+ if (parts.length < 5) {
+ continue;
+ }
+ const localAddress = parts[1];
+ const state = parts[3];
+ const pidText = parts[4];
+ if (state !== 'LISTENING' || !localAddress.endsWith(`:${numericPort}`) || !/^\d+$/.test(pidText)) {
+ continue;
+ }
+ seenPids.add(Number(pidText));
+ }
+ for (const pid of seenPids) {
+ const taskkillResult = runCommand('taskkill', ['/PID', String(pid), '/F']);
+ if (!taskkillResult.error && taskkillResult.status === 0) {
+ released = true;
+ }
+ }
+ }
+ } else {
+ const fuserResult = runCommand('fuser', ['-k', `${numericPort}/tcp`]);
+ if (!fuserResult.error && fuserResult.status === 0) {
+ released = true;
+ }
+ const fuserMissing = !!(fuserResult && fuserResult.error && fuserResult.error.code === 'ENOENT');
+ if ((fuserMissing || !released || seenPids.size === 0) && killProcess) {
+ const lsofResult = runCommand('lsof', ['-ti', `tcp:${numericPort}`]);
+ if (!(lsofResult && lsofResult.error)) {
+ // noop: lsof pid collection is handled through stdout parsing above.
+ }
+ }
+ }
+
+ if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size === 0) {
+ const psResult = runCommand('ps', ['-ef']);
+ if (!(psResult && psResult.error)) {
+ addManagedRunPidsFromPs(psResult.stdout);
+ }
+ }
+
+ if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size > 0) {
+ for (const pid of seenPids) {
+ if (pid === currentPid) {
+ continue;
+ }
+ try {
+ killProcess(pid, 'SIGKILL');
+ released = true;
+ } catch (_) {}
+ }
+ }
+
+ if (released) {
+ logger.log(`~ 已释放端口 ${numericPort} 占用`);
+ }
+
+ return {
+ attempted: true,
+ released,
+ pids: Array.from(seenPids)
+ .filter((pid) => pid !== currentPid)
+ .sort((a, b) => a - b)
+ };
+}
+// #endregion releaseRunPortIfNeeded
+
function resolveWebHost(options = {}) {
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
if (optionHost) {
@@ -236,7 +361,6 @@ function resolveWebHost(options = {}) {
}
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
-model_reasoning_effort = "high"
model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
disable_response_storage = true
@@ -10845,6 +10969,7 @@ function cmdStart(options = {}) {
const port = resolveWebPort();
const host = resolveWebHost(options);
+ releaseRunPortIfNeeded(port);
let serverHandle = createWebServer({
htmlPath,
diff --git a/tests/e2e/test-config.js b/tests/e2e/test-config.js
index 7d399d9..9cedd54 100644
--- a/tests/e2e/test-config.js
+++ b/tests/e2e/test-config.js
@@ -408,6 +408,10 @@ preferred_auth_method = "shadow-key"
/^\s*model_auto_compact_token_limit\s*=\s*185000\s*$/m.test(legacyTemplateDefaults.template),
'legacy get-config-template should restore default model_auto_compact_token_limit'
);
+ assert(
+ !/^\s*model_reasoning_effort\s*=.+$/m.test(legacyTemplateDefaults.template),
+ 'legacy get-config-template should keep default medium reasoning without model_reasoning_effort'
+ );
const legacyAddDup = await legacyApi('add-provider', {
name: 'foo.bar',
url: 'https://dup.example.com/v1',
diff --git a/tests/unit/web-run-host.test.mjs b/tests/unit/web-run-host.test.mjs
index 892f87a..2462acd 100644
--- a/tests/unit/web-run-host.test.mjs
+++ b/tests/unit/web-run-host.test.mjs
@@ -118,6 +118,11 @@ const resolveWebHostSource = extractFunctionBySignature(
'function resolveWebHost(options = {}) {',
'resolveWebHost'
);
+const releaseRunPortIfNeededSource = extractFunctionBySignature(
+ cliContent,
+ 'function releaseRunPortIfNeeded(port, deps = {}) {',
+ 'releaseRunPortIfNeeded'
+);
const resolveWebHost = instantiateFunction(resolveWebHostSource, 'resolveWebHost', {
DEFAULT_WEB_HOST: defaultHostMatch[1],
process: { env: {} }
@@ -153,6 +158,149 @@ test('web auto-open uses IPv6 loopback when binding to IPv6 any address', () =>
);
});
+test('releaseRunPortIfNeeded skips non-default ports', () => {
+ const calls = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ return { status: 0, stdout: '', stderr: '' };
+ },
+ process: { platform: 'linux' },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3999);
+ assert.deepStrictEqual(result, {
+ attempted: false,
+ released: false,
+ pids: [],
+ reason: 'non-default-port'
+ });
+ assert.deepStrictEqual(calls, []);
+});
+
+test('releaseRunPortIfNeeded clears default port via fuser on linux', () => {
+ const calls = [];
+ const logs = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'fuser') {
+ return { status: 0, stdout: '1234\n', stderr: '' };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: { platform: 'linux' },
+ console: { log(message) { logs.push(message); } }
+ });
+
+ const result = releaseRunPortIfNeeded(3737);
+ assert.deepStrictEqual(calls, [['fuser', ['-k', '3737/tcp']]]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [1234]
+ });
+ assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
+});
+
+test('releaseRunPortIfNeeded falls back to lsof pids when fuser is unavailable', () => {
+ const calls = [];
+ const killed = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'fuser') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'lsof') {
+ return { status: 0, stdout: '2222\n3333\n', stderr: '' };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3737);
+ assert.deepStrictEqual(calls, [
+ ['fuser', ['-k', '3737/tcp']],
+ ['lsof', ['-ti', 'tcp:3737']]
+ ]);
+ assert.deepStrictEqual(killed, [
+ [2222, 'SIGKILL'],
+ [3333, 'SIGKILL']
+ ]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [2222, 3333]
+ });
+});
+
+test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', () => {
+ const calls = [];
+ const killed = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'fuser') {
+ return { error: { code: 'EACCES' }, status: 1, stdout: '', stderr: 'Permission denied' };
+ }
+ if (command === 'lsof') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'ps') {
+ return {
+ status: 0,
+ stdout: [
+ 'UID PID PPID C STIME TTY TIME CMD',
+ 'u0_a876 9001 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
+ 'u0_a876 9002 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run',
+ 'u0_a876 9100 1000 0 1970 ? 00:00:00 node /repo/cli.js config'
+ ].join('\n'),
+ stderr: ''
+ };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ pid: 9002,
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3737);
+ assert.deepStrictEqual(calls, [
+ ['fuser', ['-k', '3737/tcp']],
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['ps', ['-ef']]
+ ]);
+ assert.deepStrictEqual(killed, [[9001, 'SIGKILL']]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [9001]
+ });
+});
+
+test('cmdStart releases the resolved port before creating the web server', () => {
+ assert.match(cliContent, /const port = resolveWebPort\(\);\s*const host = resolveWebHost\(options\);\s*releaseRunPortIfNeeded\(port\);\s*let serverHandle = createWebServer\(/s);
+});
+
const getCodexSkillsDirSource = extractFunctionBySignature(
cliContent,
'function getCodexSkillsDir() {',
diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs
index 5182ed1..9c3b0c7 100644
--- a/tests/unit/web-ui-behavior-parity.test.mjs
+++ b/tests/unit/web-ui-behavior-parity.test.mjs
@@ -105,7 +105,7 @@ function createLoadAllContext() {
currentProvider: 'existing-provider',
currentModel: 'existing-model',
serviceTier: 'fast',
- modelReasoningEffort: 'high',
+ modelReasoningEffort: 'medium',
modelContextWindowInput: 'dirty-context',
modelAutoCompactTokenLimitInput: 'dirty-limit',
editingCodexBudgetField: 'modelContextWindowInput',
diff --git a/web-ui/app.js b/web-ui/app.js
index 4c4da4f..cc13d0e 100644
--- a/web-ui/app.js
+++ b/web-ui/app.js
@@ -33,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
currentProvider: '',
currentModel: '',
serviceTier: 'fast',
- modelReasoningEffort: 'high',
+ modelReasoningEffort: 'medium',
modelContextWindowInput: String(DEFAULT_MODEL_CONTEXT_WINDOW),
modelAutoCompactTokenLimitInput: String(DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT),
editingCodexBudgetField: '',
diff --git a/web-ui/modules/app.methods.startup-claude.mjs b/web-ui/modules/app.methods.startup-claude.mjs
index 1e3afa7..228be8e 100644
--- a/web-ui/modules/app.methods.startup-claude.mjs
+++ b/web-ui/modules/app.methods.startup-claude.mjs
@@ -40,7 +40,7 @@ export function createStartupClaudeMethods(options = {}) {
const effort = typeof statusRes.modelReasoningEffort === 'string'
? statusRes.modelReasoningEffort.trim().toLowerCase()
: '';
- this.modelReasoningEffort = effort || 'high';
+ this.modelReasoningEffort = effort || 'medium';
}
{
const contextWindow = this.normalizePositiveIntegerInput(
diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html
index 32f8516..0c971e7 100644
--- a/web-ui/partials/index/panel-config-codex.html
+++ b/web-ui/partials/index/panel-config-codex.html
@@ -78,8 +78,8 @@
推理强度
From b99e0121b83acd33066c3aec6d77e5a44ae181be Mon Sep 17 00:00:00 2001
From: SurviveM <254925152+SurviveM@users.noreply.github.com>
Date: Mon, 6 Apr 2026 09:18:12 +0800
Subject: [PATCH 2/3] fix: verify run port cleanup targets
---
cli.js | 61 +++++++++-----
tests/unit/provider-share-command.test.mjs | 81 +++++++++++++++++++
tests/unit/web-run-host.test.mjs | 72 +++++++++++++----
web-ui/modules/app.methods.startup-claude.mjs | 3 +-
4 files changed, 178 insertions(+), 39 deletions(-)
diff --git a/cli.js b/cli.js
index b6bcfbe..7d73061 100644
--- a/cli.js
+++ b/cli.js
@@ -237,28 +237,36 @@ function releaseRunPortIfNeeded(port, deps = {}) {
? deps.kill
: (typeof processRef.kill === 'function' ? processRef.kill.bind(processRef) : null);
const seenPids = new Set();
+ const candidatePids = new Set();
const currentPid = Number(processRef.pid);
let released = false;
- const addPidsFromText = (text) => {
+ const addPidsFromText = (text, targetSet = seenPids) => {
+ if (!targetSet) {
+ return;
+ }
const lines = String(text || '').split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!/^\d+$/.test(trimmed)) {
continue;
}
- seenPids.add(Number(trimmed));
+ targetSet.add(Number(trimmed));
}
};
- const runCommand = (command, args) => {
+ const runCommand = (command, args, options = {}) => {
+ const {
+ stdoutPidSet = seenPids,
+ stderrPidSet = seenPids
+ } = options;
const result = runSpawnSync(command, args, { encoding: 'utf8' });
- if (result && result.stdout) addPidsFromText(result.stdout);
- if (result && result.stderr) addPidsFromText(result.stderr);
+ if (result && result.stdout) addPidsFromText(result.stdout, stdoutPidSet);
+ if (result && result.stderr) addPidsFromText(result.stderr, stderrPidSet);
return result || {};
};
- const addManagedRunPidsFromPs = (text) => {
+ const addManagedRunPidsFromPs = (text, allowedPids = null) => {
const lines = String(text || '').split(/\r?\n/);
for (const line of lines) {
const normalizedLine = ` ${line.replace(/\s+/g, ' ').trim()} `;
@@ -273,6 +281,9 @@ function releaseRunPortIfNeeded(port, deps = {}) {
if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) {
continue;
}
+ if (allowedPids && !allowedPids.has(pid)) {
+ continue;
+ }
seenPids.add(pid);
}
};
@@ -302,23 +313,31 @@ function releaseRunPortIfNeeded(port, deps = {}) {
}
}
} else {
- const fuserResult = runCommand('fuser', ['-k', `${numericPort}/tcp`]);
- if (!fuserResult.error && fuserResult.status === 0) {
- released = true;
- }
- const fuserMissing = !!(fuserResult && fuserResult.error && fuserResult.error.code === 'ENOENT');
- if ((fuserMissing || !released || seenPids.size === 0) && killProcess) {
- const lsofResult = runCommand('lsof', ['-ti', `tcp:${numericPort}`]);
- if (!(lsofResult && lsofResult.error)) {
- // noop: lsof pid collection is handled through stdout parsing above.
+ let psResult = null;
+ const readPsResult = () => {
+ if (psResult) {
+ return psResult;
}
- }
- }
+ psResult = runCommand('ps', ['-ef'], { stdoutPidSet: null, stderrPidSet: null });
+ return psResult;
+ };
- if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size === 0) {
- const psResult = runCommand('ps', ['-ef']);
- if (!(psResult && psResult.error)) {
- addManagedRunPidsFromPs(psResult.stdout);
+ const lsofResult = runCommand(
+ 'lsof',
+ ['-ti', `tcp:${numericPort}`],
+ { stdoutPidSet: candidatePids, stderrPidSet: null }
+ );
+ if (!(lsofResult && lsofResult.error) && candidatePids.size > 0) {
+ const managedPsResult = readPsResult();
+ if (!(managedPsResult && managedPsResult.error)) {
+ addManagedRunPidsFromPs(managedPsResult.stdout, candidatePids);
+ }
+ }
+ if (killProcess && seenPids.size === 0) {
+ const managedPsResult = readPsResult();
+ if (!(managedPsResult && managedPsResult.error)) {
+ addManagedRunPidsFromPs(managedPsResult.stdout);
+ }
}
}
diff --git a/tests/unit/provider-share-command.test.mjs b/tests/unit/provider-share-command.test.mjs
index c9a1e89..94f441d 100644
--- a/tests/unit/provider-share-command.test.mjs
+++ b/tests/unit/provider-share-command.test.mjs
@@ -757,6 +757,87 @@ test('loadAll can refresh in background without flipping the global loading stat
assert.strictEqual(context.initError, '');
});
+test('loadAll falls back to medium for unsupported reasoning effort values while preserving xhigh', async () => {
+ const loadAllSource = extractBlockBySignature(
+ appSource,
+ 'async loadAll(options = {}) {'
+ ).replace(/^async loadAll/, 'async function loadAll');
+ const responses = [
+ {
+ provider: 'alpha',
+ model: 'alpha-model',
+ serviceTier: 'fast',
+ modelReasoningEffort: 'bogus',
+ modelContextWindow: 200000,
+ modelAutoCompactTokenLimit: 180000,
+ configReady: true,
+ initNotice: ''
+ },
+ {
+ provider: 'alpha',
+ model: 'alpha-model',
+ serviceTier: 'fast',
+ modelReasoningEffort: 'xhigh',
+ modelContextWindow: 200000,
+ modelAutoCompactTokenLimit: 180000,
+ configReady: true,
+ initNotice: ''
+ }
+ ];
+ let statusIndex = 0;
+ const loadAll = instantiateFunction(loadAllSource, 'loadAll', {
+ defaultModelContextWindow: 190000,
+ defaultModelAutoCompactTokenLimit: 185000,
+ api: async (action) => {
+ if (action === 'status') {
+ return responses[statusIndex++] || responses[responses.length - 1];
+ }
+ if (action === 'list') {
+ return {
+ providers: [{ name: 'alpha', url: 'https://api.example.com/v1', hasKey: true }]
+ };
+ }
+ throw new Error(`Unexpected api action: ${action}`);
+ }
+ });
+
+ const createContext = () => ({
+ loading: false,
+ initError: '',
+ currentProvider: 'stale-provider',
+ currentModel: 'stale-model',
+ serviceTier: 'fast',
+ modelReasoningEffort: 'high',
+ modelContextWindowInput: '190000',
+ modelAutoCompactTokenLimitInput: '185000',
+ editingCodexBudgetField: '',
+ providersList: [],
+ normalizePositiveIntegerInput(value, label, fallback = '') {
+ const raw = value === undefined || value === null || value === ''
+ ? String(fallback || '')
+ : String(value);
+ const text = raw.trim();
+ const numeric = Number.parseInt(text, 10);
+ if (!Number.isFinite(numeric) || numeric <= 0) {
+ return { ok: false, error: `${label} invalid` };
+ }
+ return { ok: true, value: numeric, text: String(numeric) };
+ },
+ showMessage() {},
+ maybeShowStarPrompt() {},
+ async loadModelsForProvider() {},
+ async loadCodexAuthProfiles() {}
+ });
+
+ const invalidContext = createContext();
+ await loadAll.call(invalidContext);
+ assert.strictEqual(invalidContext.modelReasoningEffort, 'medium');
+
+ const xhighContext = createContext();
+ await loadAll.call(xhighContext);
+ assert.strictEqual(xhighContext.modelReasoningEffort, 'xhigh');
+});
+
test('loadAll treats provider list fetch failures as startup errors and skips model refresh', async () => {
const loadAllSource = extractBlockBySignature(
appSource,
diff --git a/tests/unit/web-run-host.test.mjs b/tests/unit/web-run-host.test.mjs
index 2462acd..d9d0fc6 100644
--- a/tests/unit/web-run-host.test.mjs
+++ b/tests/unit/web-run-host.test.mjs
@@ -118,6 +118,11 @@ const resolveWebHostSource = extractFunctionBySignature(
'function resolveWebHost(options = {}) {',
'resolveWebHost'
);
+const cmdStartSource = extractFunctionBySignature(
+ cliContent,
+ 'function cmdStart(options = {}) {',
+ 'cmdStart'
+);
const releaseRunPortIfNeededSource = extractFunctionBySignature(
cliContent,
'function releaseRunPortIfNeeded(port, deps = {}) {',
@@ -180,24 +185,45 @@ test('releaseRunPortIfNeeded skips non-default ports', () => {
assert.deepStrictEqual(calls, []);
});
-test('releaseRunPortIfNeeded clears default port via fuser on linux', () => {
+test('releaseRunPortIfNeeded clears default port only after lsof pids map to managed run processes', () => {
const calls = [];
+ const killed = [];
const logs = [];
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
DEFAULT_WEB_PORT: 3737,
spawnSync(command, args) {
calls.push([command, args]);
- if (command === 'fuser') {
- return { status: 0, stdout: '1234\n', stderr: '' };
+ if (command === 'lsof') {
+ return { status: 0, stdout: '1234\n8888\n', stderr: '' };
+ }
+ if (command === 'ps') {
+ return {
+ status: 0,
+ stdout: [
+ 'UID PID PPID C STIME TTY TIME CMD',
+ 'u0_a876 1234 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
+ 'u0_a876 8888 1000 0 1970 ? 00:00:00 node /repo/other-server.js'
+ ].join('\n'),
+ stderr: ''
+ };
}
throw new Error(`unexpected command: ${command}`);
},
- process: { platform: 'linux' },
+ process: {
+ platform: 'linux',
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
console: { log(message) { logs.push(message); } }
});
const result = releaseRunPortIfNeeded(3737);
- assert.deepStrictEqual(calls, [['fuser', ['-k', '3737/tcp']]]);
+ assert.deepStrictEqual(calls, [
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['ps', ['-ef']]
+ ]);
+ assert.deepStrictEqual(killed, [[1234, 'SIGKILL']]);
assert.deepStrictEqual(result, {
attempted: true,
released: true,
@@ -206,18 +232,26 @@ test('releaseRunPortIfNeeded clears default port via fuser on linux', () => {
assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
});
-test('releaseRunPortIfNeeded falls back to lsof pids when fuser is unavailable', () => {
+test('releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable', () => {
const calls = [];
const killed = [];
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
DEFAULT_WEB_PORT: 3737,
spawnSync(command, args) {
calls.push([command, args]);
- if (command === 'fuser') {
+ if (command === 'lsof') {
return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
}
- if (command === 'lsof') {
- return { status: 0, stdout: '2222\n3333\n', stderr: '' };
+ if (command === 'ps') {
+ return {
+ status: 0,
+ stdout: [
+ 'UID PID PPID C STIME TTY TIME CMD',
+ 'u0_a876 2222 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
+ 'u0_a876 3333 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run'
+ ].join('\n'),
+ stderr: ''
+ };
}
throw new Error(`unexpected command: ${command}`);
},
@@ -232,8 +266,8 @@ test('releaseRunPortIfNeeded falls back to lsof pids when fuser is unavailable',
const result = releaseRunPortIfNeeded(3737);
assert.deepStrictEqual(calls, [
- ['fuser', ['-k', '3737/tcp']],
- ['lsof', ['-ti', 'tcp:3737']]
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['ps', ['-ef']]
]);
assert.deepStrictEqual(killed, [
[2222, 'SIGKILL'],
@@ -253,11 +287,8 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
DEFAULT_WEB_PORT: 3737,
spawnSync(command, args) {
calls.push([command, args]);
- if (command === 'fuser') {
- return { error: { code: 'EACCES' }, status: 1, stdout: '', stderr: 'Permission denied' };
- }
if (command === 'lsof') {
- return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ return { status: 0, stdout: '9001\n9002\n', stderr: '' };
}
if (command === 'ps') {
return {
@@ -285,7 +316,6 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
const result = releaseRunPortIfNeeded(3737);
assert.deepStrictEqual(calls, [
- ['fuser', ['-k', '3737/tcp']],
['lsof', ['-ti', 'tcp:3737']],
['ps', ['-ef']]
]);
@@ -298,7 +328,15 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
});
test('cmdStart releases the resolved port before creating the web server', () => {
- assert.match(cliContent, /const port = resolveWebPort\(\);\s*const host = resolveWebHost\(options\);\s*releaseRunPortIfNeeded\(port\);\s*let serverHandle = createWebServer\(/s);
+ const resolveIndex = cmdStartSource.indexOf('resolveWebPort(');
+ const releaseIndex = cmdStartSource.indexOf('releaseRunPortIfNeeded(');
+ const createIndex = cmdStartSource.indexOf('createWebServer(');
+
+ assert(resolveIndex >= 0, 'cmdStart should resolve the web port');
+ assert(releaseIndex >= 0, 'cmdStart should release the run port before startup');
+ assert(createIndex >= 0, 'cmdStart should create the web server');
+ assert(resolveIndex < releaseIndex, 'cmdStart should resolve the port before releasing it');
+ assert(releaseIndex < createIndex, 'cmdStart should release the port before creating the web server');
});
const getCodexSkillsDirSource = extractFunctionBySignature(
diff --git a/web-ui/modules/app.methods.startup-claude.mjs b/web-ui/modules/app.methods.startup-claude.mjs
index 228be8e..703b8df 100644
--- a/web-ui/modules/app.methods.startup-claude.mjs
+++ b/web-ui/modules/app.methods.startup-claude.mjs
@@ -40,7 +40,8 @@ export function createStartupClaudeMethods(options = {}) {
const effort = typeof statusRes.modelReasoningEffort === 'string'
? statusRes.modelReasoningEffort.trim().toLowerCase()
: '';
- this.modelReasoningEffort = effort || 'medium';
+ const allowedReasoningEfforts = new Set(['low', 'medium', 'high', 'xhigh']);
+ this.modelReasoningEffort = allowedReasoningEfforts.has(effort) ? effort : 'medium';
}
{
const contextWindow = this.normalizePositiveIntegerInput(
From eb3ed908efeb5246cfbd1e3f468da057970fa6f8 Mon Sep 17 00:00:00 2001
From: SurviveM <254925152+SurviveM@users.noreply.github.com>
Date: Mon, 6 Apr 2026 09:36:22 +0800
Subject: [PATCH 3/3] fix: scope run port cleanup to verified listeners
---
cli.js | 123 ++++++++++++++++++++++++++-----
tests/unit/web-run-host.test.mjs | 111 ++++++++++++++++++++++++++--
2 files changed, 209 insertions(+), 25 deletions(-)
diff --git a/cli.js b/cli.js
index 7d73061..c3601a7 100644
--- a/cli.js
+++ b/cli.js
@@ -224,7 +224,7 @@ function resolveWebPort() {
}
// #region releaseRunPortIfNeeded
-function releaseRunPortIfNeeded(port, deps = {}) {
+function releaseRunPortIfNeeded(port, host, deps = {}) {
const numericPort = parseInt(String(port), 10);
if (numericPort !== DEFAULT_WEB_PORT) {
return { attempted: false, released: false, pids: [], reason: 'non-default-port' };
@@ -239,7 +239,58 @@ function releaseRunPortIfNeeded(port, deps = {}) {
const seenPids = new Set();
const candidatePids = new Set();
const currentPid = Number(processRef.pid);
+ const normalizedHost = typeof host === 'string' ? host.trim().toLowerCase() : '';
let released = false;
+ const windowsCommandLineCache = new Map();
+
+ const isManagedRunCommand = (commandLine) => {
+ const normalizedLine = ` ${String(commandLine || '').replace(/\s+/g, ' ').trim()} `;
+ return /(^|[\/\\\s])codexmate(?:\.cmd|\.exe)? run(\s|$)/i.test(normalizedLine)
+ || /(^|[\/\\\s])cli\.js run(\s|$)/i.test(normalizedLine);
+ };
+
+ const normalizeListenerHost = (value) => {
+ const trimmed = String(value || '').trim().toLowerCase();
+ if (!trimmed) {
+ return '';
+ }
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
+ return trimmed.slice(1, -1);
+ }
+ return trimmed.startsWith('::ffff:') ? trimmed.slice('::ffff:'.length) : trimmed;
+ };
+
+ const extractListenerHost = (localAddress) => {
+ const trimmed = String(localAddress || '').trim();
+ if (!trimmed) {
+ return '';
+ }
+ if (trimmed.startsWith('[')) {
+ const closingBracket = trimmed.indexOf(']');
+ if (closingBracket > 0) {
+ return normalizeListenerHost(trimmed.slice(1, closingBracket));
+ }
+ }
+ const lastColon = trimmed.lastIndexOf(':');
+ if (lastColon <= 0) {
+ return normalizeListenerHost(trimmed);
+ }
+ return normalizeListenerHost(trimmed.slice(0, lastColon));
+ };
+
+ const isMatchingWindowsListenerAddress = (localAddress) => {
+ const listenerHost = extractListenerHost(localAddress);
+ if (!listenerHost || !normalizedHost) {
+ return false;
+ }
+ if (normalizedHost === 'localhost') {
+ return listenerHost === '127.0.0.1' || listenerHost === '::1';
+ }
+ if (normalizedHost === '0.0.0.0' || normalizedHost === '::') {
+ return listenerHost === normalizedHost;
+ }
+ return listenerHost === normalizeListenerHost(normalizedHost);
+ };
const addPidsFromText = (text, targetSet = seenPids) => {
if (!targetSet) {
@@ -247,11 +298,13 @@ function releaseRunPortIfNeeded(port, deps = {}) {
}
const lines = String(text || '').split(/\r?\n/);
for (const line of lines) {
- const trimmed = line.trim();
- if (!/^\d+$/.test(trimmed)) {
- continue;
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
+ for (const token of tokens) {
+ if (!/^\d+$/.test(token)) {
+ continue;
+ }
+ targetSet.add(Number(token));
}
- targetSet.add(Number(trimmed));
}
};
@@ -288,8 +341,28 @@ function releaseRunPortIfNeeded(port, deps = {}) {
}
};
+ const getWindowsProcessCommandLine = (pid) => {
+ if (windowsCommandLineCache.has(pid)) {
+ return windowsCommandLineCache.get(pid);
+ }
+ const result = runCommand(
+ 'powershell',
+ [
+ '-NoProfile',
+ '-Command',
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($p) { $p.CommandLine }`
+ ],
+ { stdoutPidSet: null, stderrPidSet: null }
+ );
+ const commandLine = !result.error && result.status === 0
+ ? String(result.stdout || '').trim()
+ : '';
+ windowsCommandLineCache.set(pid, commandLine);
+ return commandLine;
+ };
+
if (processRef.platform === 'win32') {
- const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp']);
+ const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp'], { stdoutPidSet: null, stderrPidSet: null });
if (!(netstatResult && netstatResult.error)) {
const lines = String(netstatResult.stdout || '').split(/\r?\n/);
for (const line of lines) {
@@ -303,10 +376,24 @@ function releaseRunPortIfNeeded(port, deps = {}) {
if (state !== 'LISTENING' || !localAddress.endsWith(`:${numericPort}`) || !/^\d+$/.test(pidText)) {
continue;
}
- seenPids.add(Number(pidText));
+ if (!isMatchingWindowsListenerAddress(localAddress)) {
+ continue;
+ }
+ candidatePids.add(Number(pidText));
}
- for (const pid of seenPids) {
- const taskkillResult = runCommand('taskkill', ['/PID', String(pid), '/F']);
+ for (const pid of candidatePids) {
+ if (pid === currentPid) {
+ continue;
+ }
+ if (!isManagedRunCommand(getWindowsProcessCommandLine(pid))) {
+ continue;
+ }
+ seenPids.add(pid);
+ const taskkillResult = runCommand(
+ 'taskkill',
+ ['/PID', String(pid), '/F'],
+ { stdoutPidSet: null, stderrPidSet: null }
+ );
if (!taskkillResult.error && taskkillResult.status === 0) {
released = true;
}
@@ -327,18 +414,20 @@ function releaseRunPortIfNeeded(port, deps = {}) {
['-ti', `tcp:${numericPort}`],
{ stdoutPidSet: candidatePids, stderrPidSet: null }
);
- if (!(lsofResult && lsofResult.error) && candidatePids.size > 0) {
+ const shouldTryFuser = !!(lsofResult && lsofResult.error && lsofResult.error.code === 'ENOENT');
+ if (shouldTryFuser && candidatePids.size === 0) {
+ runCommand(
+ 'fuser',
+ [`${numericPort}/tcp`],
+ { stdoutPidSet: candidatePids, stderrPidSet: candidatePids }
+ );
+ }
+ if (candidatePids.size > 0) {
const managedPsResult = readPsResult();
if (!(managedPsResult && managedPsResult.error)) {
addManagedRunPidsFromPs(managedPsResult.stdout, candidatePids);
}
}
- if (killProcess && seenPids.size === 0) {
- const managedPsResult = readPsResult();
- if (!(managedPsResult && managedPsResult.error)) {
- addManagedRunPidsFromPs(managedPsResult.stdout);
- }
- }
}
if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size > 0) {
@@ -10988,7 +11077,7 @@ function cmdStart(options = {}) {
const port = resolveWebPort();
const host = resolveWebHost(options);
- releaseRunPortIfNeeded(port);
+ releaseRunPortIfNeeded(port, host);
let serverHandle = createWebServer({
htmlPath,
diff --git a/tests/unit/web-run-host.test.mjs b/tests/unit/web-run-host.test.mjs
index d9d0fc6..d3e4d55 100644
--- a/tests/unit/web-run-host.test.mjs
+++ b/tests/unit/web-run-host.test.mjs
@@ -125,7 +125,7 @@ const cmdStartSource = extractFunctionBySignature(
);
const releaseRunPortIfNeededSource = extractFunctionBySignature(
cliContent,
- 'function releaseRunPortIfNeeded(port, deps = {}) {',
+ 'function releaseRunPortIfNeeded(port, host, deps = {}) {',
'releaseRunPortIfNeeded'
);
const resolveWebHost = instantiateFunction(resolveWebHostSource, 'resolveWebHost', {
@@ -175,7 +175,7 @@ test('releaseRunPortIfNeeded skips non-default ports', () => {
console: { log() {} }
});
- const result = releaseRunPortIfNeeded(3999);
+ const result = releaseRunPortIfNeeded(3999, '0.0.0.0');
assert.deepStrictEqual(result, {
attempted: false,
released: false,
@@ -218,7 +218,7 @@ test('releaseRunPortIfNeeded clears default port only after lsof pids map to man
console: { log(message) { logs.push(message); } }
});
- const result = releaseRunPortIfNeeded(3737);
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
assert.deepStrictEqual(calls, [
['lsof', ['-ti', 'tcp:3737']],
['ps', ['-ef']]
@@ -232,7 +232,7 @@ test('releaseRunPortIfNeeded clears default port only after lsof pids map to man
assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
});
-test('releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable', () => {
+test('releaseRunPortIfNeeded falls back to non-destructive fuser pids when lsof is unavailable', () => {
const calls = [];
const killed = [];
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
@@ -242,6 +242,9 @@ test('releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable', ()
if (command === 'lsof') {
return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
}
+ if (command === 'fuser') {
+ return { status: 0, stdout: '', stderr: '2222 3333' };
+ }
if (command === 'ps') {
return {
status: 0,
@@ -264,9 +267,10 @@ test('releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable', ()
console: { log() {} }
});
- const result = releaseRunPortIfNeeded(3737);
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
assert.deepStrictEqual(calls, [
['lsof', ['-ti', 'tcp:3737']],
+ ['fuser', ['3737/tcp']],
['ps', ['-ef']]
]);
assert.deepStrictEqual(killed, [
@@ -314,7 +318,7 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
console: { log() {} }
});
- const result = releaseRunPortIfNeeded(3737);
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
assert.deepStrictEqual(calls, [
['lsof', ['-ti', 'tcp:3737']],
['ps', ['-ef']]
@@ -327,13 +331,104 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
});
});
+test('releaseRunPortIfNeeded skips ps-based kill when no port-scoped owner pids can be identified', () => {
+ const calls = [];
+ const killed = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'lsof') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'fuser') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'ps') {
+ throw new Error('ps should not run without port-scoped pid candidates');
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['fuser', ['3737/tcp']]
+ ]);
+ assert.deepStrictEqual(killed, []);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: false,
+ pids: []
+ });
+});
+
+test('releaseRunPortIfNeeded only taskkills Windows listeners that match host and managed command line', () => {
+ const calls = [];
+ const logs = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'netstat') {
+ return {
+ status: 0,
+ stdout: [
+ ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 1111',
+ ' TCP 127.0.0.1:3737 0.0.0.0:0 LISTENING 2222',
+ ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 3333'
+ ].join('\r\n'),
+ stderr: ''
+ };
+ }
+ if (command === 'powershell') {
+ if (String(args[2]).includes('1111')) {
+ return { status: 0, stdout: 'node C:\\repo\\cli.js run --no-browser\r\n', stderr: '' };
+ }
+ if (String(args[2]).includes('3333')) {
+ return { status: 0, stdout: 'node C:\\repo\\other-server.js\r\n', stderr: '' };
+ }
+ throw new Error(`unexpected powershell args: ${args.join(' ')}`);
+ }
+ if (command === 'taskkill') {
+ return { status: 0, stdout: '', stderr: '' };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: { platform: 'win32', pid: 9999 },
+ console: { log(message) { logs.push(message); } }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['netstat', ['-ano', '-p', 'tcp']],
+ ['powershell', ['-NoProfile', '-Command', '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 1111"; if ($p) { $p.CommandLine }']],
+ ['taskkill', ['/PID', '1111', '/F']],
+ ['powershell', ['-NoProfile', '-Command', '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 3333"; if ($p) { $p.CommandLine }']]
+ ]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [1111]
+ });
+ assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
+});
+
test('cmdStart releases the resolved port before creating the web server', () => {
const resolveIndex = cmdStartSource.indexOf('resolveWebPort(');
- const releaseIndex = cmdStartSource.indexOf('releaseRunPortIfNeeded(');
+ const releaseIndex = cmdStartSource.indexOf('releaseRunPortIfNeeded(port, host)');
const createIndex = cmdStartSource.indexOf('createWebServer(');
assert(resolveIndex >= 0, 'cmdStart should resolve the web port');
- assert(releaseIndex >= 0, 'cmdStart should release the run port before startup');
+ assert(releaseIndex >= 0, 'cmdStart should release the run port with host before startup');
assert(createIndex >= 0, 'cmdStart should create the web server');
assert(resolveIndex < releaseIndex, 'cmdStart should resolve the port before releasing it');
assert(releaseIndex < createIndex, 'cmdStart should release the port before creating the web server');