diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 6dbb0052..409b0f4e 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -110,7 +110,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex }) } - static forRoot (): ModuleWithProviders { + static forRoot (): ModuleWithProviders { return { ngModule: AppModule, providers: PROVIDERS, diff --git a/terminus-serial/src/api.ts b/terminus-serial/src/api.ts index 62ba1ebf..8c70482c 100644 --- a/terminus-serial/src/api.ts +++ b/terminus-serial/src/api.ts @@ -1,3 +1,4 @@ +import stripAnsi from 'strip-ansi' import { BaseSession } from 'terminus-terminal' import { SerialPort } from 'serialport' import { Logger } from 'terminus-core' @@ -50,49 +51,8 @@ export class SerialSession extends BaseSession { async start (): Promise { this.open = true - this.serial.on('data', data => { - const dataString = data.toString() - this.emitOutput(data) - - if (this.scripts) { - let found = false - for (const script of this.scripts) { - let match = false - let cmd = '' - if (script.isRegex) { - const re = new RegExp(script.expect, 'g') - if (dataString.match(re)) { - cmd = dataString.replace(re, script.send) - match = true - found = true - } - } else { - if (dataString.includes(script.expect)) { - cmd = script.send - match = true - found = true - } - } - - if (match) { - this.logger.info('Executing script: "' + cmd + '"') - this.serial.write(cmd + '\n') - this.scripts = this.scripts.filter(x => x !== script) - } else { - if (script.optional) { - this.logger.debug('Skip optional script: ' + script.expect) - found = true - this.scripts = this.scripts.filter(x => x !== script) - } else { - break - } - } - } - - if (found) { - this.executeUnconditionalScripts() - } - } + this.serial.on('readable', () => { + this.onData(this.serial.read()) }) this.serial.on('end', () => { @@ -123,6 +83,11 @@ export class SerialSession extends BaseSession { this.serial.close() } + emitServiceMessage (msg: string): void { + this.serviceMessage.next(msg) + this.logger.info(stripAnsi(msg)) + } + async getChildProcesses (): Promise { return [] } @@ -139,6 +104,51 @@ export class SerialSession extends BaseSession { return null } + private onData (data: Buffer) { + const dataString = data.toString() + this.emitOutput(data) + + if (this.scripts) { + let found = false + for (const script of this.scripts) { + let match = false + let cmd = '' + if (script.isRegex) { + const re = new RegExp(script.expect, 'g') + if (dataString.match(re)) { + cmd = dataString.replace(re, script.send) + match = true + found = true + } + } else { + if (dataString.includes(script.expect)) { + cmd = script.send + match = true + found = true + } + } + + if (match) { + this.logger.info('Executing script: "' + cmd + '"') + this.serial.write(cmd + '\n') + this.scripts = this.scripts.filter(x => x !== script) + } else { + if (script.optional) { + this.logger.debug('Skip optional script: ' + script.expect) + found = true + this.scripts = this.scripts.filter(x => x !== script) + } else { + break + } + } + } + + if (found) { + this.executeUnconditionalScripts() + } + } + } + private executeUnconditionalScripts () { if (this.scripts) { for (const script of this.scripts) { diff --git a/terminus-serial/src/components/serialTab.component.pug b/terminus-serial/src/components/serialTab.component.pug index 1ad628e1..a2339ffd 100644 --- a/terminus-serial/src/components/serialTab.component.pug +++ b/terminus-serial/src/components/serialTab.component.pug @@ -1,16 +1,16 @@ .tab-toolbar .btn.btn-outline-secondary.reveal-button i.fas.fa-ellipsis-h - .toolbar(*ngIf='session', [class.show]='!session.open') - i.fas.fa-circle.text-success.mr-2(*ngIf='session.open') - i.fas.fa-circle.text-danger.mr-2(*ngIf='!session.open') + .toolbar([class.show]='!session || !session.open') + i.fas.fa-circle.text-success.mr-2(*ngIf='session && session.open') + i.fas.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open') strong(*ngIf='session') {{session.connection.port}} ({{session.connection.baudrate}}) .mr-auto - button.btn.btn-secondary.mr-3((click)='changeBaudRate()', *ngIf='session.open') + button.btn.btn-secondary.mr-3((click)='changeBaudRate()', *ngIf='session && session.open') span Change baud rate - button.btn.btn-info((click)='reconnect()', *ngIf='!session.open') + button.btn.btn-info((click)='reconnect()', *ngIf='!session || !session.open') i.fas.fa-reload span Reconnect diff --git a/terminus-serial/src/components/serialTab.component.ts b/terminus-serial/src/components/serialTab.component.ts index 43f48b2d..9e2b14dd 100644 --- a/terminus-serial/src/components/serialTab.component.ts +++ b/terminus-serial/src/components/serialTab.component.ts @@ -17,8 +17,9 @@ import { Subscription } from 'rxjs' }) export class SerialTabComponent extends BaseTerminalTabComponent { connection?: SerialConnection - session?: SerialSession + session: SerialSession|null = null serialPort: any + private serialService: SerialService private homeEndSubscription: Subscription // eslint-disable-next-line @typescript-eslint/no-useless-constructor @@ -26,6 +27,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent { injector: Injector, ) { super(injector) + this.serialService = injector.get(SerialService) } ngOnInit () { @@ -62,12 +64,8 @@ export class SerialTabComponent extends BaseTerminalTabComponent { return } - this.session = this.injector.get(SerialService).createSession(this.connection) - this.session.serviceMessage$.subscribe(msg => { - this.write(`\r\n${colors.black.bgWhite(' serial ')} ${msg}\r\n`) - this.session?.resize(this.size.columns, this.size.rows) - }) - this.attachSessionHandlers() + const session = this.serialService.createSession(this.connection) + this.setSession(session) this.write(`Connecting to `) const spinner = new Spinner({ @@ -80,15 +78,32 @@ export class SerialTabComponent extends BaseTerminalTabComponent { spinner.start() try { - this.serialPort = await this.injector.get(SerialService).connectSession(this.session) + this.serialPort = await this.serialService.connectSession(this.session!) spinner.stop(true) + session.emitServiceMessage('Port opened') } catch (e) { spinner.stop(true) this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n') return } - await this.session.start() - this.session.resize(this.size.columns, this.size.rows) + await this.session!.start() + this.session!.resize(this.size.columns, this.size.rows) + } + + protected attachSessionHandlers () { + this.attachSessionHandler(this.session!.serviceMessage$.subscribe(msg => { + this.write(`\r\n${colors.black.bgWhite(' Serial ')} ${msg}\r\n`) + this.session?.resize(this.size.columns, this.size.rows) + })) + this.attachSessionHandler(this.session!.destroyed$.subscribe(() => { + this.write('Press any key to reconnect\r\n') + this.input$.pipe(first()).subscribe(() => { + if (!this.session?.open) { + this.reconnect() + } + }) + })) + super.attachSessionHandlers() } async getRecoveryToken (): Promise { @@ -99,8 +114,10 @@ export class SerialTabComponent extends BaseTerminalTabComponent { } } - reconnect () { - this.initializeSession() + async reconnect (): Promise { + this.session?.destroy() + await this.initializeSession() + this.session?.releaseInitialDataBuffer() } async changeBaudRate () { diff --git a/terminus-serial/src/services/serial.service.ts b/terminus-serial/src/services/serial.service.ts index eb38f6af..e0a30fd0 100644 --- a/terminus-serial/src/services/serial.service.ts +++ b/terminus-serial/src/services/serial.service.ts @@ -30,10 +30,17 @@ export class SerialService { } async connectSession (session: SerialSession): Promise { - const serial = new SerialPort(session.connection.port, { autoOpen: false, baudRate: session.connection.baudrate, - dataBits: session.connection.databits, stopBits: session.connection.stopbits, parity: session.connection.parity, - rtscts: session.connection.rtscts, xon: session.connection.xon, xoff: session.connection.xoff, - xany: session.connection.xany }) + const serial = new SerialPort(session.connection.port, { + autoOpen: false, + baudRate: session.connection.baudrate, + dataBits: session.connection.databits, + stopBits: session.connection.stopbits, + parity: session.connection.parity, + rtscts: session.connection.rtscts, + xon: session.connection.xon, + xoff: session.connection.xoff, + xany: session.connection.xany, + }) session.serial = serial let connected = false await new Promise(async (resolve, reject) => { @@ -50,6 +57,10 @@ export class SerialService { } }) }) + serial.on('close', () => { + session.emitServiceMessage('Port closed') + session.destroy() + }) try { serial.open() diff --git a/terminus-ssh/src/components/sshTab.component.ts b/terminus-ssh/src/components/sshTab.component.ts index b1f76cdc..8d9665d7 100644 --- a/terminus-ssh/src/components/sshTab.component.ts +++ b/terminus-ssh/src/components/sshTab.component.ts @@ -20,12 +20,11 @@ import { Subscription } from 'rxjs' }) export class SSHTabComponent extends BaseTerminalTabComponent { connection?: SSHConnection - session?: SSHSession + session: SSHSession|null = null private sessionStack: SSHSession[] = [] private homeEndSubscription: Subscription private recentInputs = '' private reconnectOffered = false - private sessionHandlers: Subscription[] = [] constructor ( injector: Injector, @@ -85,8 +84,12 @@ export class SSHTabComponent extends BaseTerminalTabComponent { await this.setupOneSession(jumpSession) - this.sessionHandlers.push( - jumpSession.destroyed$.subscribe(() => session.destroy()) + this.attachSessionHandler( + jumpSession.destroyed$.subscribe(() => { + if (session.open) { + session.destroy() + } + }) ) session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut( @@ -107,31 +110,11 @@ export class SSHTabComponent extends BaseTerminalTabComponent { this.sessionStack.push(session) } - this.sessionHandlers.push(session.serviceMessage$.subscribe(msg => { + this.attachSessionHandler(session.serviceMessage$.subscribe(msg => { this.write(`\r\n${colors.black.bgWhite(' SSH ')} ${msg}\r\n`) session.resize(this.size.columns, this.size.rows) })) - this.sessionHandlers.push(session.destroyed$.subscribe(() => { - if ( - // Ctrl-D - this.recentInputs.charCodeAt(this.recentInputs.length - 1) === 4 || - this.recentInputs.endsWith('exit\r') - ) { - // User closed the session - this.destroy() - } else { - // Session was closed abruptly - this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` ${session.connection.host}: session closed\r\n`) - if (!this.reconnectOffered) { - this.reconnectOffered = true - this.write('Press any key to reconnect\r\n') - this.input$.pipe(first()).subscribe(() => { - this.reconnect() - }) - } - } - })) this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` Connecting to ${session.connection.host}\r\n`) @@ -158,6 +141,31 @@ export class SSHTabComponent extends BaseTerminalTabComponent { } } + protected attachSessionHandlers () { + const session = this.session! + super.attachSessionHandlers() + this.attachSessionHandler(session.destroyed$.subscribe(() => { + if ( + // Ctrl-D + this.recentInputs.charCodeAt(this.recentInputs.length - 1) === 4 || + this.recentInputs.endsWith('exit\r') + ) { + // User closed the session + this.destroy() + } else { + // Session was closed abruptly + this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` ${session.connection.host}: session closed\r\n`) + if (!this.reconnectOffered) { + this.reconnectOffered = true + this.write('Press any key to reconnect\r\n') + this.attachSessionHandler(this.input$.pipe(first()).subscribe(() => { + this.reconnect() + })) + } + } + })) + } + async initializeSession (): Promise { this.reconnectOffered = false if (!this.connection) { @@ -165,18 +173,17 @@ export class SSHTabComponent extends BaseTerminalTabComponent { return } - this.session = this.ssh.createSession(this.connection) + const session = this.ssh.createSession(this.connection) + this.setSession(session) try { - await this.setupOneSession(this.session) + await this.setupOneSession(session) } catch (e) { this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n') } - this.attachSessionHandlers() - - await this.session.start() - this.session.resize(this.size.columns, this.size.rows) + await this.session!.start() + this.session!.resize(this.size.columns, this.size.rows) } async getRecoveryToken (): Promise { @@ -193,10 +200,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent { } async reconnect (): Promise { - for (const s of this.sessionHandlers) { - s.unsubscribe() - } - this.sessionHandlers = [] this.session?.destroy() await this.initializeSession() this.session?.releaseInitialDataBuffer() diff --git a/terminus-terminal/src/api/baseTerminalTab.component.ts b/terminus-terminal/src/api/baseTerminalTab.component.ts index 560fafa5..0073c098 100644 --- a/terminus-terminal/src/api/baseTerminalTab.component.ts +++ b/terminus-terminal/src/api/baseTerminalTab.component.ts @@ -36,7 +36,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit ]), ])] - session?: BaseSession + session: BaseSession|null = null savedState?: any @Input() zoom = 0 @@ -95,6 +95,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit private bellPlayer: HTMLAudioElement private termContainerSubscriptions: Subscription[] = [] private allFocusModeSubscription: Subscription|null = null + private sessionHandlers: Subscription[] = [] get input$ (): Observable { if (!this.frontend) { @@ -568,26 +569,55 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit ] } + setSession (session: BaseSession|null, destroyOnSessionClose = false) { + if (session) { + if (this.session) { + this.setSession(null) + } + this.detachSessionHandlers() + this.session = session + this.attachSessionHandlers(destroyOnSessionClose) + } else { + this.detachSessionHandlers() + this.session = null + } + } + + protected attachSessionHandler (subscription: Subscription) { + this.sessionHandlers.push(subscription) + } + protected attachSessionHandlers (destroyOnSessionClose = false): void { if (!this.session) { throw new Error('Session not set') } // this.session.output$.bufferTime(10).subscribe((datas) => { - this.session.output$.subscribe(data => { + this.attachSessionHandler(this.session.output$.subscribe(data => { if (this.enablePassthrough) { this.zone.run(() => { this.output.next(data) this.write(data) }) } - }) + })) if (destroyOnSessionClose) { - this.sessionCloseSubscription = this.session.closed$.subscribe(() => { + this.attachSessionHandler(this.sessionCloseSubscription = this.session.closed$.subscribe(() => { this.frontend?.destroy() this.destroy() - }) + })) } + + this.attachSessionHandler(this.session.destroyed$.subscribe(() => { + this.setSession(null) + })) + } + + protected detachSessionHandlers () { + for (const s of this.sessionHandlers) { + s.unsubscribe() + } + this.sessionHandlers = [] } } diff --git a/terminus-terminal/src/services/terminalFrontend.service.ts b/terminus-terminal/src/services/terminalFrontend.service.ts index 1ab46b1c..a7341feb 100644 --- a/terminus-terminal/src/services/terminalFrontend.service.ts +++ b/terminus-terminal/src/services/terminalFrontend.service.ts @@ -16,7 +16,7 @@ export class TerminalFrontendService { private hotkeys: HotkeysService, ) { } - getFrontend (session?: BaseSession): Frontend { + getFrontend (session?: BaseSession|null): Frontend { if (!session) { const frontend: Frontend = new { xterm: XTermFrontend,