import { randomUUID } from 'crypto';
import { SSHClient } from './ssh-client.js';
import { SSHConfig, SessionInfo } from '../types/index.js';
interface CreateSessionResult {
sessionId: string;
status: 'connected' | 'error';
reused?: boolean;
message?: string;
}
class SessionManager {
private static instance: SessionManager;
private sessions = new Map<string, SSHClient>();
private hostIndex = new Map<string, string>(); // host:port:user -> sessionId
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
private defaultTimeoutMinutes = 15;
private constructor() {
this.startCleanup();
}
static getInstance(): SessionManager {
if (!SessionManager.instance) {
SessionManager.instance = new SessionManager();
}
return SessionManager.instance;
}
private startCleanup(): void {
this.cleanupInterval = setInterval(() => {
const now = Date.now();
const timeout = this.defaultTimeoutMinutes * 60 * 1000;
for (const [id, client] of this.sessions) {
if (now - client.lastActivity.getTime() > timeout) {
this.closeSession(id);
}
}
}, 60000); // Check every minute
}
private getHostKey(host: string, port: number, username: string): string {
return `${host}:${port}:${username}`;
}
async createSession(config: SSHConfig, forceNew = false, timeoutMinutes?: number): Promise<CreateSessionResult> {
const hostKey = this.getHostKey(config.host, config.port, config.username);
// Reuse existing session if available
if (!forceNew) {
const existingId = this.hostIndex.get(hostKey);
if (existingId) {
const existing = this.sessions.get(existingId);
if (existing?.isConnected()) {
return { sessionId: existingId, status: 'connected', reused: true };
}
// Clean up dead session
this.closeSession(existingId);
}
}
const sessionId = randomUUID();
const client = new SSHClient(sessionId, config);
try {
await client.connect();
this.sessions.set(sessionId, client);
this.hostIndex.set(hostKey, sessionId);
if (timeoutMinutes) {
this.defaultTimeoutMinutes = timeoutMinutes;
}
return { sessionId, status: 'connected' };
} catch (err: any) {
return { sessionId: '', status: 'error', message: err.message };
}
}
getSession(sessionId: string): SSHClient | undefined {
return this.sessions.get(sessionId);
}
closeSession(sessionId: string): boolean {
const client = this.sessions.get(sessionId);
if (!client) return false;
client.close();
this.sessions.delete(sessionId);
// Remove from host index
for (const [key, id] of this.hostIndex) {
if (id === sessionId) {
this.hostIndex.delete(key);
break;
}
}
return true;
}
listSessions(): SessionInfo[] {
const sessions: SessionInfo[] = [];
for (const [key, sessionId] of this.hostIndex) {
const client = this.sessions.get(sessionId);
if (client) {
const [host, port, username] = key.split(':');
sessions.push({
id: sessionId,
host,
port: parseInt(port),
username,
createdAt: client.lastActivity, // Approximate
lastActivity: client.lastActivity,
});
}
}
return sessions;
}
closeAll(): void {
for (const [id] of this.sessions) {
this.closeSession(id);
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
}
}
export { SessionManager };