UNPKG

treesap

Version:
264 lines 9.95 kB
import { EventEmitter } from 'events'; import * as pty from 'node-pty'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; export class TerminalService { static sessions = new Map(); static SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes static PERSISTENCE_DIR = path.join(os.tmpdir(), '.treesap-terminals'); static SESSIONS_FILE = path.join(this.PERSISTENCE_DIR, 'sessions.json'); static createSession(sessionId, options) { // Clean up any existing session with the same ID this.destroySession(sessionId); const eventEmitter = new EventEmitter(); // Increase max listeners to handle multiple terminal tabs and connections eventEmitter.setMaxListeners(20); // Use provided options or defaults const cwd = options?.cwd || process.cwd(); const cols = options?.cols || 80; const rows = options?.rows || 24; const env = {}; // Filter out undefined values from process.env for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { env[key] = value; } } // Create a PTY process for proper terminal behavior const ptyProcess = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : process.env.SHELL || '/bin/bash', [], { name: 'xterm-256color', cols, rows, cwd, env }); const session = { id: sessionId, process: ptyProcess, eventEmitter, createdAt: new Date(), lastActivity: new Date(), cwd, env, cols, rows }; // Handle process output ptyProcess.onData((data) => { session.lastActivity = new Date(); eventEmitter.emit('output', { type: 'output', content: data }); }); ptyProcess.onExit((e) => { eventEmitter.emit('output', { type: 'exit', code: e.exitCode }); this.destroySession(sessionId); }); this.sessions.set(sessionId, session); // Set up session cleanup this.scheduleSessionCleanup(sessionId); // Persist session data this.persistSessionData(session); return session; } static getSession(sessionId) { return this.sessions.get(sessionId); } static executeCommand(sessionId, command) { const session = this.getSession(sessionId); if (!session) { return false; } try { session.lastActivity = new Date(); session.process.write(command + '\n'); return true; } catch (error) { console.error(`Error executing command in session ${sessionId}:`, error); return false; } } static destroySession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return false; } try { // Clean up the PTY process session.process.kill(); // Remove event listeners session.eventEmitter.removeAllListeners(); // Remove from sessions map this.sessions.delete(sessionId); // Remove from persistent storage this.removePersistedSession(sessionId); return true; } catch (error) { console.error(`Error destroying session ${sessionId}:`, error); return false; } } static getAllSessions() { return Array.from(this.sessions.values()); } static cleanupExpiredSessions() { const now = new Date(); let cleanedCount = 0; for (const [sessionId, session] of this.sessions.entries()) { const timeSinceLastActivity = now.getTime() - session.lastActivity.getTime(); if (timeSinceLastActivity > this.SESSION_TIMEOUT) { this.destroySession(sessionId); cleanedCount++; } } return cleanedCount; } static scheduleSessionCleanup(sessionId) { setTimeout(() => { const session = this.getSession(sessionId); if (session) { const timeSinceLastActivity = new Date().getTime() - session.lastActivity.getTime(); if (timeSinceLastActivity >= this.SESSION_TIMEOUT) { console.log(`Cleaning up expired terminal session: ${sessionId}`); this.destroySession(sessionId); } else { // Reschedule cleanup this.scheduleSessionCleanup(sessionId); } } }, this.SESSION_TIMEOUT); } static setupGlobalCleanup() { // Load persisted sessions on startup this.loadPersistedSessions(); // Cleanup all sessions on process exit const cleanup = () => { console.log('Cleaning up all terminal sessions...'); for (const sessionId of this.sessions.keys()) { this.destroySession(sessionId); } }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); process.on('exit', cleanup); // Periodic cleanup of expired sessions setInterval(() => { const cleaned = this.cleanupExpiredSessions(); if (cleaned > 0) { console.log(`Cleaned up ${cleaned} expired terminal sessions`); } }, 5 * 60 * 1000); // Every 5 minutes } // Persistence methods static ensurePersistenceDir() { try { if (!fs.existsSync(this.PERSISTENCE_DIR)) { fs.mkdirSync(this.PERSISTENCE_DIR, { recursive: true }); } } catch (error) { console.error('Error creating persistence directory:', error); } } static persistSessionData(session) { try { this.ensurePersistenceDir(); const sessionData = { id: session.id, createdAt: session.createdAt.toISOString(), lastActivity: session.lastActivity.toISOString(), cwd: session.cwd || process.cwd(), env: session.env || (() => { const env = {}; for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { env[key] = value; } } return env; })(), cols: session.cols || 80, rows: session.rows || 24 }; let existingData = []; if (fs.existsSync(this.SESSIONS_FILE)) { const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8'); if (content.trim()) { existingData = JSON.parse(content); } } // Remove existing session data if present existingData = existingData.filter(s => s.id !== session.id); // Add new session data existingData.push(sessionData); fs.writeFileSync(this.SESSIONS_FILE, JSON.stringify(existingData, null, 2)); } catch (error) { console.error('Error persisting session data:', error); } } static removePersistedSession(sessionId) { try { if (!fs.existsSync(this.SESSIONS_FILE)) return; const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8'); if (!content.trim()) return; let existingData = JSON.parse(content); existingData = existingData.filter(s => s.id !== sessionId); fs.writeFileSync(this.SESSIONS_FILE, JSON.stringify(existingData, null, 2)); } catch (error) { console.error('Error removing persisted session:', error); } } static loadPersistedSessions() { try { if (!fs.existsSync(this.SESSIONS_FILE)) return; const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8'); if (!content.trim()) return; const persistedSessions = JSON.parse(content); console.log(`Found ${persistedSessions.length} persisted terminal session(s)`); for (const sessionData of persistedSessions) { // Check if session is not too old const lastActivity = new Date(sessionData.lastActivity); const timeSinceLastActivity = Date.now() - lastActivity.getTime(); if (timeSinceLastActivity < this.SESSION_TIMEOUT) { console.log(`Restoring terminal session: ${sessionData.id}`); // Create new session with the persisted options this.createSession(sessionData.id, { cwd: sessionData.cwd, cols: sessionData.cols, rows: sessionData.rows }); } else { console.log(`Skipping expired session: ${sessionData.id}`); this.removePersistedSession(sessionData.id); } } } catch (error) { console.error('Error loading persisted sessions:', error); } } // Update session activity and persist static updateSessionActivity(sessionId) { const session = this.getSession(sessionId); if (session) { session.lastActivity = new Date(); this.persistSessionData(session); } } } //# sourceMappingURL=terminal.js.map