Skip to content

Commit 04af762

Browse files
committed
fix: release default web run port and align codex defaults
1 parent c45ced3 commit 04af762

7 files changed

Lines changed: 283 additions & 6 deletions

File tree

cli.js

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,131 @@ function resolveWebPort() {
223223
return parsed;
224224
}
225225

226+
// #region releaseRunPortIfNeeded
227+
function releaseRunPortIfNeeded(port, deps = {}) {
228+
const numericPort = parseInt(String(port), 10);
229+
if (numericPort !== DEFAULT_WEB_PORT) {
230+
return { attempted: false, released: false, pids: [], reason: 'non-default-port' };
231+
}
232+
233+
const processRef = deps.process || process;
234+
const runSpawnSync = deps.spawnSync || spawnSync;
235+
const logger = deps.logger || console;
236+
const killProcess = typeof deps.kill === 'function'
237+
? deps.kill
238+
: (typeof processRef.kill === 'function' ? processRef.kill.bind(processRef) : null);
239+
const seenPids = new Set();
240+
const currentPid = Number(processRef.pid);
241+
let released = false;
242+
243+
const addPidsFromText = (text) => {
244+
const lines = String(text || '').split(/\r?\n/);
245+
for (const line of lines) {
246+
const trimmed = line.trim();
247+
if (!/^\d+$/.test(trimmed)) {
248+
continue;
249+
}
250+
seenPids.add(Number(trimmed));
251+
}
252+
};
253+
254+
const runCommand = (command, args) => {
255+
const result = runSpawnSync(command, args, { encoding: 'utf8' });
256+
if (result && result.stdout) addPidsFromText(result.stdout);
257+
if (result && result.stderr) addPidsFromText(result.stderr);
258+
return result || {};
259+
};
260+
261+
const addManagedRunPidsFromPs = (text) => {
262+
const lines = String(text || '').split(/\r?\n/);
263+
for (const line of lines) {
264+
const normalizedLine = ` ${line.replace(/\s+/g, ' ').trim()} `;
265+
if (!/(^|[\/\s])codexmate run(\s|$)/.test(normalizedLine) && !/(^|[\/\s])cli\.js run(\s|$)/.test(normalizedLine)) {
266+
continue;
267+
}
268+
const pidMatch = line.match(/^\S+\s+(\d+)\s+/);
269+
if (!pidMatch) {
270+
continue;
271+
}
272+
const pid = Number(pidMatch[1]);
273+
if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) {
274+
continue;
275+
}
276+
seenPids.add(pid);
277+
}
278+
};
279+
280+
if (processRef.platform === 'win32') {
281+
const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp']);
282+
if (!(netstatResult && netstatResult.error)) {
283+
const lines = String(netstatResult.stdout || '').split(/\r?\n/);
284+
for (const line of lines) {
285+
const parts = line.trim().split(/\s+/);
286+
if (parts.length < 5) {
287+
continue;
288+
}
289+
const localAddress = parts[1];
290+
const state = parts[3];
291+
const pidText = parts[4];
292+
if (state !== 'LISTENING' || !localAddress.endsWith(`:${numericPort}`) || !/^\d+$/.test(pidText)) {
293+
continue;
294+
}
295+
seenPids.add(Number(pidText));
296+
}
297+
for (const pid of seenPids) {
298+
const taskkillResult = runCommand('taskkill', ['/PID', String(pid), '/F']);
299+
if (!taskkillResult.error && taskkillResult.status === 0) {
300+
released = true;
301+
}
302+
}
303+
}
304+
} else {
305+
const fuserResult = runCommand('fuser', ['-k', `${numericPort}/tcp`]);
306+
if (!fuserResult.error && fuserResult.status === 0) {
307+
released = true;
308+
}
309+
const fuserMissing = !!(fuserResult && fuserResult.error && fuserResult.error.code === 'ENOENT');
310+
if ((fuserMissing || !released || seenPids.size === 0) && killProcess) {
311+
const lsofResult = runCommand('lsof', ['-ti', `tcp:${numericPort}`]);
312+
if (!(lsofResult && lsofResult.error)) {
313+
// noop: lsof pid collection is handled through stdout parsing above.
314+
}
315+
}
316+
}
317+
318+
if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size === 0) {
319+
const psResult = runCommand('ps', ['-ef']);
320+
if (!(psResult && psResult.error)) {
321+
addManagedRunPidsFromPs(psResult.stdout);
322+
}
323+
}
324+
325+
if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size > 0) {
326+
for (const pid of seenPids) {
327+
if (pid === currentPid) {
328+
continue;
329+
}
330+
try {
331+
killProcess(pid, 'SIGKILL');
332+
released = true;
333+
} catch (_) {}
334+
}
335+
}
336+
337+
if (released) {
338+
logger.log(`~ 已释放端口 ${numericPort} 占用`);
339+
}
340+
341+
return {
342+
attempted: true,
343+
released,
344+
pids: Array.from(seenPids)
345+
.filter((pid) => pid !== currentPid)
346+
.sort((a, b) => a - b)
347+
};
348+
}
349+
// #endregion releaseRunPortIfNeeded
350+
226351
function resolveWebHost(options = {}) {
227352
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
228353
if (optionHost) {
@@ -236,7 +361,6 @@ function resolveWebHost(options = {}) {
236361
}
237362

238363
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
239-
model_reasoning_effort = "high"
240364
model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
241365
model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
242366
disable_response_storage = true
@@ -10845,6 +10969,7 @@ function cmdStart(options = {}) {
1084510969

1084610970
const port = resolveWebPort();
1084710971
const host = resolveWebHost(options);
10972+
releaseRunPortIfNeeded(port);
1084810973

1084910974
let serverHandle = createWebServer({
1085010975
htmlPath,

tests/e2e/test-config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ preferred_auth_method = "shadow-key"
408408
/^\s*model_auto_compact_token_limit\s*=\s*185000\s*$/m.test(legacyTemplateDefaults.template),
409409
'legacy get-config-template should restore default model_auto_compact_token_limit'
410410
);
411+
assert(
412+
!/^\s*model_reasoning_effort\s*=.+$/m.test(legacyTemplateDefaults.template),
413+
'legacy get-config-template should keep default medium reasoning without model_reasoning_effort'
414+
);
411415
const legacyAddDup = await legacyApi('add-provider', {
412416
name: 'foo.bar',
413417
url: 'https://dup.example.com/v1',

tests/unit/web-run-host.test.mjs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ const resolveWebHostSource = extractFunctionBySignature(
118118
'function resolveWebHost(options = {}) {',
119119
'resolveWebHost'
120120
);
121+
const releaseRunPortIfNeededSource = extractFunctionBySignature(
122+
cliContent,
123+
'function releaseRunPortIfNeeded(port, deps = {}) {',
124+
'releaseRunPortIfNeeded'
125+
);
121126
const resolveWebHost = instantiateFunction(resolveWebHostSource, 'resolveWebHost', {
122127
DEFAULT_WEB_HOST: defaultHostMatch[1],
123128
process: { env: {} }
@@ -153,6 +158,149 @@ test('web auto-open uses IPv6 loopback when binding to IPv6 any address', () =>
153158
);
154159
});
155160

161+
test('releaseRunPortIfNeeded skips non-default ports', () => {
162+
const calls = [];
163+
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
164+
DEFAULT_WEB_PORT: 3737,
165+
spawnSync(command, args) {
166+
calls.push([command, args]);
167+
return { status: 0, stdout: '', stderr: '' };
168+
},
169+
process: { platform: 'linux' },
170+
console: { log() {} }
171+
});
172+
173+
const result = releaseRunPortIfNeeded(3999);
174+
assert.deepStrictEqual(result, {
175+
attempted: false,
176+
released: false,
177+
pids: [],
178+
reason: 'non-default-port'
179+
});
180+
assert.deepStrictEqual(calls, []);
181+
});
182+
183+
test('releaseRunPortIfNeeded clears default port via fuser on linux', () => {
184+
const calls = [];
185+
const logs = [];
186+
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
187+
DEFAULT_WEB_PORT: 3737,
188+
spawnSync(command, args) {
189+
calls.push([command, args]);
190+
if (command === 'fuser') {
191+
return { status: 0, stdout: '1234\n', stderr: '' };
192+
}
193+
throw new Error(`unexpected command: ${command}`);
194+
},
195+
process: { platform: 'linux' },
196+
console: { log(message) { logs.push(message); } }
197+
});
198+
199+
const result = releaseRunPortIfNeeded(3737);
200+
assert.deepStrictEqual(calls, [['fuser', ['-k', '3737/tcp']]]);
201+
assert.deepStrictEqual(result, {
202+
attempted: true,
203+
released: true,
204+
pids: [1234]
205+
});
206+
assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
207+
});
208+
209+
test('releaseRunPortIfNeeded falls back to lsof pids when fuser is unavailable', () => {
210+
const calls = [];
211+
const killed = [];
212+
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
213+
DEFAULT_WEB_PORT: 3737,
214+
spawnSync(command, args) {
215+
calls.push([command, args]);
216+
if (command === 'fuser') {
217+
return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
218+
}
219+
if (command === 'lsof') {
220+
return { status: 0, stdout: '2222\n3333\n', stderr: '' };
221+
}
222+
throw new Error(`unexpected command: ${command}`);
223+
},
224+
process: {
225+
platform: 'linux',
226+
kill(pid, signal) {
227+
killed.push([pid, signal]);
228+
}
229+
},
230+
console: { log() {} }
231+
});
232+
233+
const result = releaseRunPortIfNeeded(3737);
234+
assert.deepStrictEqual(calls, [
235+
['fuser', ['-k', '3737/tcp']],
236+
['lsof', ['-ti', 'tcp:3737']]
237+
]);
238+
assert.deepStrictEqual(killed, [
239+
[2222, 'SIGKILL'],
240+
[3333, 'SIGKILL']
241+
]);
242+
assert.deepStrictEqual(result, {
243+
attempted: true,
244+
released: true,
245+
pids: [2222, 3333]
246+
});
247+
});
248+
249+
test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', () => {
250+
const calls = [];
251+
const killed = [];
252+
const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
253+
DEFAULT_WEB_PORT: 3737,
254+
spawnSync(command, args) {
255+
calls.push([command, args]);
256+
if (command === 'fuser') {
257+
return { error: { code: 'EACCES' }, status: 1, stdout: '', stderr: 'Permission denied' };
258+
}
259+
if (command === 'lsof') {
260+
return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
261+
}
262+
if (command === 'ps') {
263+
return {
264+
status: 0,
265+
stdout: [
266+
'UID PID PPID C STIME TTY TIME CMD',
267+
'u0_a876 9001 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
268+
'u0_a876 9002 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run',
269+
'u0_a876 9100 1000 0 1970 ? 00:00:00 node /repo/cli.js config'
270+
].join('\n'),
271+
stderr: ''
272+
};
273+
}
274+
throw new Error(`unexpected command: ${command}`);
275+
},
276+
process: {
277+
platform: 'linux',
278+
pid: 9002,
279+
kill(pid, signal) {
280+
killed.push([pid, signal]);
281+
}
282+
},
283+
console: { log() {} }
284+
});
285+
286+
const result = releaseRunPortIfNeeded(3737);
287+
assert.deepStrictEqual(calls, [
288+
['fuser', ['-k', '3737/tcp']],
289+
['lsof', ['-ti', 'tcp:3737']],
290+
['ps', ['-ef']]
291+
]);
292+
assert.deepStrictEqual(killed, [[9001, 'SIGKILL']]);
293+
assert.deepStrictEqual(result, {
294+
attempted: true,
295+
released: true,
296+
pids: [9001]
297+
});
298+
});
299+
300+
test('cmdStart releases the resolved port before creating the web server', () => {
301+
assert.match(cliContent, /const port = resolveWebPort\(\);\s*const host = resolveWebHost\(options\);\s*releaseRunPortIfNeeded\(port\);\s*let serverHandle = createWebServer\(/s);
302+
});
303+
156304
const getCodexSkillsDirSource = extractFunctionBySignature(
157305
cliContent,
158306
'function getCodexSkillsDir() {',

tests/unit/web-ui-behavior-parity.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function createLoadAllContext() {
105105
currentProvider: 'existing-provider',
106106
currentModel: 'existing-model',
107107
serviceTier: 'fast',
108-
modelReasoningEffort: 'high',
108+
modelReasoningEffort: 'medium',
109109
modelContextWindowInput: 'dirty-context',
110110
modelAutoCompactTokenLimitInput: 'dirty-limit',
111111
editingCodexBudgetField: 'modelContextWindowInput',

web-ui/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
3333
currentProvider: '',
3434
currentModel: '',
3535
serviceTier: 'fast',
36-
modelReasoningEffort: 'high',
36+
modelReasoningEffort: 'medium',
3737
modelContextWindowInput: String(DEFAULT_MODEL_CONTEXT_WINDOW),
3838
modelAutoCompactTokenLimitInput: String(DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT),
3939
editingCodexBudgetField: '',

web-ui/modules/app.methods.startup-claude.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function createStartupClaudeMethods(options = {}) {
4040
const effort = typeof statusRes.modelReasoningEffort === 'string'
4141
? statusRes.modelReasoningEffort.trim().toLowerCase()
4242
: '';
43-
this.modelReasoningEffort = effort || 'high';
43+
this.modelReasoningEffort = effort || 'medium';
4444
}
4545
{
4646
const contextWindow = this.normalizePositiveIntegerInput(

web-ui/partials/index/panel-config-codex.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@
7878
<span class="selector-title">推理强度</span>
7979
</div>
8080
<select class="model-select" v-model="modelReasoningEffort" @change="onReasoningEffortChange">
81-
<option value="high">high(默认)</option>
82-
<option value="medium">medium</option>
81+
<option value="high">high</option>
82+
<option value="medium">medium(默认)</option>
8383
<option value="low">low</option>
8484
<option value="xhigh">xhigh</option>
8585
</select>

0 commit comments

Comments
 (0)