mirror of
https://github.com/Eugeny/tabby
synced 2024-11-14 17:07:15 +00:00
moved login scripts processing into tabby-terminal
This commit is contained in:
parent
461cd2bec7
commit
bf762cc4c7
16 changed files with 270 additions and 344 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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: '' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
tabby-terminal/src/api/loginScriptProcessing.ts
Normal file
86
tabby-terminal/src/api/loginScriptProcessing.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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: '' })
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue