From bf762cc4c7511eaff91f2ab74523867e5dcd4988 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Mon, 5 Jul 2021 23:56:38 +0200 Subject: [PATCH] moved login scripts processing into tabby-terminal --- tabby-local/src/session.ts | 4 +- tabby-serial/src/api.ts | 83 ++---------------- .../serialProfileSettings.component.pug | 39 +-------- .../serialProfileSettings.component.ts | 49 +---------- tabby-ssh/src/api.ts | 81 ++--------------- .../sshProfileSettings.component.pug | 39 +-------- .../sshProfileSettings.component.ts | 51 +---------- .../telnetProfileSettings.component.pug | 39 ++++++--- tabby-telnet/src/profiles.ts | 4 +- tabby-telnet/src/session.ts | 13 +-- .../src/api/loginScriptProcessing.ts | 86 +++++++++++++++++++ tabby-terminal/src/api/streamProcessing.ts | 14 ++- .../loginScriptsSettings.component.pug | 38 ++++++++ .../loginScriptsSettings.component.ts | 56 ++++++++++++ tabby-terminal/src/index.ts | 4 + tabby-terminal/src/session.ts | 14 +++ 16 files changed, 270 insertions(+), 344 deletions(-) create mode 100644 tabby-terminal/src/api/loginScriptProcessing.ts create mode 100644 tabby-terminal/src/components/loginScriptsSettings.component.pug create mode 100644 tabby-terminal/src/components/loginScriptsSettings.component.ts diff --git a/tabby-local/src/session.ts b/tabby-local/src/session.ts index 7757c15b..8c942dae 100644 --- a/tabby-local/src/session.ts +++ b/tabby-local/src/session.ts @@ -2,7 +2,7 @@ import * as psNode from 'ps-node' import * as fs from 'mz/fs' import * as os from 'os' import { Injector } from '@angular/core' -import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA } from 'tabby-core' +import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core' import { BaseSession } from 'tabby-terminal' import { ipcRenderer } from 'electron' import { getWorkingDirectoryFromPID } from 'native-process-working-directory' @@ -97,7 +97,7 @@ export class Session extends BaseSession { private bootstrapData: BootstrapData constructor (injector: Injector) { - super() + super(injector.get(LogService).create('local')) this.config = injector.get(ConfigService) this.hostApp = injector.get(HostAppService) this.bootstrapData = injector.get(BOOTSTRAP_DATA) diff --git a/tabby-serial/src/api.ts b/tabby-serial/src/api.ts index a3e23f2b..03fcaadb 100644 --- a/tabby-serial/src/api.ts +++ b/tabby-serial/src/api.ts @@ -1,22 +1,15 @@ import stripAnsi from 'strip-ansi' import SerialPort from 'serialport' -import { Logger, LogService, NotificationsService, Profile } from 'tabby-core' +import { LogService, NotificationsService, Profile } from 'tabby-core' import { Subject, Observable } from 'rxjs' import { Injector, NgZone } from '@angular/core' -import { BaseSession, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' - -export interface LoginScript { - expect: string - send: string - isRegex?: boolean - optional?: boolean -} +import { BaseSession, LoginScriptsOptions, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' export interface SerialProfile extends Profile { options: SerialProfileOptions } -export interface SerialProfileOptions extends StreamProcessingOptions { +export interface SerialProfileOptions extends StreamProcessingOptions, LoginScriptsOptions { port: string baudrate?: number databits?: number @@ -26,7 +19,6 @@ export interface SerialProfileOptions extends StreamProcessingOptions { xon?: boolean xoff?: boolean xany?: boolean - scripts?: LoginScript[] color?: string } @@ -40,9 +32,7 @@ export interface SerialPortInfo { } export class SerialSession extends BaseSession { - scripts?: LoginScript[] serial: SerialPort - logger: Logger get serviceMessage$ (): Observable { return this.serviceMessage } private serviceMessage = new Subject() @@ -51,62 +41,20 @@ export class SerialSession extends BaseSession { private notifications: NotificationsService constructor (injector: Injector, public profile: SerialProfile) { - super() - - this.logger = injector.get(LogService).create(`serial-${profile.options.port}`) + super(injector.get(LogService).create(`serial-${profile.options.port}`)) this.zone = injector.get(NgZone) this.notifications = injector.get(NotificationsService) - this.scripts = profile.options.scripts ?? [] this.streamProcessor = new TerminalStreamProcessor(profile.options) this.streamProcessor.outputToSession$.subscribe(data => { this.serial?.write(data.toString()) }) this.streamProcessor.outputToTerminal$.subscribe(data => { this.emitOutput(data) - - const dataString = data.toString() - - 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 (re.test(dataString)) { - 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.loginScriptProcessor?.feedFromSession(data) }) + + this.setLoginScriptsOptions(profile.options) } async start (): Promise { @@ -151,6 +99,7 @@ export class SerialSession extends BaseSession { }) this.open = true + setTimeout(() => this.streamProcessor.start()) this.serial.on('readable', () => { this.streamProcessor.feedFromSession(this.serial.read()) @@ -163,7 +112,7 @@ export class SerialSession extends BaseSession { } }) - this.executeUnconditionalScripts() + this.loginScriptProcessor?.executeUnconditionalScripts() } write (data: Buffer): void { @@ -205,18 +154,4 @@ export class SerialSession extends BaseSession { async getWorkingDirectory (): Promise { return null } - - private executeUnconditionalScripts () { - if (this.scripts) { - for (const script of this.scripts) { - if (!script.expect) { - console.log('Executing script:', script.send) - this.serial.write(script.send + '\n') - this.scripts = this.scripts.filter(x => x !== script) - } else { - break - } - } - } - } } diff --git a/tabby-serial/src/components/serialProfileSettings.component.pug b/tabby-serial/src/components/serialProfileSettings.component.pug index f569ee4d..875362db 100644 --- a/tabby-serial/src/components/serialProfileSettings.component.pug +++ b/tabby-serial/src/components/serialProfileSettings.component.pug @@ -80,43 +80,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav') li(ngbNavItem) a(ngbNavLink) Login scripts ng-template(ngbNavContent) - table(*ngIf='profile.options.scripts.length > 0') - tr - th String to expect - th String to be sent - th.pl-2 Regex - th.pl-2 Optional - th.pl-2 Actions - tr(*ngFor='let script of profile.options.scripts') - td.pr-2 - input.form-control( - type='text', - [(ngModel)]='script.expect' - ) - td - input.form-control( - type='text', - [(ngModel)]='script.send' - ) - td.pl-2 - checkbox( - [(ngModel)]='script.isRegex', - ) - td.pl-2 - checkbox( - [(ngModel)]='script.optional', - ) - td.pl-2 - .input-group.flex-nowrap - button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') - i.fas.fa-arrow-up - button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') - i.fas.fa-arrow-down - button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') - i.fas.fa-trash - - button.btn.btn-outline-info.mt-2((click)='addScript()') - i.fas.fa-plus - span New item + login-scripts-settings([options]='profile.options') div([ngbNavOutlet]='nav') diff --git a/tabby-serial/src/components/serialProfileSettings.component.ts b/tabby-serial/src/components/serialProfileSettings.component.ts index 1a6d3f1f..2c91838f 100644 --- a/tabby-serial/src/components/serialProfileSettings.component.ts +++ b/tabby-serial/src/components/serialProfileSettings.component.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Component } from '@angular/core' import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators' -import { PlatformService, ProfileSettingsComponent } from 'tabby-core' -import { LoginScript, SerialPortInfo, BAUD_RATES, SerialProfile } from '../api' +import { ProfileSettingsComponent } from 'tabby-core' +import { SerialPortInfo, BAUD_RATES, SerialProfile } from '../api' import { SerialService } from '../services/serial.service' /** @hidden */ @@ -14,7 +14,6 @@ export class SerialProfileSettingsComponent implements ProfileSettingsComponent foundPorts: SerialPortInfo[] constructor ( - private platform: PlatformService, private serial: SerialService, ) { } @@ -40,50 +39,6 @@ export class SerialProfileSettingsComponent implements ProfileSettingsComponent } async ngOnInit () { - this.profile.options.scripts = this.profile.options.scripts ?? [] this.foundPorts = await this.serial.listPorts() } - - moveScriptUp (script: LoginScript) { - if (!this.profile.options.scripts) { - this.profile.options.scripts = [] - } - const index = this.profile.options.scripts.indexOf(script) - if (index > 0) { - this.profile.options.scripts.splice(index, 1) - this.profile.options.scripts.splice(index - 1, 0, script) - } - } - - moveScriptDown (script: LoginScript) { - if (!this.profile.options.scripts) { - this.profile.options.scripts = [] - } - const index = this.profile.options.scripts.indexOf(script) - if (index >= 0 && index < this.profile.options.scripts.length - 1) { - this.profile.options.scripts.splice(index, 1) - this.profile.options.scripts.splice(index + 1, 0, script) - } - } - - async deleteScript (script: LoginScript) { - if (this.profile.options.scripts && (await this.platform.showMessageBox( - { - type: 'warning', - message: 'Delete this script?', - detail: script.expect, - buttons: ['Keep', 'Delete'], - defaultId: 1, - } - )).response === 1) { - this.profile.options.scripts = this.profile.options.scripts.filter(x => x !== script) - } - } - - addScript () { - if (!this.profile.options.scripts) { - this.profile.options.scripts = [] - } - this.profile.options.scripts.push({ expect: '', send: '' }) - } } diff --git a/tabby-ssh/src/api.ts b/tabby-ssh/src/api.ts index 5f09dbdd..abd18139 100644 --- a/tabby-ssh/src/api.ts +++ b/tabby-ssh/src/api.ts @@ -10,8 +10,8 @@ import stripAnsi from 'strip-ansi' import socksv5 from 'socksv5' import { Injector, NgZone } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { ConfigService, FileProvidersService, HostAppService, Logger, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, Profile, LogService } from 'tabby-core' -import { BaseSession } from 'tabby-terminal' +import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, Profile, LogService } from 'tabby-core' +import { BaseSession, LoginScriptsOptions } from 'tabby-terminal' import { Server, Socket, createServer, createConnection } from 'net' import { Client, ClientChannel, SFTPWrapper } from 'ssh2' import type { FileEntry, Stats } from 'ssh2-streams' @@ -22,13 +22,6 @@ import { promisify } from 'util' const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent' -export interface LoginScript { - expect: string - send: string - isRegex?: boolean - optional?: boolean -} - export enum SSHAlgorithmType { HMAC = 'hmac', KEX = 'kex', @@ -40,14 +33,13 @@ export interface SSHProfile extends Profile { options: SSHProfileOptions } -export interface SSHProfileOptions { +export interface SSHProfileOptions extends LoginScriptsOptions { host: string port?: number user: string auth?: null|'password'|'publicKey'|'agent'|'keyboardInteractive' password?: string privateKeys?: string[] - scripts?: LoginScript[] keepaliveInterval?: number keepaliveCountMax?: number readyTimeout?: number @@ -255,12 +247,10 @@ export class SFTPSession { } export class SSHSession extends BaseSession { - scripts?: LoginScript[] shell?: ClientChannel ssh: Client sftp?: SFTPWrapper forwardedPorts: ForwardedPort[] = [] - logger: Logger jumpStream: any proxyCommandStream: ProxyCommandStream|null = null savedPassword?: string @@ -286,8 +276,7 @@ export class SSHSession extends BaseSession { injector: Injector, public profile: SSHProfile, ) { - super() - this.logger = injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`) + super(injector.get(LogService).create(`ssh-${profile.options.host}-${profile.options.port}`)) this.passwordStorage = injector.get(PasswordStorageService) this.ngbModal = injector.get(NgbModal) @@ -298,7 +287,6 @@ export class SSHSession extends BaseSession { this.fileProviders = injector.get(FileProvidersService) this.config = injector.get(ConfigService) - this.scripts = profile.options.scripts ?? [] this.destroyed$.subscribe(() => { for (const port of this.forwardedPorts) { if (port.type === PortForwardType.Local) { @@ -306,6 +294,8 @@ export class SSHSession extends BaseSession { } } }) + + this.setLoginScriptsOptions(profile.options) } async init (): Promise { @@ -389,6 +379,8 @@ export class SSHSession extends BaseSession { return } + this.loginScriptProcessor?.executeUnconditionalScripts() + this.shell.on('greeting', greeting => { this.emitServiceMessage(`Shell greeting: ${greeting}`) }) @@ -398,48 +390,7 @@ export class SSHSession extends BaseSession { }) this.shell.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.shell?.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.shell.on('end', () => { @@ -513,8 +464,6 @@ export class SSHSession extends BaseSession { }) }) }) - - this.executeUnconditionalScripts() } emitServiceMessage (msg: string): void { @@ -714,20 +663,6 @@ export class SSHSession extends BaseSession { }) } - private executeUnconditionalScripts () { - if (this.scripts) { - for (const script of this.scripts) { - if (!script.expect) { - console.log('Executing script:', script.send) - this.shell?.write(script.send + '\n') - this.scripts = this.scripts.filter(x => x !== script) - } else { - break - } - } - } - } - async loadPrivateKey (privateKeyContents?: Buffer): Promise { if (!privateKeyContents) { const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa') diff --git a/tabby-ssh/src/components/sshProfileSettings.component.pug b/tabby-ssh/src/components/sshProfileSettings.component.pug index cf5ebe96..1f55062f 100644 --- a/tabby-ssh/src/components/sshProfileSettings.component.pug +++ b/tabby-ssh/src/components/sshProfileSettings.component.pug @@ -189,43 +189,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav') li(ngbNavItem) a(ngbNavLink) Login scripts ng-template(ngbNavContent) - table(*ngIf='profile.options.scripts.length > 0') - tr - th String to expect - th String to be sent - th.pl-2 Regex - th.pl-2 Optional - th.pl-2 Actions - tr(*ngFor='let script of profile.options.scripts') - td.pr-2 - input.form-control( - type='text', - [(ngModel)]='script.expect' - ) - td - input.form-control( - type='text', - [(ngModel)]='script.send' - ) - td.pl-2 - checkbox( - [(ngModel)]='script.isRegex', - ) - td.pl-2 - checkbox( - [(ngModel)]='script.optional', - ) - td.pl-2 - .input-group.flex-nowrap - button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') - i.fas.fa-arrow-up - button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') - i.fas.fa-arrow-down - button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') - i.fas.fa-trash - - button.btn.btn-outline-info.mt-2((click)='addScript()') - i.fas.fa-plus - span New item + login-scripts-settings([options]='profile.options') div([ngbNavOutlet]='nav') diff --git a/tabby-ssh/src/components/sshProfileSettings.component.ts b/tabby-ssh/src/components/sshProfileSettings.component.ts index 0565bccc..e7a30c9e 100644 --- a/tabby-ssh/src/components/sshProfileSettings.component.ts +++ b/tabby-ssh/src/components/sshProfileSettings.component.ts @@ -2,9 +2,9 @@ import { Component } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { ConfigService, PlatformService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core' +import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core' import { PasswordStorageService } from '../services/passwordStorage.service' -import { LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST, SSHProfile } from '../api' +import { ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST, SSHProfile } from '../api' import * as ALGORITHMS from 'ssh2/lib/protocol/constants' /** @hidden */ @@ -23,9 +23,8 @@ export class SSHProfileSettingsComponent { jumpHosts: SSHProfile[] constructor ( - public config: ConfigService, public hostApp: HostAppService, - private platform: PlatformService, + private config: ConfigService, private passwordStorage: PasswordStorageService, private ngbModal: NgbModal, private fileProviders: FileProvidersService, @@ -63,7 +62,6 @@ export class SSHProfileSettingsComponent { } } - this.profile.options.scripts = this.profile.options.scripts ?? [] this.profile.options.auth = this.profile.options.auth ?? null this.profile.options.privateKeys ??= [] @@ -116,49 +114,6 @@ export class SSHProfileSettingsComponent { } } - moveScriptUp (script: LoginScript) { - if (!this.profile.options.scripts) { - this.profile.options.scripts = [] - } - const index = this.profile.options.scripts.indexOf(script) - if (index > 0) { - this.profile.options.scripts.splice(index, 1) - this.profile.options.scripts.splice(index - 1, 0, script) - } - } - - moveScriptDown (script: LoginScript) { - if (!this.profile.options.scripts) { - this.profile.options.scripts = [] - } - const index = this.profile.options.scripts.indexOf(script) - if (index >= 0 && index < this.profile.options.scripts.length - 1) { - this.profile.options.scripts.splice(index, 1) - this.profile.options.scripts.splice(index + 1, 0, script) - } - } - - async deleteScript (script: LoginScript) { - if (this.profile.options.scripts && (await this.platform.showMessageBox( - { - type: 'warning', - message: 'Delete this script?', - detail: script.expect, - buttons: ['Keep', 'Delete'], - defaultId: 1, - } - )).response === 1) { - this.profile.options.scripts = this.profile.options.scripts.filter(x => x !== script) - } - } - - addScript () { - if (!this.profile.options.scripts) { - this.profile.options.scripts = [] - } - this.profile.options.scripts.push({ expect: '', send: '' }) - } - onForwardAdded (fw: ForwardedPortConfig) { this.profile.options.forwardedPorts = this.profile.options.forwardedPorts ?? [] this.profile.options.forwardedPorts.push(fw) diff --git a/tabby-telnet/src/components/telnetProfileSettings.component.pug b/tabby-telnet/src/components/telnetProfileSettings.component.pug index cce09398..35cc448d 100644 --- a/tabby-telnet/src/components/telnetProfileSettings.component.pug +++ b/tabby-telnet/src/components/telnetProfileSettings.component.pug @@ -1,16 +1,27 @@ -.form-group - label Host - input.form-control( - type='text', - [(ngModel)]='profile.options.host', - ) +ul.nav-tabs(ngbNav, #nav='ngbNav') + li(ngbNavItem) + a(ngbNavLink) General + ng-template(ngbNavContent) + .form-group + label Host + input.form-control( + type='text', + [(ngModel)]='profile.options.host', + ) -.form-group - label Port - input.form-control( - type='number', - placeholder='22', - [(ngModel)]='profile.options.port', - ) + .form-group + label Port + input.form-control( + type='number', + placeholder='22', + [(ngModel)]='profile.options.port', + ) -stream-processing-settings([options]='profile.options') + stream-processing-settings([options]='profile.options') + + li(ngbNavItem) + a(ngbNavLink) Login scripts + ng-template(ngbNavContent) + login-scripts-settings([options]='profile.options') + +div([ngbNavOutlet]='nav') diff --git a/tabby-telnet/src/profiles.ts b/tabby-telnet/src/profiles.ts index 76e36c68..58bdf115 100644 --- a/tabby-telnet/src/profiles.ts +++ b/tabby-telnet/src/profiles.ts @@ -20,7 +20,7 @@ export class TelnetProfilesService extends ProfileProvider { options: { host: '', port: 23, - inputMode: 'local-echo', + inputMode: 'readline', outputMode: null, inputNewlines: null, outputNewlines: 'crlf', @@ -58,7 +58,7 @@ export class TelnetProfilesService extends ProfileProvider { options: { host, port, - inputMode: 'local-echo', + inputMode: 'readline', outputNewlines: 'crlf', }, } diff --git a/tabby-telnet/src/session.ts b/tabby-telnet/src/session.ts index 63ba130a..30e5bca1 100644 --- a/tabby-telnet/src/session.ts +++ b/tabby-telnet/src/session.ts @@ -2,8 +2,8 @@ import { Socket } from 'net' import colors from 'ansi-colors' import stripAnsi from 'strip-ansi' import { Injector } from '@angular/core' -import { Logger, Profile, LogService } from 'tabby-core' -import { BaseSession, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' +import { Profile, LogService } from 'tabby-core' +import { BaseSession, LoginScriptsOptions, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal' import { Subject, Observable } from 'rxjs' @@ -11,13 +11,12 @@ export interface TelnetProfile extends Profile { options: TelnetProfileOptions } -export interface TelnetProfileOptions extends StreamProcessingOptions { +export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScriptsOptions { host: string port?: number } export class TelnetSession extends BaseSession { - logger: Logger get serviceMessage$ (): Observable { return this.serviceMessage } private serviceMessage = new Subject() @@ -28,8 +27,7 @@ export class TelnetSession extends BaseSession { injector: Injector, public profile: TelnetProfile, ) { - super() - this.logger = injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`) + super(injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`)) this.streamProcessor = new TerminalStreamProcessor(profile.options) this.streamProcessor.outputToSession$.subscribe(data => { this.socket.write(data) @@ -37,6 +35,7 @@ export class TelnetSession extends BaseSession { this.streamProcessor.outputToTerminal$.subscribe(data => { this.emitOutput(data) }) + this.setLoginScriptsOptions(profile.options) } async start (): Promise { @@ -57,6 +56,8 @@ export class TelnetSession extends BaseSession { this.socket.connect(this.profile.options.port ?? 23, this.profile.options.host, () => { this.emitServiceMessage('Connected') this.open = true + setTimeout(() => this.streamProcessor.start()) + this.loginScriptProcessor?.executeUnconditionalScripts() resolve() }) }) diff --git a/tabby-terminal/src/api/loginScriptProcessing.ts b/tabby-terminal/src/api/loginScriptProcessing.ts new file mode 100644 index 00000000..fd4488c7 --- /dev/null +++ b/tabby-terminal/src/api/loginScriptProcessing.ts @@ -0,0 +1,86 @@ +import { Subject, Observable } from 'rxjs' +import { Logger } from 'tabby-core' + +export interface LoginScript { + expect: string + send: string + isRegex?: boolean + optional?: boolean +} + +export interface LoginScriptsOptions { + scripts?: LoginScript[] +} + +export class LoginScriptProcessor { + get outputToSession$ (): Observable { return this.outputToSession } + + private outputToSession = new Subject() + private remainingScripts: LoginScript[] = [] + + constructor ( + private logger: Logger, + options: LoginScriptsOptions + ) { + this.remainingScripts = options.scripts ?? [] + } + + feedFromSession (data: Buffer): boolean { + const dataString = data.toString() + + let found = false + for (const script of this.remainingScripts) { + if (!script.expect) { + continue + } + let match = false + let cmd = '' + if (script.isRegex) { + const re = new RegExp(script.expect, 'g') + if (re.exec(dataString)) { + 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.outputToSession.next(Buffer.from(cmd + '\n')) + this.remainingScripts = this.remainingScripts.filter(x => x !== script) + } else { + if (script.optional) { + this.logger.debug('Skip optional script: ' + script.expect) + found = true + this.remainingScripts = this.remainingScripts.filter(x => x !== script) + } else { + break + } + } + } + + return found + } + + close (): void { + this.outputToSession.complete() + } + + executeUnconditionalScripts (): void { + for (const script of this.remainingScripts) { + if (!script.expect) { + this.logger.info('Executing script:', script.send) + this.outputToSession.next(Buffer.from(script.send + '\n')) + this.remainingScripts = this.remainingScripts.filter(x => x !== script) + } else { + break + } + } + } +} diff --git a/tabby-terminal/src/api/streamProcessing.ts b/tabby-terminal/src/api/streamProcessing.ts index 03fdc6c0..194f883b 100644 --- a/tabby-terminal/src/api/streamProcessing.ts +++ b/tabby-terminal/src/api/streamProcessing.ts @@ -26,9 +26,10 @@ export class TerminalStreamProcessor { protected outputToTerminal = new Subject() private inputReadline: ReadLine - private inputPromptVisible = true + private inputPromptVisible = false private inputReadlineInStream: Readable & Writable private inputReadlineOutStream: Readable & Writable + private started = false constructor (private options: StreamProcessingOptions) { this.inputReadlineInStream = new PassThrough() @@ -46,7 +47,16 @@ export class TerminalStreamProcessor { this.onTerminalInput(Buffer.from(line + '\n')) this.resetInputPrompt() }) - this.outputToTerminal$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled()) + this.outputToTerminal$.pipe(debounce(() => interval(500))).subscribe(() => { + if (this.started) { + this.onOutputSettled() + } + }) + } + + start (): void { + this.started = true + this.onOutputSettled() } feedFromSession (data: Buffer): void { diff --git a/tabby-terminal/src/components/loginScriptsSettings.component.pug b/tabby-terminal/src/components/loginScriptsSettings.component.pug new file mode 100644 index 00000000..f6eba851 --- /dev/null +++ b/tabby-terminal/src/components/loginScriptsSettings.component.pug @@ -0,0 +1,38 @@ +table(*ngIf='options.scripts.length > 0') + tr + th String to expect + th String to be sent + th.pl-2 Regex + th.pl-2 Optional + th.pl-2 Actions + tr(*ngFor='let script of options.scripts') + td.pr-2 + input.form-control( + type='text', + [(ngModel)]='script.expect' + ) + td + input.form-control( + type='text', + [(ngModel)]='script.send' + ) + td.pl-2 + checkbox( + [(ngModel)]='script.isRegex', + ) + td.pl-2 + checkbox( + [(ngModel)]='script.optional', + ) + td.pl-2 + .input-group.flex-nowrap + button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') + i.fas.fa-arrow-up + button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') + i.fas.fa-arrow-down + button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') + i.fas.fa-trash + +button.btn.btn-outline-info.mt-2((click)='addScript()') + i.fas.fa-plus + span New item diff --git a/tabby-terminal/src/components/loginScriptsSettings.component.ts b/tabby-terminal/src/components/loginScriptsSettings.component.ts new file mode 100644 index 00000000..33f54b11 --- /dev/null +++ b/tabby-terminal/src/components/loginScriptsSettings.component.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { Component, Input } from '@angular/core' + +import { PlatformService } from 'tabby-core' +import { LoginScript, LoginScriptsOptions } from '../api/loginScriptProcessing' + +/** @hidden */ +@Component({ + selector: 'login-scripts-settings', + template: require('./loginScriptsSettings.component.pug'), +}) +export class LoginScriptsSettingsComponent { + @Input() options: LoginScriptsOptions + + constructor ( + private platform: PlatformService, + ) { } + + ngOnInit () { + this.options.scripts ??= [] + } + + moveScriptUp (script: LoginScript) { + const index = this.options.scripts!.indexOf(script) + if (index > 0) { + this.options.scripts!.splice(index, 1) + this.options.scripts!.splice(index - 1, 0, script) + } + } + + moveScriptDown (script: LoginScript) { + const index = this.options.scripts!.indexOf(script) + if (index >= 0 && index < this.options.scripts!.length - 1) { + this.options.scripts!.splice(index, 1) + this.options.scripts!.splice(index + 1, 0, script) + } + } + + async deleteScript (script: LoginScript) { + if ((await this.platform.showMessageBox( + { + type: 'warning', + message: 'Delete this script?', + detail: script.expect, + buttons: ['Keep', 'Delete'], + defaultId: 1, + } + )).response === 1) { + this.options.scripts = this.options.scripts!.filter(x => x !== script) + } + } + + addScript () { + this.options.scripts!.push({ expect: '', send: '' }) + } +} diff --git a/tabby-terminal/src/index.ts b/tabby-terminal/src/index.ts index 15a3de9c..1eaf3437 100644 --- a/tabby-terminal/src/index.ts +++ b/tabby-terminal/src/index.ts @@ -14,6 +14,7 @@ import { ColorPickerComponent } from './components/colorPicker.component' import { ColorSchemePreviewComponent } from './components/colorSchemePreview.component' import { SearchPanelComponent } from './components/searchPanel.component' import { StreamProcessingSettingsComponent } from './components/streamProcessingSettings.component' +import { LoginScriptsSettingsComponent } from './components/loginScriptsSettings.component' import { TerminalFrontendService } from './services/terminalFrontend.service' @@ -72,11 +73,13 @@ import { TerminalCLIHandler } from './cli' TerminalSettingsTabComponent, SearchPanelComponent, StreamProcessingSettingsComponent, + LoginScriptsSettingsComponent, ] as any[], exports: [ ColorPickerComponent, SearchPanelComponent, StreamProcessingSettingsComponent, + LoginScriptsSettingsComponent, ], }) export default class TerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class @@ -115,4 +118,5 @@ export { Frontend, XTermFrontend, XTermWebGLFrontend, HTermFrontend } export { BaseTerminalTabComponent } from './api/baseTerminalTab.component' export * from './api/interfaces' export * from './api/streamProcessing' +export * from './api/loginScriptProcessing' export * from './session' diff --git a/tabby-terminal/src/session.ts b/tabby-terminal/src/session.ts index 5473ddc4..628f26a4 100644 --- a/tabby-terminal/src/session.ts +++ b/tabby-terminal/src/session.ts @@ -1,4 +1,6 @@ import { Observable, Subject } from 'rxjs' +import { Logger } from 'tabby-core' +import { LoginScriptProcessor, LoginScriptsOptions } from './api/loginScriptProcessing' /** * A session object for a [[BaseTerminalTabComponent]] @@ -12,6 +14,7 @@ export abstract class BaseSession { protected binaryOutput = new Subject() protected closed = new Subject() protected destroyed = new Subject() + protected loginScriptProcessor: LoginScriptProcessor | null = null private initialDataBuffer = Buffer.from('') private initialDataBufferReleased = false @@ -20,12 +23,15 @@ export abstract class BaseSession { get closed$ (): Observable { return this.closed } get destroyed$ (): Observable { return this.destroyed } + constructor (protected logger: Logger) { } + emitOutput (data: Buffer): void { if (!this.initialDataBufferReleased) { this.initialDataBuffer = Buffer.concat([this.initialDataBuffer, data]) } else { this.output.next(data.toString()) this.binaryOutput.next(data) + this.loginScriptProcessor?.feedFromSession(data) } } @@ -36,9 +42,17 @@ export abstract class BaseSession { this.initialDataBuffer = Buffer.from('') } + setLoginScriptsOptions (options: LoginScriptsOptions): void { + this.loginScriptProcessor?.close() + this.loginScriptProcessor = new LoginScriptProcessor(this.logger, options) + this.loginScriptProcessor.outputToSession$.subscribe(data => this.write(data)) + } + async destroy (): Promise { if (this.open) { + this.logger.info('Destroying') this.open = false + this.loginScriptProcessor?.close() this.closed.next() this.destroyed.next() await this.gracefullyKillProcess()