cc-viewer 1.0.2 → 1.0.4

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.
@@ -1,10 +1,10 @@
1
- import { createServer, request as httpRequest } from 'node:http';
2
- import { readFileSync, existsSync, watchFile, unwatchFile, statSync, writeFileSync, unlinkSync, readdirSync, openSync, closeSync } from 'node:fs';
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join, extname, basename } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
- import { LOG_FILE } from '../interceptor.js';
7
- import { t } from '../i18n.js';
6
+ import { LOG_FILE } from './interceptor.js';
7
+ import { t } from './i18n.js';
8
8
 
9
9
  const LOG_DIR = join(homedir(), '.claude', 'cc-viewer');
10
10
  const SHOW_ALL_FILE = '/tmp/cc-viewer-show-all';
@@ -14,51 +14,12 @@ const __dirname = dirname(__filename);
14
14
  const START_PORT = 7008;
15
15
  const MAX_PORT = 7099;
16
16
  const HOST = '127.0.0.1';
17
- const PORT_FILE = '/tmp/cc-viewer-port';
18
- const LOCK_FILE = '/tmp/cc-viewer-lock';
19
-
20
- function acquireLock() {
21
- try {
22
- // wx flag: exclusive create, fails if file already exists
23
- const fd = openSync(LOCK_FILE, 'wx');
24
- writeFileSync(fd, String(process.pid));
25
- closeSync(fd);
26
- return true;
27
- } catch {
28
- // 检查锁文件是否过期(超过 10 秒视为过期)
29
- try {
30
- const stat = statSync(LOCK_FILE);
31
- if (Date.now() - stat.mtimeMs > 10000) {
32
- unlinkSync(LOCK_FILE);
33
- const fd = openSync(LOCK_FILE, 'wx');
34
- writeFileSync(fd, String(process.pid));
35
- closeSync(fd);
36
- return true;
37
- }
38
- } catch {}
39
- return false;
40
- }
41
- }
42
-
43
- function releaseLock() {
44
- try { unlinkSync(LOCK_FILE); } catch {}
45
- }
46
-
47
- function checkPortAlive(port) {
48
- return new Promise((resolve) => {
49
- const req = httpRequest({ host: HOST, port, path: '/api/requests', method: 'GET', timeout: 1000 }, (res) => {
50
- res.resume();
51
- resolve(true);
52
- });
53
- req.on('error', () => resolve(false));
54
- req.on('timeout', () => { req.destroy(); resolve(false); });
55
- req.end();
56
- });
57
- }
58
17
 
59
18
  let clients = [];
60
19
  let server;
61
20
  let actualPort = START_PORT;
21
+ // 跟踪所有被 watch 的日志文件
22
+ const watchedFiles = new Map();
62
23
 
63
24
  const MIME_TYPES = {
64
25
  '.html': 'text/html; charset=utf-8',
@@ -103,11 +64,13 @@ function sendToClients(entry) {
103
64
  });
104
65
  }
105
66
 
