226 lines · 7.7 KB · TypeScript
RawDownload
/** * SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests. * * Channels: * - ssh:connect - Connect to SSH host, create new context * - ssh:disconnect - Disconnect and switch back to local context * - ssh:getState - Get current connection state * - ssh:test - Test connection without switching */ import { createLogger } from '@shared/utils/logger'; // Channel constants (mirrored from preload/constants/ipcChannels.ts to respect module boundaries)const SSH_CONNECT = 'ssh:connect';const SSH_DISCONNECT = 'ssh:disconnect';const SSH_GET_STATE = 'ssh:getState';const SSH_TEST = 'ssh:test';const SSH_GET_CONFIG_HOSTS = 'ssh:getConfigHosts';const SSH_RESOLVE_HOST = 'ssh:resolveHost';const SSH_SAVE_LAST_CONNECTION = 'ssh:saveLastConnection';const SSH_GET_LAST_CONNECTION = 'ssh:getLastConnection';import * as path from 'path'; import { configManager, ServiceContext } from '../services'; import type {  ServiceContextRegistry,  SshConnectionConfig,  SshConnectionManager,  SshConnectionStatus,} from '../services';import type { SshLastConnection } from '@shared/types';import type { IpcMain } from 'electron'; const logger = createLogger('IPC:ssh'); // =============================================================================// Module State// ============================================================================= let connectionManager: SshConnectionManager;let registry: ServiceContextRegistry;let onContextRewire: (context: ServiceContext) => void; // =============================================================================// Initialization// ============================================================================= /** * Initialize SSH handlers with required services. * @param manager - The SSH connection manager instance * @param contextRegistry - The service context registry * @param onRewire - Rewire-only callback (no renderer notification) for renderer-initiated switches */export function initializeSshHandlers(  manager: SshConnectionManager,  contextRegistry: ServiceContextRegistry,  onRewire: (context: ServiceContext) => void): void {  connectionManager = manager;  registry = contextRegistry;  onContextRewire = onRewire;} // =============================================================================// Handler Registration// ============================================================================= export function registerSshHandlers(ipcMain: IpcMain): void {  ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => {    try {      // Connect to SSH host      await connectionManager.connect(config);       // Get provider and remote path      const provider = connectionManager.getProvider();      const remoteProjectsPath = connectionManager.getRemoteProjectsPath() ?? undefined;      const remoteTodosPath = remoteProjectsPath        ? path.join(path.dirname(remoteProjectsPath), 'todos')        : undefined;       // Generate context ID      const contextId = `ssh-${config.host}`;       // Destroy existing SSH context if any (reconnection case)      if (registry.has(contextId)) {        logger.info(`Destroying existing SSH context: ${contextId}`);        registry.destroy(contextId);      }       // Create new SSH context      const sshContext = new ServiceContext({        id: contextId,        type: 'ssh',        fsProvider: provider,        projectsDir: remoteProjectsPath,        todosDir: remoteTodosPath,      });       // Register and start SSH context      registry.registerContext(sshContext);      sshContext.start();       // Switch to SSH context      registry.switch(contextId);       // Re-wire file watcher events only (renderer's connectSsh() handles state)      onContextRewire(sshContext);       return { success: true, data: connectionManager.getStatus() };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      logger.error('SSH connect failed:', message);      return { success: false, error: message };    }  });   ipcMain.handle(SSH_DISCONNECT, async () => {    try {      // Get current SSH context ID before disconnecting      const currentContextId = registry.getActiveContextId();      const isSshContext = currentContextId.startsWith('ssh-');       // Disconnect from SSH      connectionManager.disconnect();       // If we were on an SSH context, destroy it      if (isSshContext) {        // Switch back to local first (this also starts local file watcher)        registry.switch('local');         // Destroy the SSH context        registry.destroy(currentContextId);         // Re-wire file watcher events only (renderer's disconnectSsh() handles state)        const localContext = registry.getActive();        onContextRewire(localContext);      }       return { success: true, data: connectionManager.getStatus() };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      logger.error('SSH disconnect failed:', message);      return { success: false, error: message };    }  });   ipcMain.handle(SSH_GET_STATE, async (): Promise<SshConnectionStatus> => {    return connectionManager.getStatus();  });   ipcMain.handle(SSH_TEST, async (_event, config: SshConnectionConfig) => {    try {      const result = await connectionManager.testConnection(config);      return { success: true, data: result };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      return { success: false, error: message };    }  });   ipcMain.handle(SSH_GET_CONFIG_HOSTS, async () => {    try {      const hosts = await connectionManager.getConfigHosts();      return { success: true, data: hosts };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      logger.error('Failed to get SSH config hosts:', message);      return { success: true, data: [] };    }  });   ipcMain.handle(SSH_RESOLVE_HOST, async (_event, alias: string) => {    try {      const entry = await connectionManager.resolveHostConfig(alias);      return { success: true, data: entry };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      logger.error(`Failed to resolve SSH host "${alias}":`, message);      return { success: true, data: null };    }  });   ipcMain.handle(SSH_SAVE_LAST_CONNECTION, async (_event, config: SshLastConnection) => {    try {      configManager.updateConfig('ssh', {        lastConnection: {          host: config.host,          port: config.port,          username: config.username,          authMethod: config.authMethod,          privateKeyPath: config.privateKeyPath,        },      });      return { success: true };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      logger.error('Failed to save SSH connection:', message);      return { success: false, error: message };    }  });   ipcMain.handle(SSH_GET_LAST_CONNECTION, async () => {    try {      const config = configManager.getConfig();      return { success: true, data: config.ssh.lastConnection };    } catch (err) {      const message = err instanceof Error ? err.message : String(err);      logger.error('Failed to get last SSH connection:', message);      return { success: true, data: null };    }  });   logger.info('SSH handlers registered');} export function removeSshHandlers(ipcMain: IpcMain): void {  ipcMain.removeHandler(SSH_CONNECT);  ipcMain.removeHandler(SSH_DISCONNECT);  ipcMain.removeHandler(SSH_GET_STATE);  ipcMain.removeHandler(SSH_TEST);  ipcMain.removeHandler(SSH_GET_CONFIG_HOSTS);  ipcMain.removeHandler(SSH_RESOLVE_HOST);  ipcMain.removeHandler(SSH_SAVE_LAST_CONNECTION);  ipcMain.removeHandler(SSH_GET_LAST_CONNECTION);}