@chenpu17/tcpmapping 2.1.1 → 2.4.1

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/bin/tcpmapping.js CHANGED
@@ -1,269 +1,538 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn, execSync } = require('child_process');
3
+ const { spawn } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
+ const http = require('http');
8
+ const net = require('net');
7
9
 
8
10
  const APP_NAME = 'tcpmapping';
9
- const DATA_DIR = path.join(os.homedir(), '.tcpmapping');
11
+ const DATA_DIR = process.env.TCPMAPPING_DATA_DIR || path.join(os.homedir(), '.tcpmapping');
10
12
  const PID_FILE = path.join(DATA_DIR, 'tcpmapping.pid');
11
13
  const LOG_FILE = path.join(DATA_DIR, 'tcpmapping.log');
12
14
  const SERVER_FILE = path.join(__dirname, '..', 'server.js');
13
15
  const SETUP_PASSWORD_FILE = path.join(__dirname, '..', 'setup-password.js');
14
16
 
15
- // 获取命令行参数
16
- const args = process.argv.slice(2);
17
+ // 解析命令行参数
18
+ function parseArgs(argv) {
19
+ const options = { port: null, host: null };
20
+ const positional = [];
21
+
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const arg = argv[i];
24
+
25
+ if (arg === '--port' || arg === '-p') {
26
+ options.port = argv[++i];
27
+ } else if (arg.startsWith('--port=')) {
28
+ options.port = arg.slice('--port='.length);
29
+ } else if (arg === '--host' || arg === '-H' || arg === '--listen') {
30
+ options.host = argv[++i];
31
+ } else if (arg.startsWith('--host=')) {
32
+ options.host = arg.slice('--host='.length);
33
+ } else if (arg.startsWith('--listen=')) {
34
+ options.host = arg.slice('--listen='.length);
35
+ } else {
36
+ positional.push(arg);
37
+ }
38
+ }
39
+
40
+ return { options, positional };
41
+ }
42
+
43
+ const { options: webuiOptions, positional: args } = parseArgs(process.argv.slice(2));
17
44
  const command = args[0] || 'help';
18
45
 
