Skip to content

Commit f0457ee

Browse files
committed
fix:修复rag服务无法重启的bug
1 parent a993515 commit f0457ee

File tree

2 files changed

+163
-63
lines changed

2 files changed

+163
-63
lines changed

src/chatPanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class ChatPanel {
146146
<button id="stop" style="display:none;">Stop</button>
147147
<div class="web-search">
148148
<input type="checkbox" id="web-search">
149-
<label for="web-search">联网搜索</label>
149+
<label for="web-search">Agent模式(可联网)</label>
150150
</div>
151151
</div>
152152
</div>

src/ragService.ts

Lines changed: 162 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// src/ragService.ts
2-
import { ChildProcess, execSync, spawn } from 'child_process';
2+
import { ChildProcess, execSync, exec, spawn } from 'child_process';
33
import * as vscode from 'vscode';
44
import * as path from 'path';
55
import * as fs from 'fs/promises';
@@ -10,24 +10,23 @@ import * as os from 'os';
1010
let EXTENSION_PATH: string = '';
1111

1212
const isDevMode = __dirname.includes('dist');
13-
// 动态设置 Python 脚本或 EXE 路径
13+
// Dynamically set Python script or EXE path
1414
const getPythonScriptPath = (extensionPath: string) => {
1515
if (isDevMode) {
16-
return path.join(extensionPath, 'src', 'python', 'rag.py'); // 开发模式:直接运行 Python 脚本
16+
return path.join(extensionPath, 'src', 'python', 'rag.py'); // Development mode: run Python script directly
1717
} else {
18-
return path.join(extensionPath, 'dist', 'rag.exe'); // 发布模式:运行 EXE
18+
return path.join(extensionPath, 'dist', 'rag.exe'); // Production mode: run EXE
1919
}
2020
};
2121

22-
// 配置常量 (use a function to resolve paths dynamically)
22+
// Configuration constants (use a function to resolve paths dynamically)
2323
export const CONFIG = {
2424
STORAGE_PATH: path.join(os.homedir(), 'CodeReDesignMemory', 'rag_storage'),
2525
PORT: 7111,
26-
LOCK_FILE: 'rag.lock',
27-
PYTHON_SCRIPT: getPythonScriptPath(EXTENSION_PATH)
28-
};
26+
LOCK_FILE: 'rag.lock'
27+
};
2928

30-
// 类型定义
29+
// Type definitions
3130
type ServerStatus = 'stopped' | 'starting' | 'running' | 'error';
3231

3332
interface ServerState {
@@ -53,7 +52,7 @@ class RagService {
5352

5453
private initializeStorage(): void {
5554
fs.mkdir(CONFIG.STORAGE_PATH, { recursive: true }).catch(err => {
56-
vscode.window.showErrorMessage(`创建存储目录失败: ${err}`);
55+
vscode.window.showErrorMessage(`Failed to create storage directory: ${err}`);
5756
});
5857
}
5958

@@ -71,7 +70,7 @@ class RagService {
7170
await this.waitForStartup();
7271

7372
} catch (err) {
74-
await this.cleanup(`启动失败: ${err}`);
73+
await this.cleanup(`Startup failed: ${err}`);
7574
}
7675
}
7776

@@ -91,74 +90,176 @@ class RagService {
9190
this.state.lockFileHandle = handle;
9291
} catch (err) {
9392
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
94-
throw new Error('已有服务进程在运行');
93+
throw new Error('Another service process is already running');
9594
}
9695
throw err;
9796
}
9897
}
9998

