Parcourir la source

moved login scripts processing into tabby-terminal

Eugene Pankov il y a 4 ans
Parent
commit
bf762cc4c7

+ 2 - 2
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)

+ 9 - 74
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<string> { return this.serviceMessage }
     private serviceMessage = new Subject<string>()
@@ -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<void> {
@@ -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<string|null> {
         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
-                }
-            }
-        }
-    }
 }

+ 1 - 38
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')

+ 2 - 47
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: '' })
-    }
 }

+ 8 - 73
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<void> {
@@ -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<string|null> {
         if (!privateKeyContents) {
             const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')

+ 1 - 38
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')

+ 3 - 48
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)

+ 25 - 14
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')

+ 2 - 2
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',
             },
         }

+ 7 - 6
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<string> { return this.serviceMessage }
 
     private serviceMessage = new Subject<string>()
@@ -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<void> {
@@ -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()
             })
         })

+ 86 - 0
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<Buffer> { return this.outputToSession }
+
+    private outputToSession = new Subject<Buffer>()
+    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
+            }
+        }
+    }
+}

+ 12 - 2
tabby-terminal/src/api/streamProcessing.ts

@@ -26,9 +26,10 @@ export class TerminalStreamProcessor {
     protected outputToTerminal = new Subject<Buffer>()
 
     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 {

+ 38 - 0
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

+ 56 - 0
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: '' })
+    }
+}

+ 4 - 0
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'

+ 14 - 0
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<Buffer>()
     protected closed = new Subject<void>()
     protected destroyed = new Subject<void>()
+    protected loginScriptProcessor: LoginScriptProcessor | null = null
     private initialDataBuffer = Buffer.from('')
     private initialDataBufferReleased = false
 
@@ -20,12 +23,15 @@ export abstract class BaseSession {
     get closed$ (): Observable<void> { return this.closed }
     get destroyed$ (): Observable<void> { 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<void> {
         if (this.open) {
+            this.logger.info('Destroying')
             this.open = false
+            this.loginScriptProcessor?.close()
             this.closed.next()
             this.destroyed.next()
             await this.gracefullyKillProcess()