fix: stop untracked CLIProxy installs safely

This commit is contained in:
Tam Nhu Tran
2026-03-07 11:01:35 +07:00
parent a4b626aede
commit ad01196964
3 changed files with 115 additions and 15 deletions
+12 -13
View File
@@ -9,7 +9,7 @@ import { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types';
import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service';
import { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import {
@@ -167,19 +167,18 @@ export async function installCliproxyVersion(
const effectiveBackend = backend ?? getConfiguredBackend();
const manager = new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
// Check if proxy is running and stop it first
if (isProxyRunning()) {
if (verbose) console.log(info('Stopping running CLIProxy before update...'));
const result = await stopProxy();
if (result.stopped) {
// Wait for port to be fully released
const portFree = await waitForPortFree(CLIPROXY_DEFAULT_PORT, 5000);
if (!portFree && verbose) {
console.log(warn('Port did not free up in time, proceeding anyway...'));
}
} else if (verbose && result.error) {
console.log(warn(`Could not stop proxy: ${result.error}`));
// Always attempt a best-effort stop first so we also catch untracked proxies
// that are running without a session lock.
if (verbose) console.log(info('Stopping running CLIProxy before update...'));
const result = await stopProxy();
if (result.stopped) {
// Wait for port to be fully released
const portFree = await waitForPortFree(CLIPROXY_DEFAULT_PORT, 5000);
if (!portFree && verbose) {
console.log(warn('Port did not free up in time, proceeding anyway...'));
}
} else if (verbose && result.error && result.error !== 'No active CLIProxy session found') {
console.log(warn(`Could not stop proxy: ${result.error}`));
}
if (manager.isBinaryInstalled()) {
@@ -51,8 +51,8 @@ export async function installDashboardCliproxyVersion(
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const shouldRestoreService = await wasProxyRunning(deps);
// The installer owns the stop-and-replace lifecycle: it stops a running proxy
// and waits for the port to free before swapping the binary.
// The installer owns the stop-and-replace lifecycle, including best-effort
// shutdown for tracked and untracked proxies before swapping the binary.
await deps.installCliproxyVersion(version, true, backend);
if (!shouldRestoreService) {
@@ -0,0 +1,101 @@
import { afterAll, beforeAll, describe, expect, it, mock } from 'bun:test';
const calls = {
stopProxy: 0,
waitForPortFree: 0,
deleteBinary: 0,
ensureBinary: 0,
};
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
}));
mock.module('../../../src/cliproxy/config-generator', () => ({
getBinDir: () => '/tmp/ccs-bin',
CLIPROXY_DEFAULT_PORT: 8317,
}));
mock.module('../../../src/cliproxy/platform-detector', () => ({
DEFAULT_BACKEND: 'plus',
CLIPROXY_MAX_STABLE_VERSION: '6.6.80',
BACKEND_CONFIG: {
plus: {
fallbackVersion: '6.6.80',
repo: 'router-for-me/CLIProxyAPIPlus',
},
original: {
fallbackVersion: '0.0.0',
repo: 'router-for-me/CLIProxyAPI',
},
},
}));
mock.module('../../../src/cliproxy/services/proxy-lifecycle-service', () => ({
stopProxy: async () => {
calls.stopProxy += 1;
return { stopped: false, error: 'No active CLIProxy session found' };
},
}));
mock.module('../../../src/utils/port-utils', () => ({
waitForPortFree: async () => {
calls.waitForPortFree += 1;
return true;
},
}));
mock.module('../../../src/config/unified-config-loader', () => ({
loadOrCreateUnifiedConfig: () => ({
cliproxy: { backend: 'plus' },
}),
}));
mock.module('../../../src/cliproxy/binary', () => ({
checkForUpdates: async () => ({
hasUpdate: false,
currentVersion: '6.6.80',
latestVersion: '6.6.80',
fromCache: false,
checkedAt: Date.now(),
}),
deleteBinary: () => {
calls.deleteBinary += 1;
},
getBinaryPath: () => '/tmp/ccs-bin/plus/cliproxy',
isBinaryInstalled: () => false,
getBinaryInfo: async () => null,
getPinnedVersion: () => null,
savePinnedVersion: () => {},
clearPinnedVersion: () => {},
isVersionPinned: () => false,
getVersionPinPath: () => '/tmp/ccs-bin/plus/.version-pin',
readInstalledVersion: () => '6.6.80',
ensureBinary: async () => {
calls.ensureBinary += 1;
return '/tmp/ccs-bin/plus/cliproxy';
},
migrateVersionPin: () => {},
}));
let binaryManager: typeof import('../../../src/cliproxy/binary-manager');
beforeAll(async () => {
binaryManager = await import('../../../src/cliproxy/binary-manager');
});
afterAll(() => {
mock.restore();
});
describe('installCliproxyVersion', () => {
it('attempts to stop the proxy even when there is no tracked running session', async () => {
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus');
expect(calls.stopProxy).toBe(1);
expect(calls.waitForPortFree).toBe(0);
expect(calls.deleteBinary).toBe(0);
expect(calls.ensureBinary).toBe(1);
});
});