100-
private async ensurePortAvailable(): Promise<void> {
99+
private async checkPortInUse(port: number): Promise<boolean> {
101100
return new Promise((resolve, reject) => {
102-
const server = net.createServer();
103-
server.unref();
104-
105-
server.on('error', (err: NodeJS.ErrnoException) => {
106-
if (err.code === 'EADDRINUSE') {
107-
this.killProcessOnPort(CONFIG.PORT)
108-
.then(resolve)
109-
.catch(reject);
110-
} else {
111-
reject(err);
101+
try {
102+
let command: string;
103+
let shell: string;
104+
105+
if (process.platform === 'win32') {
106+
// 使用 netstat 检查端口,查找 LISTENING 状态
107+
command = `netstat -aon | findstr /R "^.*:${port}\\s.*LISTENING.*$"`;
108+
shell = 'cmd.exe';
109+
} else {
110+
command = `lsof -i :${port} -t`;
111+
shell = '/bin/sh';
112+
}
113+
114+
exec(command, { shell, encoding: 'utf8' }, (error, stdout) => {
115+
if (error && !stdout) {
116+
console.log(`Port ${port} is not in use (command check)`);
117+
resolve(false);
118+
} else if (stdout.trim()) {
119+
console.log(`Port ${port} is in use (command check): ${stdout.trim()}`);
120+
resolve(true);
121+
} else {
122+
resolve(false);
123+
}
124+
});
125+
} catch (err) {
126+
console.error(`Failed to check port ${port}: ${(err as Error).message}`);
127+
reject(new Error(`Failed to check port ${port}: ${(err as Error).message}`));
112128
}
113-
});
129+
});
130+
}
114131

115-
server.listen(CONFIG.PORT, () => {
116-
server.close(() => resolve());
117-
});
132+
private async ensurePortAvailable(): Promise<void> {
133+
console.log(`Checking if port ${CONFIG.PORT} is available...`);
134+
135+
// 主动检查端口是否被占用
136+
const isPortInUse = await this.checkPortInUse(CONFIG.PORT);
137+
if (isPortInUse) {
138+
console.log(`Port ${CONFIG.PORT} is in use, attempting to free it...`);
139+
await this.killProcessOnPort(CONFIG.PORT);
140+
}
141+
142+
// 使用 net.createServer 进行二次验证
143+
return new Promise((resolve, reject) => {
144+
const server = net.createServer();
145+
server.unref();
146+
147+
server.on('error', (err: NodeJS.ErrnoException) => {
148+
console.log(`net.createServer error: ${err.message}`);
149+
if (err.code === 'EADDRINUSE') {
150+
console.log(`Port ${CONFIG.PORT} is still in use, attempting to free it again...`);
151+
this.killProcessOnPort(CONFIG.PORT)
152+
.then(resolve)
153+
.catch(reject);
154+
} else {
155+
reject(new Error(`Failed to check port ${CONFIG.PORT}: ${err.message}`));
156+
}
157+
});
158+
159+
server.listen(CONFIG.PORT, '0.0.0.0', () => {
160+
console.log(`Port ${CONFIG.PORT} is available via net.createServer`);
161+
server.close(() => resolve());
162+
});
118163
});
119164
}
120165

121166
private killProcessOnPort(port: number): Promise<void> {
167+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
168+
throw new Error(`Invalid port number: ${port}`);
169+
}
170+
122171
return new Promise((resolve, reject) => {
123-
try {
124-
const command = process.platform === 'win32'
125-
? `netstat -ano | findstr :${port} && FOR /F "tokens=5" %p IN ('netstat -ano ^| findstr :${port}') DO taskkill /F /PID %p`
126-
: `lsof -i :${port} -t | xargs kill -9`;
127-
128-
// 修复类型错误
129-
execSync(command, {
130-
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
131-
encoding: 'utf-8'
132-
});
133-
134-
setTimeout(resolve, 2000);
135-
} catch (err) {
136-
reject(new Error(`无法释放端口 ${port}`));
137-
}
172+
try {
173+
let command: string;
174+
let shell: string;
175+
176+
if (process.platform === 'win32') {
177+
command = `netstat -aon | findstr /R "TCP.*:${port}.*LISTENING"`;
178+
shell = 'cmd.exe';
179+
} else {
180+
command = `lsof -i :${port} -t`;
181+
shell = '/bin/sh';
182+
}
183+
184+
exec(command, { shell, encoding: 'utf8' }, (error, stdout) => {
185+
if (error && !stdout) {
186+
console.log(`No process found on port ${port}`);
187+
return resolve();
188+
}
189+
190+
const pids = stdout
191+
.trim()
192+
.split('\n')
193+
.map(line => {
194+
const parts = line.trim().split(/\s+/);
195+
return parts[parts.length - 1]; // PID 在最后一列
196+
})
197+
.filter(pid => pid && /^\d+$/.test(pid)); // 确保是数字
198+
199+
if (pids.length === 0) {
200+
console.log(`No valid PIDs found on port ${port}`);
201+
return resolve();
202+
}
203+
204+
console.log(`Terminating PIDs on port ${port}: ${pids.join(', ')}`);
205+
const killCommand = process.platform === 'win32'
206+
? `taskkill /F /PID ${pids.join(' ')}`
207+
: `kill -TERM ${pids.join(' ')}`;
208+
209+
exec(killCommand, { shell }, (killError) => {
210+
if (killError) {
211+
console.error(`Failed to terminate processes: ${killError.message}`);
212+
return reject(new Error(`Failed to terminate processes on port ${port}: ${killError.message}`));
213+
}
214+
215+
// 重试检查端口是否释放
216+
let retries = 3;
217+
const checkPort = () => {
218+
exec(command, { shell, encoding: 'utf8' }, (checkError, checkStdout) => {
219+
if (checkStdout.trim()) {
220+
console.log(`Port ${port} still in use, retries left: ${retries}`);
221+
if (--retries > 0) {
222+
setTimeout(checkPort, 1000);
223+
} else {
224+
reject(new Error(`Port ${port} still in use after termination`));
225+
}
226+
} else {
227+
console.log(`Port ${port} successfully released`);
228+
resolve();
229+
}
230+
});
231+
};
232+
setTimeout(checkPort, 1000);
233+
});
234+
});
235+
} catch (err) {
236+
console.error(`Error freeing port ${port}: ${(err as Error).message}`);
237+
reject(new Error(`Failed to free port ${port}: ${(err as Error).message}`));
238+
}
138239
});
139240
}
140241

141242
private findWindowsPython(): string {
142243
try {
143-
// 获取所有Python路径并解析
244+
// Get all Python paths and parse
144245
const output = execSync('where python', {
145246
shell: 'cmd.exe',
146247
encoding: 'utf-8'
147248
});
148249

149-
// 清洗并分割路径
250+
// Clean and split paths
150251
const paths = output
151-
.replace(/\r/g, '') // 去除回车符
252+
.replace(/\r/g, '') // Remove carriage returns
152253
.split('\n')
153254
.map(p => p.trim())
154255
.filter(p => p.length > 0);
155256

156-
// 验证第一个有效路径
257+
// Verify the first valid path
157258
if (paths.length === 0) {
158-
throw new Error('未找到Python路径');
259+
throw new Error('No Python path found');
159260
}
160261

161-
// 优先选择带空格的路径用双引号包裹
262+
// Prefer paths with spaces wrapped in quotes
162263
const validPath = paths.find(p => {
163264
try {
164265
fs.access(p, fs.constants.X_OK);
@@ -169,26 +270,26 @@ class RagService {
169270
});
170271

171272
return validPath
172-
? `"${validPath}"` // 处理路径中的空格
173-
: paths[0]; // 最后兜底方案
273+
? `"${validPath}"` // Handle spaces in path
274+
: paths[0]; // Fallback
174275

175276
} catch (error) {
176-
// 回退方案
177-
vscode.window.showWarningMessage('Python自动检测失败,使用默认路径');
277+
// Fallback scheme
278+
vscode.window.showWarningMessage('Python detection failed, using default path');
178279
return 'python.exe';
179280
}
180281
}
181282

182283
private startPythonProcess(): ChildProcess {
183-
const scriptPath = CONFIG.PYTHON_SCRIPT;
284+
const scriptPath = getPythonScriptPath(EXTENSION_PATH);
184285
const isExe = scriptPath.endsWith('.exe');
185286

186287
if (!fs.access(scriptPath)) {
187-
throw new Error(`未找到脚本或可执行文件: ${scriptPath}`);
288+
throw new Error(`Script or executable not found: ${scriptPath}`);
188289
}
189290

190291
if (isExe) {
191-
// 直接运行 EXE
292+
// Run EXE directly
192293
return spawn(scriptPath, [
193294
`--port=${CONFIG.PORT}`,
194295
`--storage_path=${CONFIG.STORAGE_PATH}`
@@ -198,7 +299,7 @@ class RagService {
198299
shell: true
199300
});
200301
} else {
201-
// 运行 Python 脚本
302+
// Run Python script
202303
const pythonPath = process.platform === 'win32'
203304
? this.findWindowsPython()
204305
: this.findUnixPython();
@@ -217,7 +318,7 @@ class RagService {
217318

218319
private findUnixPython(): string {
219320
try {
220-
// 精确获取python3路径
321+
// Precisely get python3 path
221322
return execSync('which python3', {
222323
shell: '/bin/sh',
223324
encoding: 'utf-8'
@@ -234,26 +335,25 @@ class RagService {
234335
process.stderr?.on('data', (data: Buffer) => {
235336
if (data.includes('Uvicorn running')) {
236337
this.state.status = 'running';
237-
vscode.window.showInformationMessage('RAG 服务已启动');
338+
vscode.window.showInformationMessage('RAG service started');
238339
}
239340
});
240341

241342
process.stderr?.on('data', (data: Buffer) => {
242-
//this.state.status = 'error';
243343
console.error(`[Python Error] ${data}`);
244344
});
245345

246346
process.on('exit', code => {
247347
if (code !== 0) {
248-
this.cleanup(`服务异常退出,代码: ${code}`);
348+
this.cleanup(`Service exited abnormally with code: ${code}`);
249349
}
250350
});
251351
}
252352

253353
private async waitForStartup(): Promise<void> {
254354
return new Promise((resolve, reject) => {
255355
const timeout = setTimeout(() => {
256-
reject(new Error('服务启动超时'));
356+
reject(new Error('Service startup timed out'));
257357
}, 15000);
258358

259359
const checkInterval = setInterval(() => {
@@ -281,7 +381,7 @@ class RagService {
281381
}
282382
}
283383

284-
// 插件集成
384+
// Plugin integration
285385
export const ragService = RagService.getInstance();
286386

287387
export async function activate(context: vscode.ExtensionContext) {

0 commit comments

Comments
 (0)