106
- function startWatching() {
67
+ function watchLogFile(logFile) {
68
+ if (watchedFiles.has(logFile)) return;
107
69
  let lastSize = 0;
108
- watchFile(LOG_FILE, { interval: 500 }, () => {
70
+ watchedFiles.set(logFile, true);
71
+ watchFile(logFile, { interval: 500 }, () => {
109
72
  try {
110
- const content = readFileSync(LOG_FILE, 'utf-8');
73
+ const content = readFileSync(logFile, 'utf-8');
111
74
  const newContent = content.slice(lastSize);
112
75
  lastSize = content.length;
113
76
 
@@ -128,6 +91,10 @@ function startWatching() {
128
91
  });
129
92
  }
130
93
 
94
+ function startWatching() {
95
+ watchLogFile(LOG_FILE);
96
+ }
97
+
131
98
  function handleRequest(req, res) {
132
99
  const { url, method } = req;
133
100
 
@@ -142,6 +109,29 @@ function handleRequest(req, res) {
142
109
  return;
143
110
  }
144
111
 
112
+ // 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
113
+ if (url === '/api/register-log' && method === 'POST') {
114
+ let body = '';
115
+ req.on('data', chunk => { body += chunk; });
116
+ req.on('end', () => {
117
+ try {
118
+ const { logFile } = JSON.parse(body);
119
+ if (logFile && typeof logFile === 'string' && logFile.startsWith(LOG_DIR) && existsSync(logFile)) {
120
+ watchLogFile(logFile);
121
+ res.writeHead(200, { 'Content-Type': 'application/json' });
122
+ res.end(JSON.stringify({ ok: true }));
123
+ } else {
124
+ res.writeHead(400, { 'Content-Type': 'application/json' });
125
+ res.end(JSON.stringify({ error: 'Invalid log file path' }));
126
+ }
127
+ } catch {
128
+ res.writeHead(400, { 'Content-Type': 'application/json' });
129
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
130
+ }
131
+ });
132
+ return;
133
+ }
134
+
145
135
  // SSE endpoint
146
136
  if (url === '/events' && method === 'GET') {
147
137
  res.writeHead(200, {
@@ -297,88 +287,42 @@ function handleRequest(req, res) {
297
287
  }
298
288
 
299
289
  export async function startViewer() {
300
- // 尝试获取文件锁,防止多个进程同时启动服务器
301
- if (!acquireLock()) {
302
- // 另一个进程正在启动,等待它完成后复用
303
- await new Promise(resolve => setTimeout(resolve, 2000));
304
- if (existsSync(PORT_FILE)) {
305
- try {
306
- const existingPort = parseInt(readFileSync(PORT_FILE, 'utf-8').trim(), 10);
307
- if (existingPort >= START_PORT && existingPort <= MAX_PORT) {
308
- const alive = await checkPortAlive(existingPort);
309
- if (alive) {
310
- actualPort = existingPort;
311
- return null;
312
- }
313
- }
314
- } catch {}
315
- }
316
- // 等待后仍无法复用,静默退出
317
- return null;
318
- }
319
-
320
- try {
321
- // 检查是否已有 cc-viewer 实例在运行
322
- if (existsSync(PORT_FILE)) {
323
- try {
324
- const existingPort = parseInt(readFileSync(PORT_FILE, 'utf-8').trim(), 10);
325
- if (existingPort >= START_PORT && existingPort <= MAX_PORT) {
326
- const alive = await checkPortAlive(existingPort);
327
- if (alive) {
328
- actualPort = existingPort;
329
- releaseLock();
330
- console.log(t('server.reuse', { host: HOST, port: existingPort }));
331
- return null;
332
- }
333
- }
334
- } catch {
335
- // PORT_FILE 读取失败,继续正常启动
290
+ return new Promise((resolve, reject) => {
291
+ function tryListen(port) {
292
+ if (port > MAX_PORT) {
293
+ console.log(t('server.portsBusy', { start: START_PORT, end: MAX_PORT }));
294
+ resolve(null);
295
+ return;
336
296
  }
337
- // 旧实例已不存在,清理 PORT_FILE
338
- try { unlinkSync(PORT_FILE); } catch {}
339
- }
340
-
341
- return new Promise((resolve, reject) => {
342
- function tryListen(port) {
343
- if (port > MAX_PORT) {
344
- console.log(t('server.portsBusy', { start: START_PORT, end: MAX_PORT }));
345
- releaseLock();
346
- resolve(null);
347
- return;
348
- }
349
297
 
350
- const currentServer = createServer(handleRequest);
298
+ const currentServer = createServer(handleRequest);
351
299
 
352
- currentServer.listen(port, HOST, () => {
353
- server = currentServer;
354
- actualPort = port;
355
- try { writeFileSync(PORT_FILE, String(port)); } catch {}
356
- releaseLock();
357
- console.log(t('server.started', { host: HOST, port }));
358
- startWatching();
359
- resolve(server);
360
- });
300
+ currentServer.listen(port, HOST, () => {
301
+ server = currentServer;
302
+ actualPort = port;
303
+ console.log(t('server.started', { host: HOST, port }));
304
+ startWatching();
305
+ resolve(server);
306
+ });
361
307
 
362
- currentServer.on('error', (err) => {
363
- if (err.code === 'EADDRINUSE') {
364
- tryListen(port + 1);
365
- } else {
366
- releaseLock();
367
- reject(err);
368
- }
369
- });
370
- }
308
+ currentServer.on('error', (err) => {
309
+ if (err.code === 'EADDRINUSE') {
310
+ tryListen(port + 1);
311
+ } else {
312
+ reject(err);
313
+ }
314
+ });
315
+ }
371
316
 
372
- tryListen(START_PORT);
373
- });
374
- } catch (err) {
375
- releaseLock();
376
- throw err;
377
- }
317
+ tryListen(START_PORT);
318
+ });
378
319
  }
379
320
 
380
321
  export function stopViewer() {
381
- unwatchFile(LOG_FILE);
322
+ for (const logFile of watchedFiles.keys()) {
323
+ unwatchFile(logFile);
324
+ }
325
+ watchedFiles.clear();
382
326
  clients.forEach(client => client.end());
383
327
  clients = [];
384
328
  if (server) {
File without changes