19
- // 颜色输出
46
+ if (webuiOptions.port !== null && !isValidPort(webuiOptions.port)) {
47
+ console.error(`无效的端口: ${webuiOptions.port}(应为 1-65535)`);
48
+ process.exit(1);
49
+ }
50
+
51
+ if (webuiOptions.host !== null && !isValidHost(webuiOptions.host)) {
52
+ console.error(`无效的监听地址: ${webuiOptions.host}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ // 颜色输出(非 TTY 或设置了 NO_COLOR 时自动关闭)
57
+ const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
58
+ const colorize = (code) => (text) => useColor ? `\x1b[${code}m${text}\x1b[0m` : String(text);
59
+
20
60
  const colors = {
21
- green: (text) => `\x1b[32m${text}\x1b[0m`,
22
- red: (text) => `\x1b[31m${text}\x1b[0m`,
23
- yellow: (text) => `\x1b[33m${text}\x1b[0m`,
24
- blue: (text) => `\x1b[34m${text}\x1b[0m`,
25
- cyan: (text) => `\x1b[36m${text}\x1b[0m`,
26
- gray: (text) => `\x1b[90m${text}\x1b[0m`
61
+ green: colorize(32),
62
+ red: colorize(31),
63
+ yellow: colorize(33),
64
+ blue: colorize(34),
65
+ cyan: colorize(36),
66
+ gray: colorize(90)
27
67
  };
28
68
 
69
+ function delay(ms) {
70
+ return new Promise(resolve => setTimeout(resolve, ms));
71
+ }
72
+
29
73
  // 确保数据目录存在
30
74
  function ensureDataDir() {
31
75
  if (!fs.existsSync(DATA_DIR)) {
32
- fs.mkdirSync(DATA_DIR, { recursive: true });
76
+ fs.mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
33
77
  }
34
78
  }
35
79
 
80
+ // 读取配置文件
81
+ function readConfig() {
82
+ const candidates = [
83
+ path.join(process.cwd(), 'config.json'),
84
+ path.join(DATA_DIR, 'config.json')
85
+ ];
86
+
87
+ for (const configPath of candidates) {
88
+ try {
89
+ if (fs.existsSync(configPath)) {
90
+ return { path: configPath, data: JSON.parse(fs.readFileSync(configPath, 'utf8')) };
91
+ }
92
+ } catch (error) {
93
+ console.warn(colors.yellow(`警告: 配置文件解析失败 (${configPath}): ${error.message}`));
94
+ }
95
+ }
96
+
97
+ return { path: null, data: null };
98
+ }
99
+
36
100
  // 获取配置文件路径(按优先级)
37
101
  function getConfigPath() {
38
- // 优先级1: 当前工作目录
39
- const cwdPath = path.join(process.cwd(), 'config.json');
40
- if (fs.existsSync(cwdPath)) {
41
- return cwdPath;
42
- }
102
+ return readConfig().path;
103
+ }
43
104
 
44
- // 优先级2: 用户数据目录
45
- const dataPath = path.join(DATA_DIR, 'config.json');
46
- if (fs.existsSync(dataPath)) {
47
- return dataPath;
105
+ // 获取认证配置文件路径
106
+ function getAuthConfigPath() {
107
+ const candidates = [
108
+ path.join(process.cwd(), 'auth-config.json'),
109
+ path.join(DATA_DIR, 'auth-config.json')
110
+ ];
111
+
112
+ for (const authPath of candidates) {
113
+ if (fs.existsSync(authPath)) {
114
+ return authPath;
115
+ }
48
116
  }
49
117
 
50
118
  return null;
51
119
  }
52
120
 
53
- // 获取认证配置文件路径
54
- function getAuthConfigPath() {
55
- const cwdPath = path.join(process.cwd(), 'auth-config.json');
56
- if (fs.existsSync(cwdPath)) {
57
- return cwdPath;
121
+ // 验证端口
122
+ function isValidPort(port) {
123
+ const p = Number(port);
124
+ return Number.isInteger(p) && p >= 1 && p <= 65535;
125
+ }
126
+
127
+ // 验证 IP 地址或主机名(与 server.js 保持一致)
128
+ function isValidHost(host) {
129
+ if (!host || typeof host !== 'string') return false;
130
+
131
+ const value = host.trim();
132
+ if (!value || value.length > 253) return false;
133
+ if (net.isIP(value)) return true;
134
+
135
+ const hostnamePattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
136
+ return hostnamePattern.test(value);
137
+ }
138
+
139
+ // 获取 WebUI 访问信息(优先级:命令行参数 > 环境变量 > 配置文件)
140
+ function getWebuiInfo() {
141
+ const config = readConfig().data;
142
+ const webui = (config && config.webui) || {};
143
+
144
+ let host = webui.host || '127.0.0.1';
145
+ let port = Number(webui.port) || 9999;
146
+
147
+ if (process.env.WEBUI_PORT && isValidPort(process.env.WEBUI_PORT)) {
148
+ port = Number(process.env.WEBUI_PORT);
149
+ }
150
+ if (process.env.WEBUI_HOST && isValidHost(process.env.WEBUI_HOST)) {
151
+ host = process.env.WEBUI_HOST.trim();
58
152
  }
59
153
 
60
- const dataPath = path.join(DATA_DIR, 'auth-config.json');
61
- if (fs.existsSync(dataPath)) {
62
- return dataPath;
154
+ // 命令行参数优先级最高
155
+ if (webuiOptions.port !== null) {
156
+ port = Number(webuiOptions.port);
157
+ }
158
+ if (webuiOptions.host !== null) {
159
+ host = webuiOptions.host.trim();
63
160
  }
64
161
 
65
- return null;
162
+ return {
163
+ enabled: webui.enabled !== false,
164
+ host,
165
+ port
166
+ };
167
+ }
168
+
169
+ // 将监听地址转换为可访问地址
170
+ function getAccessHost(host) {
171
+ if (host === '0.0.0.0' || host === '::' || !host) {
172
+ return 'localhost';
173
+ }
174
+ if (host.includes(':')) {
175
+ return `[${host}]`;
176
+ }
177
+ return host;
178
+ }
179
+
180
+ // 列出可访问地址(监听所有网卡时包含本机局域网 IP,便于远程访问)
181
+ function getAccessHosts(host) {
182
+ if (host !== '0.0.0.0' && host !== '::') {
183
+ return [getAccessHost(host)];
184
+ }
185
+
186
+ const hosts = ['localhost'];
187
+ const interfaces = os.networkInterfaces();
188
+ for (const entries of Object.values(interfaces)) {
189
+ for (const entry of entries || []) {
190
+ if (entry.family === 'IPv4' && !entry.internal) {
191
+ hosts.push(entry.address);
192
+ }
193
+ }
194
+ }
195
+ return hosts;
196
+ }
197
+
198
+ // 检查进程是否存在
199
+ function isAlive(pid) {
200
+ try {
201
+ process.kill(pid, 0);
202
+ return true;
203
+ } catch (error) {
204
+ // 无权限说明进程存在(属于其他用户)
205
+ return error.code === 'EPERM';
206
+ }
66
207
  }
67
208
 
68
209
  // 检查进程是否运行
69
210
  function isRunning() {
70
211
  ensureDataDir();
212
+
71
213
  if (!fs.existsSync(PID_FILE)) {
72
- return false;
214
+ return null;
73
215
  }
74
216
 
75
- const pid = fs.readFileSync(PID_FILE, 'utf8').trim();
217
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
218
+
219
+ if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) {
220
+ return pid;
221
+ }
76
222
 
223
+ // 进程不存在,清理过期的 PID 文件
77
224
  try {
78
- process.kill(parseInt(pid), 0);
79
- return parseInt(pid);
80
- } catch (e) {
81
- // 进程不存在,清理PID文件
82
225
  fs.unlinkSync(PID_FILE);
83
- return false;
226
+ } catch (error) {
227
+ // 忽略清理失败
228
+ }
229
+ return null;
230
+ }
231
+
232
+ // 读取日志末尾内容
233
+ function readLogTail(lineCount) {
234
+ try {
235
+ if (!fs.existsSync(LOG_FILE)) {
236
+ return null;
237
+ }
238
+
239
+ const stat = fs.statSync(LOG_FILE);
240
+ const maxBytes = 64 * 1024;
241
+ const size = Math.min(stat.size, maxBytes);
242
+ if (size === 0) {
243
+ return '';
244
+ }
245
+
246
+ const buffer = Buffer.alloc(size);
247
+ const fd = fs.openSync(LOG_FILE, 'r');
248
+ try {
249
+ fs.readSync(fd, buffer, 0, size, stat.size - size);
250
+ } finally {
251
+ fs.closeSync(fd);
252
+ }
253
+
254
+ return buffer.toString('utf8').split('\n').slice(-lineCount).join('\n');
255
+ } catch (error) {
256
+ return null;
84
257
  }
85
258
  }
86
259
 
87
260
  // 启动服务
88
- function start() {
261
+ async function start() {
89
262
  const pid = isRunning();
90
263
  if (pid) {
91
- console.log(colors.yellow(`${APP_NAME} is already running (PID: ${pid})`));
264
+ console.log(colors.yellow(`${APP_NAME} 已在运行 (PID: ${pid})`));
265
+ console.log(colors.gray('如需修改端口或监听地址,请使用: tcpmapping restart --port <port> --host <host>'));
92
266
  return;
93
267
  }
94
268
 
95
- // 检查是否已初始化
96
- const configPath = getConfigPath();
97
- const authPath = getAuthConfigPath();
98
-
99
- if (!authPath) {
269
+ if (!getAuthConfigPath()) {
100
270
  console.log(colors.yellow('未设置管理员密码,请先运行:'));
101
271
  console.log(colors.cyan(' tcpmapping setup-password'));
102
272
  return;
103
273
  }
104
274
 
105
275
  ensureDataDir();
106
- console.log(colors.blue(`Starting ${APP_NAME}...`));
276
+ console.log(colors.blue(`正在启动 ${APP_NAME}...`));
277
+
278
+ // 将生效的 WebUI 地址显式传给子进程,确保实际监听与提示一致
279
+ const webui = getWebuiInfo();
107
280
 
108
281
  const out = fs.openSync(LOG_FILE, 'a');
109
282
  const err = fs.openSync(LOG_FILE, 'a');
110
283
 
111
- const child = spawn('node', [SERVER_FILE], {
112
- detached: true,
113
- stdio: ['ignore', out, err],
114
- cwd: process.cwd() // 使用当前目录,以便server可以找到当前目录的config
115
- });
284
+ let child;
285
+ try {
286
+ child = spawn(process.execPath, [SERVER_FILE], {
287
+ detached: true,
288
+ stdio: ['ignore', out, err],
289
+ // 使用当前目录,以便 server 可以找到当前目录的 config.json
290
+ cwd: process.cwd(),
291
+ env: {
292
+ ...process.env,
293
+ WEBUI_HOST: webui.host,
294
+ WEBUI_PORT: String(webui.port)
295
+ }
296
+ });
297
+ } finally {
298
+ // 子进程已继承文件描述符,父进程可以关闭
299
+ fs.closeSync(out);
300
+ fs.closeSync(err);
301
+ }
116
302
 
117
- fs.writeFileSync(PID_FILE, child.pid.toString());
303
+ fs.writeFileSync(PID_FILE, String(child.pid));
118
304
  child.unref();
119
305
 
120
- console.log(colors.green(`${APP_NAME} started successfully (PID: ${child.pid})`));
121
- console.log(colors.cyan(`WebUI: http://localhost:9999`));
122
- console.log(colors.gray(`Log file: ${LOG_FILE}`));
306
+ // 短暂等待,捕获启动即失败的情况(如端口被占用、配置错误)
307
+ await delay(500);
308
+
309
+ if (!isAlive(child.pid)) {
310
+ try {
311
+ fs.unlinkSync(PID_FILE);
312
+ } catch (error) {
313
+ // 忽略
314
+ }
315
+ console.log(colors.red(`${APP_NAME} 启动失败`));
316
+ const tail = readLogTail(15);
317
+ if (tail) {
318
+ console.log(colors.gray('最近日志:'));
319
+ console.log(tail);
320
+ }
321
+ console.log(colors.gray(`完整日志: ${LOG_FILE}`));
322
+ process.exitCode = 1;
323
+ return;
324
+ }
325
+
326
+ console.log(colors.green(`${APP_NAME} 启动成功 (PID: ${child.pid})`));
327
+ if (webui.enabled) {
328
+ const urls = getAccessHosts(webui.host).map(host => `http://${host}:${webui.port}`);
329
+ console.log(colors.cyan(`WebUI: ${urls.join('\n ')}`));
330
+ if (webui.host === '0.0.0.0' || webui.host === '::') {
331
+ console.log(colors.yellow('提示: WebUI 监听在所有网卡上,请确保使用强密码'));
332
+ }
333
+ }
334
+ console.log(colors.gray(`日志文件: ${LOG_FILE}`));
335
+ const configPath = getConfigPath();
123
336
  if (configPath) {
124
- console.log(colors.gray(`Config: ${configPath}`));
337
+ console.log(colors.gray(`配置文件: ${configPath}`));
125
338
  }
126
339
  }
127
340
 
128
341
  // 停止服务
129
- function stop() {
342
+ async function stop() {
130
343
  const pid = isRunning();
131
344
  if (!pid) {
132
- console.log(colors.yellow(`${APP_NAME} is not running`));
345
+ console.log(colors.yellow(`${APP_NAME} 未在运行`));
133
346
  return;
134
347
  }
135
348
 
136
- console.log(colors.blue(`Stopping ${APP_NAME} (PID: ${pid})...`));
349
+ console.log(colors.blue(`正在停止 ${APP_NAME} (PID: ${pid})...`));
137
350
 
138
351
  try {
139
352
  process.kill(pid, 'SIGTERM');
140
353
 
141
- // 等待进程退出
354
+ // 最多等待 5 秒,让服务优雅退出
142
355
  let attempts = 0;
143
- while (attempts < 10) {
356
+ while (attempts < 20 && isAlive(pid)) {
357
+ await delay(250);
358
+ attempts++;
359
+ }
360
+
361
+ // 仍未退出则强制终止
362
+ if (isAlive(pid)) {
363
+ console.log(colors.yellow('服务未在预期时间内退出,正在强制终止...'));
144
364
  try {
145
- process.kill(pid, 0);
146
- // 进程仍在运行
147
- require('child_process').execSync('sleep 0.5');
148
- attempts++;
149
- } catch (e) {
150
- // 进程已退出
151
- break;
365
+ process.kill(pid, 'SIGKILL');
366
+ } catch (error) {
367
+ // 进程可能已退出
152
368
  }
369
+ await delay(200);
153
370
  }
154
371
 
155
- // 如果进程仍在运行,强制终止
156
372
  try {
157
- process.kill(pid, 0);
158
- process.kill(pid, 'SIGKILL');
159
- } catch (e) {
160
- // 进程已退出
161
- }
162
-
163
- if (fs.existsSync(PID_FILE)) {
164
373
  fs.unlinkSync(PID_FILE);
374
+ } catch (error) {
375
+ // 忽略
165
376
  }
166
377
 
167
- console.log(colors.green(`${APP_NAME} stopped successfully`));
168
- } catch (e) {
169
- console.log(colors.red(`Failed to stop ${APP_NAME}: ${e.message}`));
378
+ console.log(colors.green(`${APP_NAME} 已停止`));
379
+ } catch (error) {
380
+ console.log(colors.red(`停止 ${APP_NAME} 失败: ${error.message}`));
170
381
  }
171
382
  }
172
383
 
173
384
  // 重启服务
174
- function restart() {
175
- console.log(colors.blue(`Restarting ${APP_NAME}...`));
176
- stop();
177
- setTimeout(() => {
178
- start();
179
- }, 1000);
385
+ async function restart() {
386
+ console.log(colors.blue(`正在重启 ${APP_NAME}...`));
387
+ await stop();
388
+ await delay(300);
389
+ await start();
390
+ }
391
+
392
+ // 请求健康检查接口
393
+ function fetchHealth(host, port) {
394
+ return new Promise((resolve) => {
395
+ const request = http.get({
396
+ host: host,
397
+ port: port,
398
+ path: '/api/health',
399
+ timeout: 2000
400
+ }, (res) => {
401
+ let data = '';
402
+ res.on('data', chunk => data += chunk);
403
+ res.on('end', () => {
404
+ try {
405
+ resolve(JSON.parse(data));
406
+ } catch (error) {
407
+ resolve(null);
408
+ }
409
+ });
410
+ });
411
+
412
+ request.on('error', () => resolve(null));
413
+ request.on('timeout', () => {
414
+ request.destroy();
415
+ resolve(null);
416
+ });
417
+ });
180
418
  }
181
419
 
182
420
  // 查看状态
183
- function status() {
421
+ async function status() {
184
422
  const pid = isRunning();
185
423
  const configPath = getConfigPath();
186
424
  const authPath = getAuthConfigPath();
425
+ const webui = getWebuiInfo();
187
426
 
188
427
  console.log(colors.cyan('=== TcpMapping Status ===\n'));
189
428
 
190
- // 服务状态
191
429
  if (pid) {
192
- console.log(colors.green(`Service: Running (PID: ${pid})`));
193
-
194
- // 尝试获取健康状态
195
- try {
196
- const http = require('http');
197
- const req = http.get('http://localhost:9999/api/health', { timeout: 2000 }, (res) => {
198
- let data = '';
199
- res.on('data', chunk => data += chunk);
200
- res.on('end', () => {
201
- try {
202
- const health = JSON.parse(data);
203
- console.log(` Uptime: ${Math.floor(health.uptime / 60)} minutes`);
204
- console.log(` Active Mappings: ${health.activeMappings}`);
205
- console.log(` Active Connections: ${health.activeConnections}`);
206
- } catch (e) {}
207
- showPaths();
208
- });
209
- });
210
- req.on('error', () => showPaths());
211
- req.on('timeout', () => { req.destroy(); showPaths(); });
212
- } catch (e) {
213
- showPaths();
430
+ console.log(colors.green(`服务状态: 运行中 (PID: ${pid})`));
431
+
432
+ if (webui.enabled) {
433
+ const host = webui.host === '0.0.0.0' || webui.host === '::' ? '127.0.0.1' : webui.host;
434
+ const health = await fetchHealth(host, webui.port);
435
+
436
+ if (health) {
437
+ console.log(` WebUI: ${getAccessHosts(webui.host).map(item => `http://${item}:${webui.port}`).join(' ')}`);
438
+ console.log(` 运行时间: ${Math.floor(health.uptime / 60)} 分钟`);
439
+ console.log(` 活跃映射: ${health.activeMappings}`);
440
+ console.log(` 活跃连接: ${health.activeConnections}`);
441
+ } else {
442
+ console.log(colors.yellow(` 健康检查失败: http://${getAccessHost(webui.host)}:${webui.port}/api/health`));
443
+ }
444
+ } else {
445
+ console.log(colors.gray(' WebUI: 已禁用'));
214
446
  }
215
447
  } else {
216
- console.log(colors.red('Service: Stopped'));
217
- showPaths();
448
+ console.log(colors.red('服务状态: 已停止'));
218
449
  }
219
450
 
220
- function showPaths() {
221
- console.log('');
222
- console.log(colors.cyan('=== Configuration ===\n'));
223
- console.log(`Data Dir: ${DATA_DIR}`);
224
- console.log(`Config: ${configPath || colors.yellow('Not found (run: tcpmapping init)')}`);
225
- console.log(`Auth: ${authPath || colors.yellow('Not found (run: tcpmapping setup-password)')}`);
226
- console.log(`Log: ${LOG_FILE}`);
227
- }
451
+ console.log('');
452
+ console.log(colors.cyan('=== Configuration ===\n'));
453
+ console.log(`数据目录: ${DATA_DIR}`);
454
+ console.log(`配置文件: ${configPath || colors.yellow('未找到 (运行: tcpmapping init)')}`);
455
+ console.log(`认证配置: ${authPath || colors.yellow('未找到 (运行: tcpmapping setup-password)')}`);
456
+ console.log(`日志文件: ${LOG_FILE}`);
228
457
  }
229
458
 
230
459
  // 查看日志
231
460
  function logs(follow = false) {
232
461
  ensureDataDir();
462
+
233
463
  if (!fs.existsSync(LOG_FILE)) {
234
- console.log(colors.yellow('No log file found'));
464
+ console.log(colors.yellow('未找到日志文件'));
235
465
  return;
236
466
  }
237
467
 
238
- if (follow) {
239
- console.log(colors.blue(`Tailing ${LOG_FILE}... (Ctrl+C to exit)`));
240
- const tail = spawn('tail', ['-f', LOG_FILE], { stdio: 'inherit' });
241
- process.on('SIGINT', () => {
242
- tail.kill();
243
- process.exit(0);
244
- });
245
- } else {
246
- const content = fs.readFileSync(LOG_FILE, 'utf8');
247
- const lines = content.split('\n').slice(-50);
248
- console.log(lines.join('\n'));
468
+ if (!follow) {
469
+ const tail = readLogTail(50);
470
+ if (tail !== null) {
471
+ console.log(tail);
472
+ }
473
+ return;
249
474
  }
475
+
476
+ console.log(colors.blue(`正在跟踪 ${LOG_FILE}... (Ctrl+C 退出)`));
477
+
478
+ const tail = readLogTail(20);
479
+ if (tail) {
480
+ console.log(tail);
481
+ }
482
+
483
+ let offset = fs.statSync(LOG_FILE).size;
484
+
485
+ // 使用轮询方式,兼容 Windows / macOS / Linux
486
+ const watcher = (current) => {
487
+ if (current.size < offset) {
488
+ // 文件被截断(例如日志轮转)
489
+ offset = 0;
490
+ }
491
+
492
+ if (current.size > offset) {
493
+ try {
494
+ const length = current.size - offset;
495
+ const buffer = Buffer.alloc(length);
496
+ const fd = fs.openSync(LOG_FILE, 'r');
497
+ try {
498
+ fs.readSync(fd, buffer, 0, length, offset);
499
+ } finally {
500
+ fs.closeSync(fd);
501
+ }
502
+ process.stdout.write(buffer);
503
+ offset = current.size;
504
+ } catch (error) {
505
+ // 文件可能在读取过程中被轮转,忽略本次读取
506
+ }
507
+ }
508
+ };
509
+
510
+ fs.watchFile(LOG_FILE, { interval: 500 }, watcher);
511
+
512
+ const cleanup = () => {
513
+ fs.unwatchFile(LOG_FILE, watcher);
514
+ process.exit(0);
515
+ };
516
+
517
+ process.on('SIGINT', cleanup);
518
+ process.on('SIGTERM', cleanup);
250
519
  }
251
520
 
252
521
  // 设置密码
253
522
  function setupPassword() {
254
523
  ensureDataDir();
255
- console.log(colors.blue('Setting up password...'));
256
- console.log(colors.gray(`Config will be saved to: ${DATA_DIR}`));
524
+ console.log(colors.blue('正在设置管理员密码...'));
525
+ console.log(colors.gray(`配置将保存到: ${DATA_DIR}`));
257
526
  console.log('');
258
527
 
259
- const child = spawn('node', [SETUP_PASSWORD_FILE], {
528
+ const child = spawn(process.execPath, [SETUP_PASSWORD_FILE], {
260
529
  stdio: 'inherit',
261
- cwd: DATA_DIR, // 在数据目录运行,使auth-config.json保存到数据目录
530
+ cwd: DATA_DIR, // 在数据目录运行,使 auth-config.json 保存到数据目录
262
531
  env: { ...process.env, TCPMAPPING_DATA_DIR: DATA_DIR }
263
532
  });
264
533
 
265
534
  child.on('exit', (code) => {
266
- process.exit(code);
535
+ process.exit(code === null ? 1 : code);
267
536
  });
268
537
  }
269
538
 
@@ -271,39 +540,47 @@ function setupPassword() {
271
540
  function init() {
272
541
  ensureDataDir();
273
542
 
274
- // 检查当前目录和数据目录是否已有配置
275
543
  const cwdConfig = path.join(process.cwd(), 'config.json');
276
544
  const dataConfig = path.join(DATA_DIR, 'config.json');
277
545
 
278
546
  if (fs.existsSync(cwdConfig)) {
279
- console.log(colors.yellow(`config.json already exists in current directory`));
547
+ console.log(colors.yellow('当前目录已存在 config.json'));
280
548
  console.log(colors.gray(` ${cwdConfig}`));
281
549
  return;
282
550
  }
283
551
 
284
552
  if (fs.existsSync(dataConfig)) {
285
- console.log(colors.yellow(`config.json already exists in data directory`));
553
+ console.log(colors.yellow('数据目录已存在 config.json'));
286
554
  console.log(colors.gray(` ${dataConfig}`));
287
555
  console.log('');
288
- console.log('To create a local config, copy it to current directory:');
556
+ console.log('如需为当前项目创建独立配置,可复制一份:');
289
557
  console.log(colors.cyan(` cp ${dataConfig} ./config.json`));
290
558
  return;
291
559
  }
292
560
 
293
561
  const defaultConfig = {
294
562
  mappings: [],
295
- webui: { port: 9999 },
296
- logging: { level: "info" }
563
+ webui: {
564
+ enabled: true,
565
+ // 默认只监听本机;如需远程访问管理面板,请使用 --host 0.0.0.0 并配置防火墙
566
+ host: webuiOptions.host ? webuiOptions.host.trim() : '127.0.0.1',
567
+ port: webuiOptions.port !== null ? Number(webuiOptions.port) : 9999
568
+ },
569
+ logging: {
570
+ enabled: true,
571
+ level: 'info'
572
+ },
573
+ timeouts: {
574
+ connect: 30000,
575
+ idle: 300000
576
+ }
297
577
  };
298
578
 
299
- // 保存到数据目录
300
579
  fs.writeFileSync(dataConfig, JSON.stringify(defaultConfig, null, 2));
301
- console.log(colors.green('Created config.json'));
580
+ console.log(colors.green('已创建 config.json'));
302
581
  console.log(colors.gray(` ${dataConfig}`));
303
582
  console.log('');
304
- console.log('Edit the config file to add your port mappings.');
305
- console.log('Or copy it to current directory for project-specific config:');
306
- console.log(colors.cyan(` cp ${dataConfig} ./config.json`));
583
+ console.log('请编辑配置文件添加端口映射,或启动后通过 WebUI 添加。');
307
584
  }
308
585
 
309
586
  // 显示版本
@@ -317,52 +594,75 @@ function help() {
317
594
  console.log(`
318
595
  ${colors.cyan('TCP Mapping Server')} - TCP端口转发服务器
319
596
 
320
- ${colors.yellow('Usage:')}
321
- tcpmapping <command>
322
-
323
- ${colors.yellow('Commands:')}
324
- ${colors.green('start')} Start the server in background
325
- ${colors.green('stop')} Stop the server
326
- ${colors.green('restart')} Restart the server
327
- ${colors.green('status')} Show server status and config paths
328
- ${colors.green('logs')} Show recent logs
329
- ${colors.green('logs -f')} Follow logs in real-time
330
- ${colors.green('setup-password')} Set up admin password
331
- ${colors.green('init')} Create default config.json
332
- ${colors.green('version')} Show version
333
- ${colors.green('help')} Show this help message
334
-
335
- ${colors.yellow('Config Paths (priority order):')}
336
- 1. ./config.json (current directory - project config)
337
- 2. ~/.tcpmapping/ (user data directory - global config)
338
-
339
- ${colors.yellow('Examples:')}
340
- tcpmapping init # Create config file
341
- tcpmapping setup-password # Set admin password
342
- tcpmapping start # Start server
343
- tcpmapping status # Check status and config paths
597
+ ${colors.yellow('用法:')}
598
+ tcpmapping <command> [options]
599
+
600
+ ${colors.yellow('命令:')}
601
+ ${colors.green('start')} 后台启动服务
602
+ ${colors.green('stop')} 停止服务
603
+ ${colors.green('restart')} 重启服务
604
+ ${colors.green('status')} 查看服务状态和配置路径
605
+ ${colors.green('logs')} 查看最近日志
606
+ ${colors.green('logs -f')} 实时跟踪日志
607
+ ${colors.green('setup-password')} 设置管理员密码
608
+ ${colors.green('init')} 创建默认 config.json
609
+ ${colors.green('version')} 显示版本
610
+ ${colors.green('help')} 显示帮助
611
+
612
+ ${colors.yellow('选项:')}
613
+ ${colors.green('-p, --port <port>')} WebUI 端口(默认 9999)
614
+ ${colors.green('-H, --host <host>')} 监听地址(默认 127.0.0.1;公网访问用 0.0.0.0,别名 --listen)
615
+
616
+ 选项对 start / restart / status / init 生效,优先级高于环境变量和配置文件。
617
+
618
+ ${colors.yellow('配置路径(按优先级):')}
619
+ 1. ./config.json 当前目录(项目配置)
620
+ 2. ~/.tcpmapping/ 用户数据目录(全局配置)
621
+
622
+ ${colors.yellow('环境变量:')}
623
+ WEBUI_PORT / WEBUI_HOST 等效于 --port / --host
624
+
625
+ ${colors.yellow('示例:')}
626
+ tcpmapping init --host 0.0.0.0 --port 5000 # 初始化并允许远程访问
627
+ tcpmapping setup-password # 设置管理员密码
628
+ tcpmapping start --host 0.0.0.0 --port 5000 # 对外监听 5000 端口
629
+ tcpmapping status --port 5000 # 查看 5000 端口上的服务状态
630
+ tcpmapping restart --port 5000 # 以新端口重启
344
631
 
345
632
  ${colors.yellow('WebUI:')}
346
- After starting, visit http://localhost:9999
633
+ 默认访问 http://localhost:9999;使用 --host 0.0.0.0 后可通过公网 IP 访问,
634
+ 请务必设置强密码,并建议配合防火墙或 HTTPS 反向代理使用。
347
635
  `);
348
636
  }
349
637
 
350
638
  // 执行命令
351
639
  switch (command) {
352
640
  case 'start':
353
- start();
641
+ start().catch(error => {
642
+ console.error(colors.red(`启动失败: ${error.message}`));
643
+ process.exitCode = 1;
644
+ });
354
645
  break;
355
646
  case 'stop':
356
- stop();
647
+ stop().catch(error => {
648
+ console.error(colors.red(`停止失败: ${error.message}`));
649
+ process.exitCode = 1;
650
+ });
357
651
  break;
358
652
  case 'restart':
359
- restart();
653
+ restart().catch(error => {
654
+ console.error(colors.red(`重启失败: ${error.message}`));
655
+ process.exitCode = 1;
656
+ });
360
657
  break;
361
658
  case 'status':
362
- status();
659
+ status().catch(error => {
660
+ console.error(colors.red(`获取状态失败: ${error.message}`));
661
+ process.exitCode = 1;
662
+ });
363
663
  break;
364
664
  case 'logs':
365
- logs(args[1] === '-f');
665
+ logs(args[1] === '-f' || args[1] === '--follow');
366
666
  break;
367
667
  case 'setup-password':
368
668
  case 'passwd':