feat: Implement local service management

- Add local service start/stop functionality
- Integrate local service port discovery and setup
- Update DevChatClient to use dynamic port from environment
This commit is contained in:
bobo.yang 2024-07-16 07:55:02 +08:00
parent f707bef5fc
commit 99f7a1d4da
8 changed files with 212 additions and 20 deletions

View File

@ -13,6 +13,8 @@ import { chatWithDevChat } from '../handler/chatHandler';
import { focusDevChatInput } from '../handler/focusHandler';
import { DevChatConfig } from '../util/config';
import { MessageHandler } from "../handler/messageHandler";
import { startLocalService } from '../util/localService';
import { logger } from "../util/logger";
const readdir = util.promisify(fs.readdir);
const mkdir = util.promisify(fs.mkdir);
@ -222,6 +224,28 @@ export function registerInstallCommandsCommand(
}
export function registerStartLocalServiceCommand(
context: vscode.ExtensionContext
) {
let disposable = vscode.commands.registerCommand(
"DevChat.StartLocalService",
async () => {
try {
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() ?? '';
logger.channel()?.debug(`extensionPath: ${context.extensionPath}`);
logger.channel()?.debug(`workspacePath: ${workspaceDir}`);
const port = await startLocalService(context.extensionPath, workspaceDir);
logger.channel()?.debug(`Local service started on port ${port}`);
} catch (error) {
logger.channel()?.error('Failed to start local service:', error);
}
}
);
context.subscriptions.push(disposable);
}
export function registerDevChatChatCommand(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand(
"DevChat.Chat",

View File

@ -3,7 +3,7 @@ import { insertCodeBlockToFile } from './codeBlockHandler';
import { replaceCodeBlockToFile } from './codeBlockHandler';
import { doCommit } from './commitHandler';
import { getHistoryMessages } from './historyMessagesHandler';
import { getWorkflowCommandList } from './workflowCommandHandler';
import { handleRegCommandList } from './workflowCommandHandler';
import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage';
import { applyCodeWithDiff } from './diffHandler';
import { addConext } from './contextHandler';
@ -36,7 +36,7 @@ messageHandler.registerHandler('doCommit', doCommit);
messageHandler.registerHandler('historyMessages', getHistoryMessages);
// Register the command list
// Response: { command: 'regCommandList', result: <command list> }
messageHandler.registerHandler('regCommandList', getWorkflowCommandList);
messageHandler.registerHandler('regCommandList', handleRegCommandList);
// Send a message, send the message entered by the user to AI
// Response:
// { command: 'receiveMessagePartial', text: <response message text>, user: <user>, date: <date> }

View File

@ -12,11 +12,17 @@ regOutMessage({
command: "regCommandList",
result: [{ name: "", pattern: "", description: "" }],
});
export async function getWorkflowCommandList(
export async function handleRegCommandList(
message: any,
panel: vscode.WebviewPanel | vscode.WebviewView
): Promise<void> {
existPannel = panel;
}
export async function getWorkflowCommandList(
message: any,
panel: vscode.WebviewPanel | vscode.WebviewView
): Promise<void> {
const dcClient = new DevChatClient();
// All workflows registered in DevChat
@ -34,10 +40,12 @@ export async function getWorkflowCommandList(
recommend: recommends.indexOf(workflow.name),
}));
MessageHandler.sendMessage(panel, {
command: "regCommandList",
result: commandList,
});
if (commandList.length > 0) {
MessageHandler.sendMessage(panel, {
command: "regCommandList",
result: commandList,
});
}
return;
}

View File

@ -51,6 +51,7 @@ export function createStatusBarItem(context: vscode.ExtensionContext): vscode.St
// install devchat workflow commands
if (!hasInstallCommands) {
hasInstallCommands = true;
await vscode.commands.executeCommand('DevChat.StartLocalService');
await vscode.commands.executeCommand('DevChat.InstallCommands');
// vscode.commands.executeCommand('DevChat.InstallCommandPython');
}

View File

@ -8,6 +8,14 @@ import { getFileContent } from "../util/commonUtil";
import { UiUtilWrapper } from "../util/uiUtil";
class DCLocalServicePortNotSetError extends Error {
constructor() {
super("DC_LOCALSERVICE_PORT is not set");
this.name = "DCLocalServicePortNotSetError";
}
}
function timeThis(
target: Object,
propertyKey: string,
@ -44,6 +52,11 @@ function catchAndReturn(defaultReturn: any) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
if (error instanceof DCLocalServicePortNotSetError) {
logger.channel()?.warn(`DC_LOCALSERVICE_PORT is not set in [${propertyKey}]`);
return defaultReturn;
}
logger.channel()?.error(`Error in [${propertyKey}]: ${error}`);
return defaultReturn;
}
@ -126,25 +139,26 @@ export async function buildRoleContextsFromFiles(
return contexts;
}
// TODO: 在插件启动为每个vscode窗口启动一个devchat local service
// 1. 分配单独的端口号,该窗口的所有请求都通过该端口号发送 (22222仅为作为开发默认端口号不应用于生产)
// 2. 启动local service时要配置多个worker以便处理并发请求
// TODO: 在插件关闭时关闭其对应的devchat local service
export class DevChatClient {
private baseURL: string;
private baseURL: string | undefined;
private _cancelMessageToken: CancelTokenSource | null = null;
static readonly logRawDataSizeLimit = 4 * 1024;
// TODO: init devchat client with a port number
// TODO: the default 22222 is for dev only, should not be used in production
constructor(port: number = 22222) {
this.baseURL = `http://localhost:${port}`;
constructor() {
}
async _get(path: string, config?: any): Promise<AxiosResponse> {
if (!this.baseURL) {
if (!process.env.DC_LOCALSERVICE_PORT) {
logger.channel()?.info("No local service port found.");
throw new DCLocalServicePortNotSetError();
}
const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10);
this.baseURL = `http://localhost:${port}`;
}
try {
logger.channel()?.debug(`GET request to ${this.baseURL}${path}`);
const response = await axios.get(`${this.baseURL}${path}`, config);
@ -155,6 +169,15 @@ export class DevChatClient {
}
}
async _post(path: string, data: any = undefined): Promise<AxiosResponse> {
if (!this.baseURL) {
if (!process.env.DC_LOCALSERVICE_PORT) {
logger.channel()?.info("No local service port found.");
throw new DCLocalServicePortNotSetError();
}
const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10);
this.baseURL = `http://localhost:${port}`;
}
try {
logger.channel()?.debug(`POST request to ${this.baseURL}${path}`);
const response = await axios.post(`${this.baseURL}${path}`, data);
@ -211,6 +234,14 @@ export class DevChatClient {
message: ChatRequest,
onData: (data: ChatResponse) => void
): Promise<ChatResponse> {
if (!this.baseURL) {
if (!process.env.DC_LOCALSERVICE_PORT) {
logger.channel()?.info("No local service port found.");
}
const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10);
this.baseURL = `http://localhost:${port}`;
}
this._cancelMessageToken = axios.CancelToken.source();
const workspace = UiUtilWrapper.workspaceFoldersFirstPath();
// const workspace = undefined;

View File

@ -1,7 +1,5 @@
// src/apiKey.ts
import DevChat from '@/toolwrapper/devchat';
import { UiUtilWrapper } from './uiUtil';
import { DevChatConfig } from './config';
import { logger } from './logger';

View File

@ -0,0 +1,26 @@
import net from 'net';
export async function findAvailablePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer().listen();
server.on('listening', () => {
const address = server.address();
if (typeof address !== 'object' || !address?.port) {
server.close();
reject(new Error('Failed to get port from server'));
return;
}
server.close(() => resolve(address.port));
});
server.on('error', (err) => {
const errWithCode = err as NodeJS.ErrnoException;
if (errWithCode.code === 'EADDRINUSE') {
reject(new Error('Port already in use'));
} else {
reject(err);
}
});
});
}

104
src/util/localService.ts Normal file
View File

@ -0,0 +1,104 @@
import { spawn, ChildProcess } from 'child_process';
import { findAvailablePort } from './findServicePort';
import * as http from 'http';
import { logger } from './logger';
import { DevChatConfig } from './config';
let serviceProcess: ChildProcess | null = null;
export async function startLocalService(extensionPath: string, workspacePath: string): Promise<number> {
if (serviceProcess) {
throw new Error('Local service is already running');
}
try {
// 1. 获取可用端口号
const port = await findAvailablePort();
// 2. 设置环境变量 DC_SVC_PORT
process.env.DC_SVC_PORT = port.toString();
// 3. 设置 DC_SVC_WORKSPACE 环境变量
process.env.DC_SVC_WORKSPACE = workspacePath;
// 新增:设置 PYTHONPATH 环境变量
process.env.PYTHONPATH = `${extensionPath}/tools/site-packages`;
// 4. 启动进程 python main.py
const mainPyPath = extensionPath + "/tools/site-packages/devchat/_service/main.py";
const pythonApp =
DevChatConfig.getInstance().get("python_for_chat") || "python3";
serviceProcess = spawn(pythonApp, [mainPyPath], {
env: { ...process.env },
stdio: 'inherit'
});
serviceProcess.on('error', (err) => {
logger.channel()?.error('Failed to start local service:', err);
serviceProcess = null;
});
serviceProcess.on('exit', (code) => {
logger.channel()?.info(`Local service exited with code ${code}`);
serviceProcess = null;
});
// 5. 等待服务启动并验证
await waitForServiceToStart(port);
// 6. 服务启动成功后,记录启动的端口号到环境变量
process.env.DC_LOCALSERVICE_PORT = port.toString();
logger.channel()?.info(`Local service port recorded: ${port}`);
return port;
} catch (error) {
logger.channel()?.error('Error starting local service:', error);
throw error;
}
}
async function waitForServiceToStart(port: number): Promise<void> {
const maxRetries = 30;
const retryInterval = 1000; // 1 second
for (let i = 0; i < maxRetries; i++) {
try {
const response = await new Promise<string>((resolve, reject) => {
http.get(`http://localhost:${port}/ping`, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(data));
}).on('error', reject);
});
if (response === '{"message":"pong"}') {
logger.channel()?.info('Local service started successfully');
return;
}
} catch (error) {
// Ignore errors and continue retrying
}
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
throw new Error('Failed to start local service: timeout');
}
export async function stopLocalService(): Promise<void> {
return new Promise((resolve) => {
if (!serviceProcess) {
logger.channel()?.warn('No local service is running');
resolve();
return;
}
serviceProcess.on('exit', () => {
serviceProcess = null;
logger.channel()?.info('Local service stopped');
resolve();
});
serviceProcess.kill();
});
}