moved login scripts processing into tabby-terminal

This commit is contained in:
Eugene Pankov 2021-07-05 23:56:38 +02:00
parent 461cd2bec7
commit bf762cc4c7
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
16 changed files with 270 additions and 344 deletions

View file

@ -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)

View file

@ -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
}
}
}
}
}

View file

@ -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')

View file

@ -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: '' })
}
}

View file

@ -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')

View file

@ -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')

View file

@ -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)

View file

@ -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')

View file

@ -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',
},
}

View file

@ -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()
})
})

View file

@ -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
}
}
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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: '' })
}
}

View file

@ -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'

View file

@ -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()