freeform-modeling-mcp 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/index.cjs +21 -0
- package/package.json +48 -0
- package/src/cli.ts +47 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/status.ts +81 -0
- package/src/dev.ts +24 -0
- package/src/lib/fileproxy/index.ts +505 -0
- package/src/lib/fileproxy/proxy-server.js +347 -0
- package/src/lib/gltf2obj/compress.js +33 -0
- package/src/lib/gltf2obj/engine/core/core.js +34 -0
- package/src/lib/gltf2obj/engine/core/eventnotifier.js +39 -0
- package/src/lib/gltf2obj/engine/core/taskrunner.js +88 -0
- package/src/lib/gltf2obj/engine/export/exporter.js +38 -0
- package/src/lib/gltf2obj/engine/export/exporterbase.js +86 -0
- package/src/lib/gltf2obj/engine/export/exportermodel.js +117 -0
- package/src/lib/gltf2obj/engine/export/exporterobj.js +147 -0
- package/src/lib/gltf2obj/engine/geometry/box3d.js +60 -0
- package/src/lib/gltf2obj/engine/geometry/coord2d.js +35 -0
- package/src/lib/gltf2obj/engine/geometry/coord3d.js +126 -0
- package/src/lib/gltf2obj/engine/geometry/coord4d.js +15 -0
- package/src/lib/gltf2obj/engine/geometry/geometry.js +56 -0
- package/src/lib/gltf2obj/engine/geometry/matrix.js +440 -0
- package/src/lib/gltf2obj/engine/geometry/octree.js +160 -0
- package/src/lib/gltf2obj/engine/geometry/quaternion.js +83 -0
- package/src/lib/gltf2obj/engine/geometry/transformation.js +63 -0
- package/src/lib/gltf2obj/engine/geometry/tween.js +31 -0
- package/src/lib/gltf2obj/engine/import/importer.js +270 -0
- package/src/lib/gltf2obj/engine/import/importerbase.js +115 -0
- package/src/lib/gltf2obj/engine/import/importerfiles.js +139 -0
- package/src/lib/gltf2obj/engine/import/importergltf.js +1045 -0
- package/src/lib/gltf2obj/engine/import/importerutils.js +102 -0
- package/src/lib/gltf2obj/engine/io/binaryreader.js +93 -0
- package/src/lib/gltf2obj/engine/io/binarywriter.js +92 -0
- package/src/lib/gltf2obj/engine/io/bufferutils.js +85 -0
- package/src/lib/gltf2obj/engine/io/externallibs.js +42 -0
- package/src/lib/gltf2obj/engine/io/fileutils.js +120 -0
- package/src/lib/gltf2obj/engine/io/textwriter.js +41 -0
- package/src/lib/gltf2obj/engine/main.js +19 -0
- package/src/lib/gltf2obj/engine/model/color.js +130 -0
- package/src/lib/gltf2obj/engine/model/generator.js +433 -0
- package/src/lib/gltf2obj/engine/model/material.js +243 -0
- package/src/lib/gltf2obj/engine/model/mesh.js +173 -0
- package/src/lib/gltf2obj/engine/model/meshbuffer.js +233 -0
- package/src/lib/gltf2obj/engine/model/meshinstance.js +128 -0
- package/src/lib/gltf2obj/engine/model/meshutils.js +64 -0
- package/src/lib/gltf2obj/engine/model/model.js +195 -0
- package/src/lib/gltf2obj/engine/model/modelfinalization.js +377 -0
- package/src/lib/gltf2obj/engine/model/modelutils.js +117 -0
- package/src/lib/gltf2obj/engine/model/node.js +178 -0
- package/src/lib/gltf2obj/engine/model/object.js +90 -0
- package/src/lib/gltf2obj/engine/model/property.js +86 -0
- package/src/lib/gltf2obj/engine/model/quantities.js +46 -0
- package/src/lib/gltf2obj/engine/model/topology.js +139 -0
- package/src/lib/gltf2obj/engine/model/triangle.js +99 -0
- package/src/lib/gltf2obj/engine/threejs/threemodelloader.js +85 -0
- package/src/lib/gltf2obj/handler.js +63 -0
- package/src/lib/gltf2obj/index.js +47 -0
- package/src/lib/logger.ts +304 -0
- package/src/lib/print.ts +37 -0
- package/src/lib/utils.ts +34 -0
- package/src/lib/wsbridge/bridge.ts +639 -0
- package/src/lib/wsbridge/lock.ts +258 -0
- package/src/llmClient.ts +245 -0
- package/src/prompt.ts +187 -0
- package/src/server.ts +125 -0
- package/src/tools/assets-tool.ts +314 -0
- package/src/tools/basic-tools.ts +531 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/material-tools.ts +88 -0
- package/src/tools/modeling/auxiliary-curve-tool.ts +44 -0
- package/src/tools/modeling/find-face.ts +38 -0
- package/src/tools/modeling/sweep-tool.ts +60 -0
- package/src/tools/modeling/utils.ts +181 -0
- package/src/tools/modeling-tools.ts +135 -0
- package/src/tools/screenshot-tools.ts +107 -0
- package/src/tools/tools-info.ts +107 -0
- package/src/tools/tripo3d-tools.ts +496 -0
- package/src/types/index.ts +25 -0
- package/src/types/tripo3d.ts +38 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import * as http from 'http';
|
|
6
|
+
import * as net from 'net';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import logger from '../logger';
|
|
9
|
+
|
|
10
|
+
// 在ES模块中获取__dirname
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// 用户主目录下的应用数据目录
|
|
15
|
+
const USER_HOME = os.homedir();
|
|
16
|
+
const APP_DATA_DIR = path.join(USER_HOME, '.koomaster-proxy');
|
|
17
|
+
|
|
18
|
+
// 配置
|
|
19
|
+
const config = {
|
|
20
|
+
serverScript: path.join(__dirname, './proxy-server.js'),
|
|
21
|
+
port: process.env.PORT || 3000,
|
|
22
|
+
logDirectory: path.join(APP_DATA_DIR, 'logs'),
|
|
23
|
+
lockDirectory: path.join(APP_DATA_DIR, 'locks')
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// 确保应用数据目录存在
|
|
27
|
+
if (!fs.existsSync(APP_DATA_DIR)) {
|
|
28
|
+
fs.mkdirSync(APP_DATA_DIR, { recursive: true });
|
|
29
|
+
logger.info(`已创建应用数据目录: ${APP_DATA_DIR}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 确保日志和锁目录存在
|
|
33
|
+
if (!fs.existsSync(config.logDirectory)) {
|
|
34
|
+
fs.mkdirSync(config.logDirectory, { recursive: true });
|
|
35
|
+
logger.info(`已创建日志目录: ${config.logDirectory}`);
|
|
36
|
+
}
|
|
37
|
+
if (!fs.existsSync(config.lockDirectory)) {
|
|
38
|
+
fs.mkdirSync(config.lockDirectory, { recursive: true });
|
|
39
|
+
logger.info(`已创建锁目录: ${config.lockDirectory}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 生成锁文件路径
|
|
43
|
+
function getLockFilePath(pid: number, port: number): string {
|
|
44
|
+
return path.join(config.lockDirectory, `proxy-${pid}-${port}.lock`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 创建锁文件
|
|
48
|
+
function createLockFile(pid: number, port: number): void {
|
|
49
|
+
try {
|
|
50
|
+
const lockFilePath = getLockFilePath(pid, port);
|
|
51
|
+
const data = JSON.stringify({
|
|
52
|
+
pid,
|
|
53
|
+
port,
|
|
54
|
+
startTime: new Date().toISOString(),
|
|
55
|
+
hostname: os.hostname(),
|
|
56
|
+
appVersion: process.env.npm_package_version || 'unknown'
|
|
57
|
+
}, null, 2);
|
|
58
|
+
fs.writeFileSync(lockFilePath, data, 'utf8');
|
|
59
|
+
logger.info(`已创建锁文件: ${lockFilePath}`);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
logger.error('创建锁文件失败:', e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 删除锁文件
|
|
66
|
+
function removeLockFile(pid: number, port: number): void {
|
|
67
|
+
try {
|
|
68
|
+
const lockFilePath = getLockFilePath(pid, port);
|
|
69
|
+
if (fs.existsSync(lockFilePath)) {
|
|
70
|
+
fs.unlinkSync(lockFilePath);
|
|
71
|
+
logger.info(`已删除锁文件: ${lockFilePath}`);
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
logger.error('删除锁文件失败:', e);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 获取所有活跃服务实例
|
|
79
|
+
export function getActiveInstances(): { pid: number, port: number }[] {
|
|
80
|
+
const instances: { pid: number, port: number }[] = [];
|
|
81
|
+
try {
|
|
82
|
+
logger.info(`正在检查锁目录: ${config.lockDirectory}`);
|
|
83
|
+
// 读取锁目录中的所有文件
|
|
84
|
+
if (!fs.existsSync(config.lockDirectory)) {
|
|
85
|
+
logger.info('锁目录不存在,创建中...');
|
|
86
|
+
fs.mkdirSync(config.lockDirectory, { recursive: true });
|
|
87
|
+
return instances;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const files = fs.readdirSync(config.lockDirectory);
|
|
91
|
+
logger.info(`发现 ${files.length} 个锁文件`);
|
|
92
|
+
|
|
93
|
+
// 通过文件名解析PID和端口
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const match = file.match(/proxy-(\d+)-(\d+)\.lock/);
|
|
96
|
+
if (match) {
|
|
97
|
+
const [_, pidStr, portStr] = match;
|
|
98
|
+
const pid = parseInt(pidStr, 10);
|
|
99
|
+
const port = parseInt(portStr, 10);
|
|
100
|
+
|
|
101
|
+
logger.info(`检查进程 PID ${pid} 是否在运行...`);
|
|
102
|
+
|
|
103
|
+
// 检查进程是否仍在运行
|
|
104
|
+
if (isProcessRunning(pid)) {
|
|
105
|
+
logger.info(`进程 PID ${pid} 在运行中,添加到活跃实例列表`);
|
|
106
|
+
instances.push({ pid, port });
|
|
107
|
+
} else {
|
|
108
|
+
// 如果进程不存在,删除过期的锁文件
|
|
109
|
+
try {
|
|
110
|
+
const staleLockPath = path.join(config.lockDirectory, file);
|
|
111
|
+
logger.info(`进程 PID ${pid} 不在运行,删除过期锁文件: ${staleLockPath}`);
|
|
112
|
+
fs.unlinkSync(staleLockPath);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
logger.error(`删除过期锁文件失败: ${e.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
logger.error('获取活跃实例失败:', e);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logger.info(`找到 ${instances.length} 个活跃实例`);
|
|
124
|
+
return instances;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 检查进程是否存在
|
|
128
|
+
function isProcessRunning(pid: number): boolean {
|
|
129
|
+
if (!pid) return false;
|
|
130
|
+
try {
|
|
131
|
+
// 在Unix上,如果进程不存在,process.kill会抛出异常
|
|
132
|
+
// 在Windows上,这仅会检查进程是否有权接收信号
|
|
133
|
+
process.kill(pid, 0);
|
|
134
|
+
return true;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 检查端口是否可用
|
|
141
|
+
async function isPortAvailable(port: number): Promise<boolean> {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const server = net.createServer()
|
|
144
|
+
.once('error', (err: any) => {
|
|
145
|
+
// 如果端口已被占用,会抛出 EADDRINUSE 错误
|
|
146
|
+
if (err.code === 'EADDRINUSE') {
|
|
147
|
+
resolve(false);
|
|
148
|
+
} else {
|
|
149
|
+
resolve(false); // 其他错误也视为不可用
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
.once('listening', () => {
|
|
153
|
+
// 端口可用,关闭测试服务器
|
|
154
|
+
server.close();
|
|
155
|
+
resolve(true);
|
|
156
|
+
})
|
|
157
|
+
.listen(port);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 查找可用端口
|
|
162
|
+
async function findAvailablePort(startPort: number, maxTries: number = 100): Promise<number> {
|
|
163
|
+
let port = startPort;
|
|
164
|
+
for (let i = 0; i < maxTries; i++) {
|
|
165
|
+
if (await isPortAvailable(port)) {
|
|
166
|
+
return port;
|
|
167
|
+
}
|
|
168
|
+
port++;
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`无法在 ${startPort} 到 ${startPort + maxTries - 1} 范围内找到可用端口`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 检查特定端口是否有服务响应
|
|
174
|
+
async function checkServiceAtPort(port: number): Promise<boolean> {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
logger.info(`检查端口 ${port} 上的服务...`);
|
|
177
|
+
const req = http.get(`http://localhost:${port}`, (res) => {
|
|
178
|
+
// 收到响应,检查是否是我们的服务
|
|
179
|
+
let data = '';
|
|
180
|
+
res.on('data', (chunk) => {
|
|
181
|
+
data += chunk;
|
|
182
|
+
});
|
|
183
|
+
res.on('end', () => {
|
|
184
|
+
// 简单检查响应内容是否包含我们的服务特征
|
|
185
|
+
const isOurService = data.includes('文件转发服务') ||
|
|
186
|
+
data.includes('proxy') ||
|
|
187
|
+
res.headers['server']?.includes('proxy');
|
|
188
|
+
|
|
189
|
+
logger.info(`端口 ${port} 服务检查结果: ${isOurService ? '是我们的服务' : '不是我们的服务'}`);
|
|
190
|
+
resolve(isOurService);
|
|
191
|
+
});
|
|
192
|
+
}).on('error', (err) => {
|
|
193
|
+
logger.info(`连接到端口 ${port} 失败: ${err.message}`);
|
|
194
|
+
resolve(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
req.setTimeout(1000, () => {
|
|
198
|
+
logger.info(`连接到端口 ${port} 超时`);
|
|
199
|
+
req.destroy();
|
|
200
|
+
resolve(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class ProxyServiceManager {
|
|
206
|
+
private serverProcess: ChildProcess | null = null;
|
|
207
|
+
private isShuttingDown: boolean = false;
|
|
208
|
+
private exitTimeout: NodeJS.Timeout | null = null;
|
|
209
|
+
private actualPort: number | null = null;
|
|
210
|
+
private pid: number | null = null;
|
|
211
|
+
|
|
212
|
+
constructor() {
|
|
213
|
+
this.setupSignalHandlers();
|
|
214
|
+
this.setupExitHooks();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 获取实际端口
|
|
218
|
+
public getPort(): number | null {
|
|
219
|
+
return this.actualPort;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 获取进程ID
|
|
223
|
+
public getPid(): number | null {
|
|
224
|
+
return this.pid;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 列出所有活跃实例
|
|
228
|
+
public static listActiveInstances(): { pid: number, port: number }[] {
|
|
229
|
+
return getActiveInstances();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 启动服务
|
|
233
|
+
public async start(): Promise<number> {
|
|
234
|
+
// 先列出现有实例
|
|
235
|
+
const activeInstances = getActiveInstances();
|
|
236
|
+
if (activeInstances.length > 0) {
|
|
237
|
+
logger.info('当前活跃的文件转发服务实例:');
|
|
238
|
+
activeInstances.forEach(({ pid, port }) => {
|
|
239
|
+
logger.info(`- PID: ${pid}, 端口: ${port}`);
|
|
240
|
+
});
|
|
241
|
+
this.actualPort = activeInstances[0].port;
|
|
242
|
+
return this.actualPort;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 检查指定的端口是否可用
|
|
246
|
+
const portAvailable = await isPortAvailable(Number(config.port));
|
|
247
|
+
|
|
248
|
+
// 如果端口不可用,寻找可用端口
|
|
249
|
+
let finalPort = Number(config.port);
|
|
250
|
+
if (!portAvailable) {
|
|
251
|
+
logger.info(`端口 ${config.port} 不可用,寻找其他可用端口...`);
|
|
252
|
+
try {
|
|
253
|
+
finalPort = await findAvailablePort(Number(config.port) + 1);
|
|
254
|
+
logger.info(`找到可用端口: ${finalPort}`);
|
|
255
|
+
config.port = finalPort;
|
|
256
|
+
} catch (e) {
|
|
257
|
+
logger.error(`无法找到可用端口:`, e.message);
|
|
258
|
+
throw new Error(`无法启动服务: 找不到可用端口`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
logger.info(`启动文件转发服务在端口 ${finalPort}...`);
|
|
263
|
+
await this.startServer(finalPort);
|
|
264
|
+
this.actualPort = finalPort;
|
|
265
|
+
return finalPort;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 启动服务器进程
|
|
269
|
+
private async startServer(port: number): Promise<void> {
|
|
270
|
+
// 创建日志文件 - 使用端口号作为区分
|
|
271
|
+
const logFile = path.join(config.logDirectory, `proxy-${port}.log`);
|
|
272
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
273
|
+
|
|
274
|
+
// 添加时间戳到日志
|
|
275
|
+
const timestamp = new Date().toISOString();
|
|
276
|
+
logStream.write(`\n[${timestamp}] === 服务启动于端口 ${port} ===\n`);
|
|
277
|
+
|
|
278
|
+
// 设置环境变量
|
|
279
|
+
const env = {
|
|
280
|
+
...process.env,
|
|
281
|
+
PORT: port.toString()
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// 启动节点进程
|
|
285
|
+
this.serverProcess = spawn('node', [config.serverScript], {
|
|
286
|
+
env,
|
|
287
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
288
|
+
detached: false
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// 保存进程ID
|
|
292
|
+
const { pid } = this.serverProcess;
|
|
293
|
+
this.pid = pid;
|
|
294
|
+
logger.info(`文件转发服务进程 (PID: ${pid}) 已启动,监听端口 ${port}`);
|
|
295
|
+
|
|
296
|
+
// 创建锁文件
|
|
297
|
+
if (pid) {
|
|
298
|
+
createLockFile(pid, port);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 连接输出到日志文件和控制台
|
|
302
|
+
this.serverProcess.stdout?.pipe(logStream);
|
|
303
|
+
this.serverProcess.stderr?.pipe(logStream);
|
|
304
|
+
|
|
305
|
+
// 同时显示在控制台
|
|
306
|
+
this.serverProcess.stdout?.on('data', (data) => {
|
|
307
|
+
process.stdout.write(data);
|
|
308
|
+
});
|
|
309
|
+
this.serverProcess.stderr?.on('data', (data) => {
|
|
310
|
+
process.stderr.write(data);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// 处理服务器退出
|
|
314
|
+
this.serverProcess.on('exit', (code, signal) => {
|
|
315
|
+
logger.info(`服务进程 (PID: ${pid}) 退出,代码: ${code}, 信号: ${signal || 'none'}`);
|
|
316
|
+
|
|
317
|
+
// 删除锁文件
|
|
318
|
+
if (pid && port) {
|
|
319
|
+
removeLockFile(pid, port);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 关闭日志流
|
|
323
|
+
logStream.end();
|
|
324
|
+
|
|
325
|
+
// 清除进程引用
|
|
326
|
+
this.serverProcess = null;
|
|
327
|
+
|
|
328
|
+
// 如果不是正在关闭且进程异常退出,则重启服务
|
|
329
|
+
if (!this.isShuttingDown && code !== 0) {
|
|
330
|
+
logger.info('服务异常退出,2秒后重启...');
|
|
331
|
+
setTimeout(() => this.startServer(port), 2000);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// 处理错误
|
|
336
|
+
this.serverProcess.on('error', (err) => {
|
|
337
|
+
logger.error('服务进程启动错误:', err);
|
|
338
|
+
|
|
339
|
+
// 写入错误到日志
|
|
340
|
+
logStream.write(`[ERROR] 进程启动错误: ${err.message}\n`);
|
|
341
|
+
|
|
342
|
+
// 删除锁文件
|
|
343
|
+
if (pid && port) {
|
|
344
|
+
removeLockFile(pid, port);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// 等待服务器启动并检查是否可访问
|
|
349
|
+
let retries = 10;
|
|
350
|
+
logger.info(`等待服务在端口 ${port} 上响应...`);
|
|
351
|
+
|
|
352
|
+
while (retries > 0) {
|
|
353
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
354
|
+
const isResponding = await checkServiceAtPort(port);
|
|
355
|
+
if (isResponding) {
|
|
356
|
+
logger.info(`服务已在端口 ${port} 上成功启动并响应请求`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
retries--;
|
|
360
|
+
logger.info(`等待服务响应,剩余尝试次数: ${retries}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
logger.info(`警告: 服务进程已启动但未在预期时间内响应请求`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 关闭服务
|
|
367
|
+
public shutdown(): void {
|
|
368
|
+
if (this.isShuttingDown) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.isShuttingDown = true;
|
|
373
|
+
logger.info('正在关闭文件转发服务...');
|
|
374
|
+
|
|
375
|
+
// 清除之前的退出超时
|
|
376
|
+
if (this.exitTimeout) {
|
|
377
|
+
clearTimeout(this.exitTimeout);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 删除锁文件
|
|
381
|
+
if (this.pid && this.actualPort) {
|
|
382
|
+
removeLockFile(this.pid, this.actualPort);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!this.serverProcess) {
|
|
386
|
+
logger.info('没有运行中的服务进程');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 尝试优雅关闭
|
|
391
|
+
try {
|
|
392
|
+
logger.info(`发送终止信号到进程 PID: ${this.serverProcess.pid}`);
|
|
393
|
+
// 首先尝试SIGTERM
|
|
394
|
+
this.serverProcess.kill('SIGTERM');
|
|
395
|
+
|
|
396
|
+
// 设置强制终止超时
|
|
397
|
+
this.exitTimeout = setTimeout(() => {
|
|
398
|
+
if (this.serverProcess) {
|
|
399
|
+
logger.info('服务未能在时间内优雅关闭,强制终止');
|
|
400
|
+
try {
|
|
401
|
+
// 尝试SIGKILL
|
|
402
|
+
this.serverProcess.kill('SIGKILL');
|
|
403
|
+
} catch (err) {
|
|
404
|
+
logger.error('强制终止进程失败:', err);
|
|
405
|
+
}
|
|
406
|
+
// 额外保障:通过系统命令强制终止进程
|
|
407
|
+
this.forceKillProcess(this.serverProcess.pid);
|
|
408
|
+
}
|
|
409
|
+
// 清除引用
|
|
410
|
+
this.serverProcess = null;
|
|
411
|
+
}, 5000);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
logger.error('关闭服务时出错:', error);
|
|
414
|
+
// 如果常规方法失败,尝试系统级终止
|
|
415
|
+
if (this.serverProcess && this.serverProcess.pid) {
|
|
416
|
+
this.forceKillProcess(this.serverProcess.pid);
|
|
417
|
+
}
|
|
418
|
+
this.serverProcess = null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 使用系统命令强制终止进程
|
|
423
|
+
private forceKillProcess(pid: number): void {
|
|
424
|
+
try {
|
|
425
|
+
const isWin = process.platform === 'win32';
|
|
426
|
+
if (isWin) {
|
|
427
|
+
// Windows
|
|
428
|
+
spawn('taskkill', ['/F', '/PID', pid.toString()]);
|
|
429
|
+
} else {
|
|
430
|
+
// Unix/Linux/macOS
|
|
431
|
+
spawn('kill', ['-9', pid.toString()]);
|
|
432
|
+
}
|
|
433
|
+
logger.info(`已通过系统命令强制终止进程 PID: ${pid}`);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
logger.error(`使用系统命令终止进程 ${pid} 失败:`, err);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 设置信号处理
|
|
440
|
+
private setupSignalHandlers(): void {
|
|
441
|
+
// 处理系统信号
|
|
442
|
+
process.on('SIGINT', () => {
|
|
443
|
+
logger.info('收到 SIGINT 信号');
|
|
444
|
+
this.shutdown();
|
|
445
|
+
// 给进程一些时间来清理后强制退出
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}, 2000);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
process.on('SIGTERM', () => {
|
|
452
|
+
logger.info('收到 SIGTERM 信号');
|
|
453
|
+
this.shutdown();
|
|
454
|
+
setTimeout(() => {
|
|
455
|
+
process.exit(0);
|
|
456
|
+
}, 2000);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// 处理未捕获的异常
|
|
460
|
+
process.on('uncaughtException', (err) => {
|
|
461
|
+
logger.error('主进程未捕获的异常:', err);
|
|
462
|
+
this.shutdown();
|
|
463
|
+
setTimeout(() => {
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}, 2000);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 设置Node进程退出钩子
|
|
470
|
+
private setupExitHooks(): void {
|
|
471
|
+
// 在Node进程退出时确保子进程被终止
|
|
472
|
+
process.on('exit', () => {
|
|
473
|
+
logger.info('主进程退出,正在清理...');
|
|
474
|
+
// 删除锁文件
|
|
475
|
+
if (this.pid && this.actualPort) {
|
|
476
|
+
try {
|
|
477
|
+
const lockFilePath = getLockFilePath(this.pid, this.actualPort);
|
|
478
|
+
if (fs.existsSync(lockFilePath)) {
|
|
479
|
+
fs.unlinkSync(lockFilePath);
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
// 忽略清理锁文件的错误
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 直接使用process.exit()时,Node不会等待异步操作
|
|
487
|
+
// 所以我们需要在同步代码中尽可能清理
|
|
488
|
+
if (this.serverProcess && this.serverProcess.pid) {
|
|
489
|
+
try {
|
|
490
|
+
// 同步操作:尝试发送SIGKILL
|
|
491
|
+
this.serverProcess.kill('SIGKILL');
|
|
492
|
+
logger.info(`在主进程退出前终止了子进程 PID: ${this.serverProcess.pid}`);
|
|
493
|
+
} catch (e) {
|
|
494
|
+
// 无法处理,但至少我们尝试了
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// 处理未处理的Promise拒绝
|
|
500
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
501
|
+
logger.error('未处理的Promise拒绝:', reason);
|
|
502
|
+
// 不需要退出,但应记录下来
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|