UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge.

284 lines • 12.9 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var TerminalService_1; import os from 'node:os'; import process from 'node:process'; import { Inject, Injectable } from '@nestjs/common'; import { pathExists } from 'fs-extra/esm'; import { ConfigService } from '../../../core/config/config.service.js'; import { Logger } from '../../../core/logger/logger.service.js'; import { NodePtyService } from '../../../core/node-pty/node-pty.service.js'; let TerminalService = class TerminalService { static { TerminalService_1 = this; } configService; logger; nodePtyService; ending = false; static persistentTerminal = null; static connectedClients = new Set(); static dataListenerAttached = false; static terminalBuffer = ''; static persistentAptHintInjected = false; instanceId; constructor(configService, logger, nodePtyService) { this.configService = configService; this.logger = logger; this.nodePtyService = nodePtyService; this.instanceId = Math.random().toString(36).substring(2, 11); this.logger.debug(`TerminalService instance created: ${this.instanceId}`); } shouldInjectAptPackageCommandHint() { return this.configService.runningInPackageMode && !this.configService.runningInSynologyPackage; } getAptPackageCommandHintLine() { return '\x1B[37mUpdate Homebridge APT package: \x1B[92mhb-service update-homebridge\x1B[0m\r\n'; } injectAptHintBeforeNodeUpdateLine(data) { if (!data.includes('Update Node.js:')) { return data; } return data.replace('Update Node.js:', `${this.getAptPackageCommandHintLine()}Update Node.js:`); } async getPreferredShell() { if (os.platform() === 'darwin' && await pathExists('/bin/zsh')) { return '/bin/zsh'; } return await pathExists('/bin/bash') ? '/bin/bash' : '/bin/sh'; } async startSession(client, size) { this.ending = false; if (!this.configService.enableTerminalAccess) { this.logger.warn('Terminal is not enabled, disconnecting client...'); client.disconnect(); return; } const terminalPersistence = Boolean(this.configService.ui.terminal?.persistence); if (terminalPersistence) { return this.attachToPersistentTerminal(client, size); } else { return this.createNewTerminal(client, size); } } async createNewTerminal(client, size) { this.logger.debug('Starting new terminal session.'); const shell = await this.getPreferredShell(); const term = this.nodePtyService.spawn(shell, [], { name: 'xterm-color', cols: size.cols, rows: size.rows, cwd: this.configService.storagePath, env: process.env, }); let aptHintInjected = false; term.onData((data) => { let output = data; if (!aptHintInjected && this.shouldInjectAptPackageCommandHint()) { const injectedOutput = this.injectAptHintBeforeNodeUpdateLine(data); aptHintInjected = injectedOutput !== data; output = injectedOutput; } client.emit('stdout', output); }); term.onExit((exitInfo) => { try { if (!this.ending) { client.emit('process-exit', exitInfo.exitCode); } } catch (e) { } }); client.on('stdin', (data) => { term.write(data); }); client.on('resize', (resize) => { try { term.resize(resize.cols, resize.rows); } catch { } }); const onEnd = () => { this.ending = true; client.removeAllListeners('stdin'); client.removeAllListeners('resize'); client.removeAllListeners('end'); client.removeAllListeners('disconnect'); try { this.logger.debug('Terminal session ended.'); term.kill(); } catch { } }; client.on('end', onEnd.bind(this)); client.on('disconnect', onEnd.bind(this)); } async attachToPersistentTerminal(client, size) { this.logger.debug(`[${this.instanceId}] attachToPersistentTerminal called`); if (!TerminalService_1.persistentTerminal) { this.logger.debug(`[${this.instanceId}] Creating new persistent terminal session.`); const shell = await this.getPreferredShell(); TerminalService_1.persistentTerminal = this.nodePtyService.spawn(shell, [], { name: 'xterm-color', cols: size.cols, rows: size.rows, cwd: this.configService.storagePath, env: process.env, }); TerminalService_1.persistentAptHintInjected = false; if (!TerminalService_1.dataListenerAttached) { this.logger.debug(`[${this.instanceId}] Attaching data listener`); TerminalService_1.persistentTerminal.onData((data) => { try { let output = data; if (!TerminalService_1.persistentAptHintInjected && this.shouldInjectAptPackageCommandHint()) { const injectedOutput = this.injectAptHintBeforeNodeUpdateLine(data); TerminalService_1.persistentAptHintInjected = injectedOutput !== data; output = injectedOutput; } TerminalService_1.terminalBuffer += output; const maxBufferSize = this.configService.ui.terminal?.bufferSize || globalThis.terminal.bufferSize; if (TerminalService_1.terminalBuffer.length > maxBufferSize) { TerminalService_1.terminalBuffer = TerminalService_1.terminalBuffer.slice(-maxBufferSize); } if (TerminalService_1.connectedClients.size > 0) { TerminalService_1.connectedClients.forEach((client) => { try { client.emit('stdout', output); } catch (e) { this.logger.error(`[${this.instanceId}] Error sending output to a client: ${e}`); TerminalService_1.connectedClients.delete(client); } }); } } catch (e) { this.logger.error(`[${this.instanceId}] Error sending output to client: ${e}`); } }); TerminalService_1.dataListenerAttached = true; } TerminalService_1.persistentTerminal.onExit((exitInfo) => { this.logger.debug(`[${this.instanceId}] Persistent terminal exited.`); TerminalService_1.connectedClients.forEach((client) => { try { client.emit('process-exit', exitInfo.exitCode); } catch (e) { TerminalService_1.connectedClients.delete(client); } }); TerminalService_1.persistentTerminal = null; TerminalService_1.connectedClients.clear(); TerminalService_1.dataListenerAttached = false; TerminalService_1.terminalBuffer = ''; TerminalService_1.persistentAptHintInjected = false; }); } else { this.logger.debug(`[${this.instanceId}] Attaching to existing persistent terminal.`); try { TerminalService_1.persistentTerminal.resize(size.cols, size.rows); } catch { } } this.logger.debug(`[${this.instanceId}] Cleaning up existing client listeners`); client.removeAllListeners('stdin'); client.removeAllListeners('resize'); this.logger.debug(`[${this.instanceId}] Adding client to connected clients`); TerminalService_1.connectedClients.add(client); if (TerminalService_1.terminalBuffer && TerminalService_1.terminalBuffer.length > 0) { this.logger.debug(`[${this.instanceId}] Sending ${TerminalService_1.terminalBuffer.length} chars of buffer to new client`); try { client.emit('stdout', TerminalService_1.terminalBuffer); } catch (e) { this.logger.error(`[${this.instanceId}] Error sending buffer to client: ${e}`); } } else { this.logger.debug(`[${this.instanceId}] No buffer to send to new client`); } this.logger.debug(`[${this.instanceId}] Adding stdin and resize listeners`); client.on('stdin', (data) => { if (TerminalService_1.persistentTerminal) { TerminalService_1.persistentTerminal.write(data); } else { this.logger.warn(`[${this.instanceId}] No persistent terminal to write to!`); } }); client.on('resize', (resize) => { this.logger.debug(`[${this.instanceId}] Received resize from client`); try { if (TerminalService_1.persistentTerminal) { TerminalService_1.persistentTerminal.resize(resize.cols, resize.rows); } } catch { } }); const onEnd = () => { this.logger.debug(`[${this.instanceId}] Client disconnecting`); client.removeAllListeners('stdin'); client.removeAllListeners('resize'); client.removeAllListeners('end'); client.removeAllListeners('disconnect'); if (TerminalService_1.connectedClients.has(client)) { TerminalService_1.connectedClients.delete(client); this.logger.debug(`[${this.instanceId}] Removed client from connected clients`); } this.logger.debug(`[${this.instanceId}] Client cleanup complete`); }; client.on('end', onEnd); client.on('disconnect', onEnd); } hasPersistentSession() { const hasPersistent = TerminalService_1.persistentTerminal !== null; this.logger.debug(`[${this.instanceId}] hasPersistentSession: ${hasPersistent}`); return hasPersistent; } destroyPersistentSession() { this.logger.debug(`[${this.instanceId}] Destroying persistent terminal session`); if (TerminalService_1.persistentTerminal) { try { this.logger.debug(`[${this.instanceId}] Killing persistent terminal process`); TerminalService_1.persistentTerminal.kill(); } catch (e) { this.logger.error(`[${this.instanceId}] Error killing persistent terminal: ${e}`); } TerminalService_1.persistentTerminal = null; } TerminalService_1.terminalBuffer = ''; TerminalService_1.dataListenerAttached = false; TerminalService_1.connectedClients.clear(); TerminalService_1.persistentAptHintInjected = false; this.logger.debug(`[${this.instanceId}] Persistent terminal session destroyed`); } }; TerminalService = TerminalService_1 = __decorate([ Injectable(), __param(0, Inject(ConfigService)), __param(1, Inject(Logger)), __param(2, Inject(NodePtyService)), __metadata("design:paramtypes", [ConfigService, Logger, NodePtyService]) ], TerminalService); export { TerminalService }; //# sourceMappingURL=terminal.service.js.map