ai-cli-online
Advanced tools
Sorry, the diff of this file is too big to display
@@ -90,5 +90,4 @@ #!/usr/bin/env bash | ||
| NoNewPrivileges=true | ||
| ProtectSystem=strict | ||
| ReadWritePaths=${RUN_HOME} | ||
| PrivateTmp=true | ||
| ProtectSystem=full | ||
| PrivateTmp=false | ||
@@ -95,0 +94,0 @@ [Install] |
+1
-1
| { | ||
| "name": "ai-cli-online", | ||
| "version": "3.0.6", | ||
| "version": "3.0.12", | ||
| "description": "AI-Cli Online - Web Terminal for Claude Code via xterm.js + tmux", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+30
-23
@@ -13,3 +13,3 @@ import express from 'express'; | ||
| import { setupWebSocket, clearWsIntervals } from './websocket.js'; | ||
| import { isTmuxAvailable, cleanupStaleSessions } from './tmux.js'; | ||
| import { isTmuxAvailable, cleanupOrphanedProcesses } from './tmux.js'; | ||
| import { cleanupOldDrafts, cleanupOldAnnotations, closeDb } from './db.js'; | ||
@@ -32,3 +32,2 @@ import { safeTokenCompare } from './auth.js'; | ||
| const MAX_CONNECTIONS = parseInt(process.env.MAX_CONNECTIONS || '10', 10); | ||
| const SESSION_TTL_HOURS = parseInt(process.env.SESSION_TTL_HOURS || '24', 10); | ||
| const RATE_LIMIT_READ = parseInt(process.env.RATE_LIMIT_READ || '300', 10); | ||
@@ -164,4 +163,10 @@ const RATE_LIMIT_WRITE = parseInt(process.env.RATE_LIMIT_WRITE || '100', 10); | ||
| }); | ||
| // --- Cleanup --- | ||
| // --- Startup cleanup --- | ||
| try { | ||
| await cleanupOrphanedProcesses(); | ||
| } | ||
| catch (e) { | ||
| console.error('[startup:orphans]', e); | ||
| } | ||
| try { | ||
| const purged = cleanupOldDrafts(7); | ||
@@ -174,22 +179,24 @@ if (purged > 0) | ||
| } | ||
| let cleanupTimer = null; | ||
| if (SESSION_TTL_HOURS > 0) { | ||
| const CLEANUP_INTERVAL = 60 * 60 * 1000; | ||
| cleanupTimer = setInterval(() => { | ||
| cleanupStaleSessions(SESSION_TTL_HOURS).catch((e) => console.error('[cleanup]', e)); | ||
| try { | ||
| cleanupOldDrafts(7); | ||
| } | ||
| catch (e) { | ||
| console.error('[cleanup:drafts]', e); | ||
| } | ||
| try { | ||
| cleanupOldAnnotations(7); | ||
| } | ||
| catch (e) { | ||
| console.error('[cleanup:annotations]', e); | ||
| } | ||
| }, CLEANUP_INTERVAL); | ||
| console.log(`Session TTL: ${SESSION_TTL_HOURS}h (cleanup every hour)`); | ||
| } | ||
| const CLEANUP_INTERVAL = 60 * 60 * 1000; | ||
| const cleanupTimer = setInterval(async () => { | ||
| try { | ||
| await cleanupOrphanedProcesses(); | ||
| } | ||
| catch (e) { | ||
| console.error('[cleanup:orphans]', e); | ||
| } | ||
| try { | ||
| cleanupOldDrafts(7); | ||
| } | ||
| catch (e) { | ||
| console.error('[cleanup:drafts]', e); | ||
| } | ||
| try { | ||
| cleanupOldAnnotations(7); | ||
| } | ||
| catch (e) { | ||
| console.error('[cleanup:annotations]', e); | ||
| } | ||
| }, CLEANUP_INTERVAL); | ||
| console.log('Sessions persist until manually closed (cleanup every hour)'); | ||
| // --- Graceful shutdown --- | ||
@@ -196,0 +203,0 @@ const shutdown = () => { |
@@ -7,2 +7,3 @@ import { Router } from 'express'; | ||
| import { spawn } from 'child_process'; | ||
| import { fileURLToPath } from 'url'; | ||
| import { resolveSession } from '../middleware/auth.js'; | ||
@@ -12,4 +13,5 @@ import { getCwd } from '../tmux.js'; | ||
| const router = Router(); | ||
| // Multer setup for file uploads | ||
| const UPLOAD_TMP_DIR = '/tmp/ai-cli-online-uploads'; | ||
| // Multer setup for file uploads — use server/data/ instead of /tmp to survive tmpfs cleanup | ||
| const __files_dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const UPLOAD_TMP_DIR = join(__files_dirname, '../../data/uploads'); | ||
| mkdirSync(UPLOAD_TMP_DIR, { recursive: true, mode: 0o700 }); | ||
@@ -16,0 +18,0 @@ const upload = multer({ |
@@ -37,3 +37,3 @@ export declare const TMUX_SOCKET_PATH: string; | ||
| export declare function listSessions(token: string): Promise<TmuxSessionInfo[]>; | ||
| /** Clean up idle tmux sessions older than the given TTL (hours) */ | ||
| /** Clean up idle tmux sessions whose last activity exceeds the given TTL (hours) */ | ||
| export declare function cleanupStaleSessions(ttlHours: number): Promise<void>; | ||
@@ -46,1 +46,13 @@ /** 获取 tmux session 当前活动 pane 的工作目录 */ | ||
| export declare function isTmuxAvailable(): boolean; | ||
| /** | ||
| * Clean up orphaned process trees from dead tmux sessions. | ||
| * | ||
| * With KillMode=process, tmux child processes (bash → claude → plugins) survive | ||
| * service restarts. When a tmux session is killed (by cleanup or manually), its | ||
| * child processes may keep running as orphans. This function identifies tmux server | ||
| * processes in the service cgroup whose sessions no longer exist and kills their | ||
| * entire process trees. | ||
| * | ||
| * Called once at startup. | ||
| */ | ||
| export declare function cleanupOrphanedProcesses(): Promise<void>; |
+95
-7
| import { execFile as execFileCb, execFileSync } from 'child_process'; | ||
| import { promisify } from 'util'; | ||
| import { createHash } from 'crypto'; | ||
| import { mkdirSync } from 'fs'; | ||
| import { mkdirSync, existsSync } from 'fs'; | ||
| import { join } from 'path'; | ||
@@ -150,3 +150,3 @@ const _execFile = promisify(execFileCb); | ||
| } | ||
| /** Clean up idle tmux sessions older than the given TTL (hours) */ | ||
| /** Clean up idle tmux sessions whose last activity exceeds the given TTL (hours) */ | ||
| export async function cleanupStaleSessions(ttlHours) { | ||
@@ -158,3 +158,3 @@ const cutoff = Math.floor(Date.now() / 1000) - ttlHours * 3600; | ||
| '-F', | ||
| '#{session_name}:#{session_created}:#{session_attached}', | ||
| '#{session_name}:#{session_activity}:#{session_attached}', | ||
| ], { encoding: 'utf-8' }); | ||
@@ -173,3 +173,3 @@ const staleNames = []; | ||
| continue; | ||
| const created = parseInt(rest.slice(secondLastColon + 1), 10); | ||
| const lastActivity = parseInt(rest.slice(secondLastColon + 1), 10); | ||
| const name = rest.slice(0, secondLastColon); | ||
@@ -180,4 +180,4 @@ if (!name.startsWith('ai-cli-online-')) | ||
| continue; | ||
| if (created < cutoff) { | ||
| console.log(`[tmux] Cleaning up stale session: ${name} (created ${new Date(created * 1000).toISOString()})`); | ||
| if (lastActivity < cutoff) { | ||
| console.log(`[tmux] Cleaning up stale session: ${name} (last activity ${new Date(lastActivity * 1000).toISOString()})`); | ||
| staleNames.push(name); | ||
@@ -198,3 +198,12 @@ } | ||
| ], { encoding: 'utf-8' }); | ||
| return stdout.trim(); | ||
| let cwd = stdout.trim(); | ||
| // tmux appends " (deleted)" when the CWD directory has been removed (e.g. /tmp after cleanup) | ||
| if (cwd.endsWith(' (deleted)')) { | ||
| cwd = cwd.slice(0, -' (deleted)'.length); | ||
| } | ||
| // Fall back to DEFAULT_WORKING_DIR or HOME if the path no longer exists | ||
| if (!cwd || !existsSync(cwd)) { | ||
| cwd = process.env.DEFAULT_WORKING_DIR || process.env.HOME || '/root'; | ||
| } | ||
| return cwd; | ||
| } | ||
@@ -223,1 +232,80 @@ /** 获取 tmux pane 当前运行的命令名称 */ | ||
| } | ||
| /** | ||
| * Clean up orphaned process trees from dead tmux sessions. | ||
| * | ||
| * With KillMode=process, tmux child processes (bash → claude → plugins) survive | ||
| * service restarts. When a tmux session is killed (by cleanup or manually), its | ||
| * child processes may keep running as orphans. This function identifies tmux server | ||
| * processes in the service cgroup whose sessions no longer exist and kills their | ||
| * entire process trees. | ||
| * | ||
| * Called once at startup. | ||
| */ | ||
| export async function cleanupOrphanedProcesses() { | ||
| // Get live session names from the socket-based tmux server | ||
| const liveSessions = new Set(); | ||
| try { | ||
| const { stdout } = await tmuxExec([ | ||
| 'list-sessions', '-F', '#{session_name}', | ||
| ], { encoding: 'utf-8' }); | ||
| for (const line of stdout.trim().split('\n')) { | ||
| if (line) | ||
| liveSessions.add(line); | ||
| } | ||
| } | ||
| catch { | ||
| // tmux server not running — nothing to clean | ||
| } | ||
| // Find tmux server processes that belong to ai-cli-online but manage dead sessions. | ||
| // These show up as `tmux new-session -d -s <session-name>` in /proc/*/cmdline. | ||
| try { | ||
| const { stdout } = await _execFile('ps', [ | ||
| '-eo', 'pid,ppid,args', '--no-headers', | ||
| ], { encoding: 'utf-8', timeout: EXEC_TIMEOUT }); | ||
| const orphanPids = []; | ||
| for (const line of stdout.trim().split('\n')) { | ||
| if (!line) | ||
| continue; | ||
| const match = line.match(/^\s*(\d+)\s+\d+\s+tmux.*new-session\s+-d\s+-s\s+(ai-cli-online-\S+)/); | ||
| if (!match) | ||
| continue; | ||
| const pid = parseInt(match[1], 10); | ||
| const sessionName = match[2]; | ||
| if (!liveSessions.has(sessionName)) { | ||
| orphanPids.push(pid); | ||
| console.log(`[cleanup] Found orphaned tmux process tree: PID ${pid}, dead session: ${sessionName}`); | ||
| } | ||
| } | ||
| for (const pid of orphanPids) { | ||
| try { | ||
| // Kill the entire process group/tree rooted at this tmux server | ||
| process.kill(-pid, 'SIGTERM'); | ||
| } | ||
| catch { | ||
| // Process group kill failed, try individual kill | ||
| try { | ||
| process.kill(pid, 'SIGTERM'); | ||
| } | ||
| catch { /* already gone */ } | ||
| } | ||
| } | ||
| if (orphanPids.length > 0) { | ||
| // Give processes time to exit gracefully, then force-kill survivors | ||
| await new Promise((resolve) => setTimeout(resolve, 2000)); | ||
| for (const pid of orphanPids) { | ||
| try { | ||
| process.kill(-pid, 'SIGKILL'); | ||
| } | ||
| catch { /* already gone */ } | ||
| try { | ||
| process.kill(pid, 'SIGKILL'); | ||
| } | ||
| catch { /* already gone */ } | ||
| } | ||
| console.log(`[cleanup] Cleaned up ${orphanPids.length} orphaned tmux process tree(s)`); | ||
| } | ||
| } | ||
| catch (err) { | ||
| console.error('[cleanup] Failed to scan for orphaned processes:', err); | ||
| } | ||
| } |
| { | ||
| "name": "ai-cli-online-server", | ||
| "version": "3.0.3", | ||
| "version": "3.0.12", | ||
| "description": "CLI-Online Backend Server", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
| { | ||
| "name": "ai-cli-online-shared", | ||
| "version": "3.0.3", | ||
| "version": "3.0.12", | ||
| "description": "Shared types for CLI-Online", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -13,3 +13,3 @@ <!DOCTYPE html> | ||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-webfont@1.7.0/lxgwwenkaimono-bold.css" /> | ||
| <script type="module" crossorigin src="/assets/index-D4ZKzY3K.js"></script> | ||
| <script type="module" crossorigin src="/assets/index-BHAsZRWj.js"></script> | ||
| <link rel="modulepreload" crossorigin href="/assets/react-vendor-BCIvbQoU.js"> | ||
@@ -16,0 +16,0 @@ <link rel="modulepreload" crossorigin href="/assets/terminal-DnNpv9tw.js"> |
+1
-1
| { | ||
| "name": "ai-cli-online-web", | ||
| "version": "3.0.3", | ||
| "version": "3.0.12", | ||
| "description": "CLI-Online Web Frontend", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 14 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 15 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1139720
0.63%5994
2.04%77
1.32%