mirror of
https://github.com/Eugeny/tabby.git
synced 2025-10-04 22:14:55 +00:00
simpler tab recovery system
This commit is contained in:
@@ -41,8 +41,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@terminus-term/node-pty": "0.7.8",
|
||||
"@types/async-lock": "0.0.19",
|
||||
"async-lock": "^1.0.0",
|
||||
"font-manager": "0.3.0",
|
||||
"hterm-umdjs": "1.4.1",
|
||||
"mz": "^2.6.0",
|
||||
|
@@ -21,21 +21,9 @@ export interface SessionOptions {
|
||||
env?: any
|
||||
width?: number
|
||||
height?: number
|
||||
recoveryId?: string
|
||||
recoveredTruePID$?: Observable<number>
|
||||
pauseAfterExit?: boolean
|
||||
}
|
||||
|
||||
export abstract class SessionPersistenceProvider {
|
||||
abstract id: string
|
||||
abstract displayName: string
|
||||
|
||||
abstract isAvailable (): boolean
|
||||
abstract async attachSession (recoveryId: any): Promise<SessionOptions>
|
||||
abstract async startSession (options: SessionOptions): Promise<any>
|
||||
abstract async terminateSession (recoveryId: string): Promise<void>
|
||||
}
|
||||
|
||||
export interface ITerminalColorScheme {
|
||||
name: string
|
||||
foreground: string
|
||||
|
@@ -27,20 +27,6 @@ h3.mb-3 Shell
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
|
||||
.form-line(*ngIf='persistenceProviders.length > 0')
|
||||
.header
|
||||
.title Session persistence
|
||||
.description Restores tabs when Terminus is restarted
|
||||
select.form-control(
|
||||
[(ngModel)]='config.store.terminal.persistence',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option([ngValue]='null') Off
|
||||
option(
|
||||
*ngFor='let provider of persistenceProviders',
|
||||
[ngValue]='provider.id'
|
||||
) {{provider.displayName}}
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Working directory
|
||||
|
@@ -1,14 +1,13 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ConfigService, ElectronService } from 'terminus-core'
|
||||
import { IShell, ShellProvider, SessionPersistenceProvider } from '../api'
|
||||
import { IShell, ShellProvider } from '../api'
|
||||
|
||||
@Component({
|
||||
template: require('./shellSettingsTab.component.pug'),
|
||||
})
|
||||
export class ShellSettingsTabComponent {
|
||||
shells: IShell[] = []
|
||||
persistenceProviders: SessionPersistenceProvider[]
|
||||
|
||||
environmentVars: {key: string, value: string}[] = []
|
||||
private configSubscription: Subscription
|
||||
@@ -17,10 +16,7 @@ export class ShellSettingsTabComponent {
|
||||
public config: ConfigService,
|
||||
private electron: ElectronService,
|
||||
@Inject(ShellProvider) private shellProviders: ShellProvider[],
|
||||
@Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[],
|
||||
) {
|
||||
this.persistenceProviders = this.config.enabledServices(persistenceProviders).filter(x => x.isAvailable())
|
||||
|
||||
config.store.terminal.environment = config.store.terminal.environment || {}
|
||||
this.reloadEnvironment()
|
||||
this.configSubscription = config.changed$.subscribe(() => this.reloadEnvironment())
|
||||
|
@@ -61,7 +61,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.decorators = this.decorators || []
|
||||
this.setTitle('Terminal')
|
||||
|
||||
this.session = new Session()
|
||||
this.session = new Session(this.config)
|
||||
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
@@ -143,10 +143,14 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
})
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
let cwd = this.session ? await this.session.getWorkingDirectory() : null
|
||||
return {
|
||||
type: 'app:terminal',
|
||||
recoveryId: this.sessionOptions.recoveryId,
|
||||
type: 'app:terminal-tab',
|
||||
sessionOptions: {
|
||||
...this.sessionOptions,
|
||||
cwd: cwd || this.sessionOptions.cwd,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -59,7 +59,6 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
terminal: {
|
||||
font: 'Menlo',
|
||||
shell: 'default',
|
||||
persistence: 'screen',
|
||||
},
|
||||
hotkeys: {
|
||||
'ctrl-c': ['Ctrl-C'],
|
||||
@@ -100,7 +99,6 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
terminal: {
|
||||
font: 'Consolas',
|
||||
shell: 'clink',
|
||||
persistence: null,
|
||||
rightClick: 'paste',
|
||||
copyOnSelect: true,
|
||||
},
|
||||
@@ -143,7 +141,6 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
terminal: {
|
||||
font: 'Liberation Mono',
|
||||
shell: 'default',
|
||||
persistence: 'tmux',
|
||||
},
|
||||
hotkeys: {
|
||||
'ctrl-c': ['Ctrl-C'],
|
||||
|
@@ -21,11 +21,9 @@ import { SessionsService, BaseSession } from './services/sessions.service'
|
||||
import { TerminalFrontendService } from './services/terminalFrontend.service'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
|
||||
import { ScreenPersistenceProvider } from './persistence/screen'
|
||||
import { TMuxPersistenceProvider } from './persistence/tmux'
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { RecoveryProvider } from './recoveryProvider'
|
||||
import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api'
|
||||
import { TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api'
|
||||
import { TerminalSettingsTabProvider, AppearanceSettingsTabProvider, ShellSettingsTabProvider } from './settings'
|
||||
import { PathDropDecorator } from './pathDrop'
|
||||
import { TerminalConfigProvider } from './config'
|
||||
@@ -71,9 +69,6 @@ import { hterm } from './hterm'
|
||||
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
||||
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
|
||||
|
||||
{ provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider, multi: true },
|
||||
{ provide: SessionPersistenceProvider, useClass: TMuxPersistenceProvider, multi: true },
|
||||
|
||||
{ provide: ShellProvider, useClass: WindowsDefaultShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true },
|
||||
|
@@ -1,142 +0,0 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { exec, spawn } from 'mz/child_process'
|
||||
import { exec as execAsync, execFileSync } from 'child_process'
|
||||
|
||||
import { AsyncSubject } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService, ElectronService } from 'terminus-core'
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
declare function delay (ms: number): Promise<void>
|
||||
|
||||
interface IChildProcess {
|
||||
pid: number
|
||||
ppid: number
|
||||
command: string
|
||||
}
|
||||
|
||||
async function listProcesses (): Promise<IChildProcess[]> {
|
||||
return (await exec(`ps -A -o pid,ppid,command`))[0].toString()
|
||||
.split('\n')
|
||||
.slice(1)
|
||||
.map(line => line.split(' ').filter(x => x).slice(0, 3))
|
||||
.map(([pid, ppid, command]) => {
|
||||
return {
|
||||
pid: parseInt(pid), ppid: parseInt(ppid), command
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ScreenPersistenceProvider extends SessionPersistenceProvider {
|
||||
id = 'screen'
|
||||
displayName = 'GNU Screen'
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
log: LogService,
|
||||
private electron: ElectronService,
|
||||
) {
|
||||
super()
|
||||
this.logger = log.create('main')
|
||||
}
|
||||
|
||||
isAvailable () {
|
||||
try {
|
||||
execFileSync('sh', ['-c', 'which screen'])
|
||||
return true
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async attachSession (recoveryId: any): Promise<SessionOptions> {
|
||||
let lines = await new Promise<string[]>(resolve => {
|
||||
execAsync('screen -list', (_err, stdout) => {
|
||||
// returns an error code on macOS
|
||||
resolve(stdout.split('\n'))
|
||||
})
|
||||
})
|
||||
let screenPID = lines
|
||||
.filter(line => line.indexOf('.' + recoveryId) !== -1)
|
||||
.map(line => parseInt(line.trim().split('.')[0]))[0]
|
||||
|
||||
if (!screenPID) {
|
||||
return null
|
||||
}
|
||||
|
||||
let truePID$ = new AsyncSubject<number>()
|
||||
|
||||
this.extractShellPID(screenPID).then(pid => {
|
||||
truePID$.next(pid)
|
||||
truePID$.complete()
|
||||
})
|
||||
|
||||
return {
|
||||
recoveryId,
|
||||
recoveredTruePID$: truePID$.asObservable(),
|
||||
command: 'screen',
|
||||
args: ['-d', '-r', recoveryId, '-c', await this.prepareConfig()],
|
||||
}
|
||||
}
|
||||
|
||||
async extractShellPID (screenPID: number): Promise<number> {
|
||||
let processes = await listProcesses()
|
||||
let child = processes.find(x => x.ppid === screenPID)
|
||||
|
||||
if (!child) {
|
||||
throw new Error(`Could not find any children of the screen process (PID ${screenPID})!`)
|
||||
}
|
||||
|
||||
if (child.command === 'login') {
|
||||
await delay(1000)
|
||||
child = processes.find(x => x.ppid === child.pid)
|
||||
}
|
||||
|
||||
return child.pid
|
||||
}
|
||||
|
||||
async startSession (options: SessionOptions): Promise<any> {
|
||||
let recoveryId = `term-tab-${Date.now()}`
|
||||
let args = ['-d', '-m', '-c', await this.prepareConfig(), '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', '-' + options.command].concat(options.args || [])
|
||||
this.logger.debug('Spawning screen with', args.join(' '))
|
||||
await spawn('screen', args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env || process.env,
|
||||
})
|
||||
return recoveryId
|
||||
}
|
||||
|
||||
async terminateSession (recoveryId: string): Promise<void> {
|
||||
try {
|
||||
await exec(`screen -S ${recoveryId} -X quit`)
|
||||
} catch (_) {
|
||||
// screen has already quit
|
||||
}
|
||||
}
|
||||
|
||||
private async prepareConfig (): Promise<string> {
|
||||
let configPath = path.join(this.electron.app.getPath('userData'), 'screen-config.tmp')
|
||||
await fs.writeFile(configPath, `
|
||||
escape ^^^
|
||||
vbell off
|
||||
deflogin on
|
||||
defflow off
|
||||
term xterm-color
|
||||
bindkey "^[OH" beginning-of-line
|
||||
bindkey "^[OF" end-of-line
|
||||
bindkey "^[[H" beginning-of-line
|
||||
bindkey "^[[F" end-of-line
|
||||
bindkey "\\027[?1049h" stuff ----alternate enter-----
|
||||
bindkey "\\027[?1049l" stuff ----alternate leave-----
|
||||
termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007'
|
||||
defhstatus "^Et"
|
||||
hardstatus off
|
||||
altscreen on
|
||||
defutf8 on
|
||||
defencoding utf8
|
||||
`, 'utf-8')
|
||||
return configPath
|
||||
}
|
||||
}
|
@@ -1,248 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { execFileSync } from 'child_process'
|
||||
import AsyncLock = require('async-lock')
|
||||
import { ConnectableObservable, AsyncSubject, Subject } from 'rxjs'
|
||||
import { first, publish } from 'rxjs/operators'
|
||||
import * as childProcess from 'child_process'
|
||||
|
||||
import { Logger } from 'terminus-core'
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
declare function delay (ms: number): Promise<void>
|
||||
|
||||
const TMUX_CONFIG = `
|
||||
set -g status off
|
||||
set -g focus-events on
|
||||
set -g bell-action any
|
||||
set -g bell-on-alert on
|
||||
set -g visual-bell off
|
||||
set -g set-titles on
|
||||
set -g set-titles-string "#W"
|
||||
set -g window-status-format '#I:#(pwd="#{pane_current_path}"; echo \${pwd####*/})#F'
|
||||
set -g window-status-current-format '#I:#(pwd="#{pane_current_path}"; echo \${pwd####*/})#F'
|
||||
set-option -g prefix C-^
|
||||
set-option -g status-interval 1
|
||||
`
|
||||
|
||||
export class TMuxBlock {
|
||||
time: number
|
||||
number: number
|
||||
error: boolean
|
||||
lines: string[]
|
||||
|
||||
constructor (line: string) {
|
||||
this.time = parseInt(line.split(' ')[1])
|
||||
this.number = parseInt(line.split(' ')[2])
|
||||
this.lines = []
|
||||
}
|
||||
}
|
||||
|
||||
export class TMuxMessage {
|
||||
type: string
|
||||
content: string
|
||||
|
||||
constructor (line: string) {
|
||||
this.type = line.substring(0, line.indexOf(' '))
|
||||
this.content = line.substring(line.indexOf(' ') + 1)
|
||||
}
|
||||
}
|
||||
|
||||
export class TMuxCommandProcess {
|
||||
private process: childProcess.ChildProcess
|
||||
private rawOutput$ = new Subject<string>()
|
||||
private line$ = new Subject<string>()
|
||||
private message$ = new Subject<string>()
|
||||
private block$ = new Subject<TMuxBlock>()
|
||||
private response$: ConnectableObservable<TMuxBlock>
|
||||
private lock = new AsyncLock({ timeout: 1000 })
|
||||
private logger = new Logger(null, 'tmuxProcess')
|
||||
|
||||
constructor () {
|
||||
this.process = childProcess.spawn('tmux', ['-C', '-f', '/dev/null', '-L', 'terminus', 'new-session', '-A', '-D', '-s', 'control'])
|
||||
this.logger.log('started')
|
||||
this.process.stdout.on('data', data => {
|
||||
// console.debug('tmux says:', data.toString())
|
||||
this.rawOutput$.next(data.toString())
|
||||
})
|
||||
|
||||
let rawBuffer = ''
|
||||
this.rawOutput$.subscribe(raw => {
|
||||
rawBuffer += raw
|
||||
if (rawBuffer.includes('\n')) {
|
||||
let lines = rawBuffer.split('\n')
|
||||
rawBuffer = lines.pop()
|
||||
lines.forEach(line => this.line$.next(line))
|
||||
}
|
||||
})
|
||||
|
||||
let currentBlock = null
|
||||
this.line$.subscribe(line => {
|
||||
if (currentBlock) {
|
||||
if (line.startsWith('%end ')) {
|
||||
let block = currentBlock
|
||||
currentBlock = null
|
||||
setImmediate(() => {
|
||||
this.block$.next(block)
|
||||
})
|
||||
} else if (line.startsWith('%error ')) {
|
||||
let block = currentBlock
|
||||
block.error = true
|
||||
currentBlock = null
|
||||
setImmediate(() => {
|
||||
this.block$.next(block)
|
||||
})
|
||||
} else {
|
||||
currentBlock.lines.push(line)
|
||||
}
|
||||
} else {
|
||||
if (line.startsWith('%begin ')) {
|
||||
currentBlock = new TMuxBlock(line)
|
||||
} else {
|
||||
this.message$.next(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.response$ = this.block$.asObservable().pipe(publish()) as ConnectableObservable<TMuxBlock>
|
||||
this.response$.connect()
|
||||
|
||||
this.block$.subscribe(block => {
|
||||
this.logger.debug('block:', block)
|
||||
})
|
||||
|
||||
this.message$.subscribe(message => {
|
||||
this.logger.debug('message:', message)
|
||||
})
|
||||
}
|
||||
|
||||
command (command: string): Promise<TMuxBlock> {
|
||||
return this.lock.acquire('key', () => {
|
||||
let p = this.response$.pipe(first()).toPromise()
|
||||
this.logger.debug('command:', command)
|
||||
this.process.stdin.write(command + '\n')
|
||||
return p
|
||||
}).then(response => {
|
||||
if (response.error) {
|
||||
throw response
|
||||
}
|
||||
return response
|
||||
}) as Promise<TMuxBlock>
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.rawOutput$.complete()
|
||||
this.line$.complete()
|
||||
this.block$.complete()
|
||||
this.message$.complete()
|
||||
this.process.kill('SIGTERM')
|
||||
}
|
||||
}
|
||||
|
||||
export class TMux {
|
||||
private process: TMuxCommandProcess
|
||||
private ready: Promise<void>
|
||||
private logger = new Logger(null, 'tmux')
|
||||
|
||||
constructor () {
|
||||
this.process = new TMuxCommandProcess()
|
||||
this.ready = (async () => {
|
||||
for (let line of TMUX_CONFIG.split('\n')) {
|
||||
if (line) {
|
||||
try {
|
||||
await this.process.command(line)
|
||||
} catch (e) {
|
||||
this.logger.warn('Skipping failing config line:', line)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tmux sometimes sends a stray response block at start
|
||||
await delay(500)
|
||||
})()
|
||||
}
|
||||
|
||||
async create (id: string, options: SessionOptions): Promise<void> {
|
||||
await this.ready
|
||||
let args = [options.command].concat(options.args.slice(1))
|
||||
let cmd = args.map(x => `"${x.replace('"', '\\"')}"`).join(' ')
|
||||
await this.process.command(
|
||||
`new-session -s "${id}" -d`
|
||||
+ (options.cwd ? ` -c '${options.cwd.replace("'", "\\'")}'` : '')
|
||||
+ ` '${cmd}'`
|
||||
)
|
||||
}
|
||||
|
||||
async list (): Promise<string[]> {
|
||||
await this.ready
|
||||
let block = await this.process.command('list-sessions -F "#{session_name}"')
|
||||
return block.lines
|
||||
}
|
||||
|
||||
async getPID (id: string): Promise<number|null> {
|
||||
await this.ready
|
||||
let response = await this.process.command(`list-panes -t ${id} -F "#{pane_pid}"`)
|
||||
if (response.lines.length === 0) {
|
||||
return null
|
||||
} else {
|
||||
return parseInt(response.lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
async terminate (id: string): Promise<void> {
|
||||
await this.ready
|
||||
this.process.command(`kill-session -t ${id}`).catch(() => {
|
||||
console.debug('Session already killed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TMuxPersistenceProvider extends SessionPersistenceProvider {
|
||||
id = 'tmux'
|
||||
displayName = 'Tmux'
|
||||
private tmux: TMux
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
if (this.isAvailable()) {
|
||||
this.tmux = new TMux()
|
||||
}
|
||||
}
|
||||
|
||||
isAvailable (): boolean {
|
||||
try {
|
||||
execFileSync('tmux', ['-V'])
|
||||
return true
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async attachSession (recoveryId: any): Promise<SessionOptions> {
|
||||
let sessions = await this.tmux.list()
|
||||
if (!sessions.includes(recoveryId)) {
|
||||
return null
|
||||
}
|
||||
let truePID$ = new AsyncSubject<number>()
|
||||
this.tmux.getPID(recoveryId).then(pid => {
|
||||
truePID$.next(pid)
|
||||
truePID$.complete()
|
||||
})
|
||||
return {
|
||||
command: 'tmux',
|
||||
args: ['-L', 'terminus', 'attach-session', '-d', '-t', recoveryId, ';', 'refresh-client'],
|
||||
recoveredTruePID$: truePID$.asObservable(),
|
||||
recoveryId,
|
||||
}
|
||||
}
|
||||
|
||||
async startSession (options: SessionOptions): Promise<any> {
|
||||
// TODO env
|
||||
let recoveryId = Date.now().toString()
|
||||
await this.tmux.create(recoveryId, options)
|
||||
return recoveryId
|
||||
}
|
||||
|
||||
async terminateSession (recoveryId: string): Promise<void> {
|
||||
await this.tmux.terminate(recoveryId)
|
||||
}
|
||||
}
|
@@ -2,25 +2,20 @@ import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
||||
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { SessionsService } from './services/sessions.service'
|
||||
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
constructor (
|
||||
private sessions: SessionsService,
|
||||
// private sessions: SessionsService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken.type === 'app:terminal') {
|
||||
let sessionOptions = await this.sessions.recover(recoveryToken.recoveryId)
|
||||
if (!sessionOptions) {
|
||||
return null
|
||||
}
|
||||
if (recoveryToken.type === 'app:terminal-tab') {
|
||||
return {
|
||||
type: TerminalTabComponent,
|
||||
options: { sessionOptions },
|
||||
options: { sessionOptions: recoveryToken.sessionOptions },
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
@@ -3,11 +3,11 @@ let nodePTY
|
||||
import * as fs from 'mz/fs'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService, ConfigService } from 'terminus-core'
|
||||
import { exec } from 'mz/child_process'
|
||||
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
import { SessionOptions } from '../api'
|
||||
|
||||
let macOSNativeProcessList
|
||||
try {
|
||||
@@ -29,7 +29,6 @@ export interface IChildProcess {
|
||||
export abstract class BaseSession {
|
||||
open: boolean
|
||||
name: string
|
||||
recoveryId: string
|
||||
truePID: number
|
||||
protected output = new Subject<string>()
|
||||
protected closed = new Subject<void>()
|
||||
@@ -78,14 +77,18 @@ export class Session extends BaseSession {
|
||||
private pty: any
|
||||
private pauseAfterExit = false
|
||||
|
||||
constructor (private config: ConfigService) {
|
||||
super()
|
||||
}
|
||||
|
||||
start (options: SessionOptions) {
|
||||
this.name = options.name
|
||||
this.recoveryId = options.recoveryId
|
||||
|
||||
let env = {
|
||||
...process.env,
|
||||
TERM: 'xterm-256color',
|
||||
...options.env,
|
||||
...this.config.store.terminal.environment || {},
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin' && !process.env.LC_ALL) {
|
||||
@@ -107,13 +110,7 @@ export class Session extends BaseSession {
|
||||
env: env,
|
||||
})
|
||||
|
||||
if (options.recoveredTruePID$) {
|
||||
options.recoveredTruePID$.subscribe(pid => {
|
||||
this.truePID = pid
|
||||
})
|
||||
} else {
|
||||
this.truePID = (this.pty as any).pid
|
||||
}
|
||||
this.truePID = (this.pty as any).pid
|
||||
|
||||
setTimeout(async () => {
|
||||
// Retrieve any possible single children now that shell has fully started
|
||||
@@ -257,52 +254,21 @@ export class SessionsService {
|
||||
private lastID = 0
|
||||
|
||||
constructor (
|
||||
@Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
|
||||
private config: ConfigService,
|
||||
log: LogService,
|
||||
) {
|
||||
nodePTY = require('@terminus-term/node-pty')
|
||||
nodePTY = require('../bufferizedPTY')(nodePTY)
|
||||
this.logger = log.create('sessions')
|
||||
this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
|
||||
}
|
||||
|
||||
async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
|
||||
let persistence = this.getPersistence()
|
||||
if (persistence) {
|
||||
let recoveryId = await persistence.startSession(options)
|
||||
options = await persistence.attachSession(recoveryId)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
addSession (session: BaseSession, options: SessionOptions) {
|
||||
this.lastID++
|
||||
options.name = `session-${this.lastID}`
|
||||
session.start(options)
|
||||
let persistence = this.getPersistence()
|
||||
session.destroyed$.pipe(first()).subscribe(() => {
|
||||
delete this.sessions[session.name]
|
||||
if (persistence) {
|
||||
persistence.terminateSession(session.recoveryId)
|
||||
}
|
||||
})
|
||||
this.sessions[session.name] = session
|
||||
return session
|
||||
}
|
||||
|
||||
async recover (recoveryId: string): Promise<SessionOptions> {
|
||||
let persistence = this.getPersistence()
|
||||
if (persistence) {
|
||||
return await persistence.attachSession(recoveryId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private getPersistence (): SessionPersistenceProvider {
|
||||
if (!this.config.store.terminal.persistence) {
|
||||
return null
|
||||
}
|
||||
return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ import { Observable, AsyncSubject } from 'rxjs'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { AppService, Logger, LogService, ConfigService } from 'terminus-core'
|
||||
import { IShell, ShellProvider } from '../api'
|
||||
import { SessionsService } from './sessions.service'
|
||||
import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
|
||||
@Injectable()
|
||||
@@ -14,7 +13,6 @@ export class TerminalService {
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private sessions: SessionsService,
|
||||
private config: ConfigService,
|
||||
@Inject(ShellProvider) private shellProviders: ShellProvider[],
|
||||
log: LogService,
|
||||
@@ -52,16 +50,15 @@ export class TerminalService {
|
||||
let shells = await this.shells$.toPromise()
|
||||
shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0]
|
||||
}
|
||||
let env: any = Object.assign({}, process.env, shell.env || {}, this.config.store.terminal.environment || {})
|
||||
|
||||
this.logger.log(`Starting shell ${shell.name}`, shell)
|
||||
let sessionOptions = await this.sessions.prepareNewSession({
|
||||
let sessionOptions = {
|
||||
command: shell.command,
|
||||
args: shell.args || [],
|
||||
cwd,
|
||||
env,
|
||||
env: shell.env,
|
||||
pauseAfterExit: pause,
|
||||
})
|
||||
}
|
||||
|
||||
this.logger.log('Using session options:', sessionOptions)
|
||||
|
||||
|
@@ -12,70 +12,76 @@
|
||||
"@terminus-term/xterm@3.8.4":
|
||||
version "3.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@terminus-term/xterm/-/xterm-3.8.4.tgz#c9a9d9e0d46dbd8a94e06384e2d7268d36f5b0c6"
|
||||
|
||||
"@types/async-lock@0.0.19":
|
||||
version "0.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-0.0.19.tgz#4bdb7f8d9ac2826588b98068903aedbd9d95dce8"
|
||||
integrity sha512-DrxCjnJh9n3ivpldwI098PnuVYwg9e5lFlU8/1qfh/J/wFHbG3dX/bEtB4ynfTi3IXVJozFO2psD96+W2h3yeQ==
|
||||
|
||||
"@types/deep-equal@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
|
||||
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==
|
||||
|
||||
"@types/mz@0.0.31":
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.31.tgz#a4d80c082fefe71e40a7c0f07d1e6555bbbc7b52"
|
||||
integrity sha1-pNgMCC/v5x5Ap8DwfR5lVbu8e1I=
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "10.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235"
|
||||
integrity sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ==
|
||||
|
||||
"@types/node@7.0.12":
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.12.tgz#ae5f67a19c15f752148004db07cbbb372e69efc9"
|
||||
integrity sha1-rl9noZwV91IUgATbB8u7Ny5p78k=
|
||||
|
||||
"@types/webpack-env@1.13.0":
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.0.tgz#3044381647e11ee973c5af2e925323930f691d80"
|
||||
integrity sha1-MEQ4FkfhHulzxa8uklMjkw9pHYA=
|
||||
|
||||
any-promise@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
|
||||
|
||||
async-lock@^1.0.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.1.3.tgz#e47f1cbb6bec765b73e27ed8961d58006457ec08"
|
||||
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
|
||||
|
||||
big.js@^3.1.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
|
||||
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
|
||||
|
||||
connected-domain@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93"
|
||||
integrity sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM=
|
||||
|
||||
dataurl@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dataurl/-/dataurl-0.1.0.tgz#1f4734feddec05ffe445747978d86759c4b33199"
|
||||
integrity sha1-H0c0/t3sBf/kRXR5eNhnWcSzMZk=
|
||||
|
||||
deep-equal@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
|
||||
|
||||
file-loader@^0.11.2:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34"
|
||||
integrity sha512-N+uhF3mswIFeziHQjGScJ/yHXYt3DiLBeC+9vWW+WjUBiClMSOlV1YrXQi+7KM2aA3Rn4Bybgv+uXFQbfkzpvg==
|
||||
dependencies:
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
font-finder@^1.0.2, font-finder@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.0.4.tgz#2ca944954dd8d0e1b5bdc4c596cc08607761d89b"
|
||||
integrity sha512-naF16RpjWUTFLqzhmdivYpBCrqySN6PI+a4GPtoEsCdvOpbKYTGeTjO7mxh3Wwjz4xKU+Oqx9kwOcteLDeMFQA==
|
||||
dependencies:
|
||||
get-system-fonts "^2.0.0"
|
||||
promise-stream-reader "^1.0.1"
|
||||
@@ -83,6 +89,7 @@ font-finder@^1.0.2, font-finder@^1.0.3:
|
||||
font-ligatures@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/font-ligatures/-/font-ligatures-1.3.2.tgz#227eb5fc38fef34b5373aa19b555320b82842a71"
|
||||
integrity sha512-h9t+gvKVr/c2GnQs4GhXHY39/qyLlXNaIxupU1cxj7YOXEFT8+sJfcchIrZ9UETZUUT7dNcI7RDOXN7gFtuw2g==
|
||||
dependencies:
|
||||
font-finder "^1.0.3"
|
||||
lru-cache "^4.1.3"
|
||||
@@ -91,24 +98,29 @@ font-ligatures@^1.3.1:
|
||||
font-manager@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/font-manager/-/font-manager-0.3.0.tgz#9efdc13e521a3d8752e7ab56c3938818043a311f"
|
||||
integrity sha512-6N3pzO+9kxE3yD9c4VN7reg5fqgFvjcUdxZmwauRzsExaeKRu0APfEi3DOISFakokybgKlZcLFQHawwc2TMpQQ==
|
||||
dependencies:
|
||||
nan ">=2.10.0"
|
||||
|
||||
get-system-fonts@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6"
|
||||
integrity sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==
|
||||
|
||||
hterm-umdjs@1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.4.1.tgz#0cd5352eaf927c70b83c36146cf2c2a281dba957"
|
||||
integrity sha512-r5JOmdDK1bZCmp3cKcuGRLVeum33H+pzD119ZxmQou+QUVe6SAVSz03HvKWVhM2Ao1Biv+fkhFDmnsaRPq0tFg==
|
||||
|
||||
json5@^0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
|
||||
integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
|
||||
|
||||
loader-utils@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
|
||||
integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
|
||||
dependencies:
|
||||
big.js "^3.1.3"
|
||||
emojis-list "^2.0.0"
|
||||
@@ -117,6 +129,7 @@ loader-utils@^1.0.2:
|
||||
lru-cache@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
|
||||
integrity sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==
|
||||
dependencies:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
@@ -124,12 +137,14 @@ lru-cache@^4.1.3:
|
||||
macos-native-processlist@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/macos-native-processlist/-/macos-native-processlist-1.0.0.tgz#1dcf1fac554e057f90c6451c39420e065d186a68"
|
||||
integrity sha512-FYA5DzCBvt+1wcCR8iFoCW2zZ8GZXtR6Ee/kpC9gVlqvEcM2ooma71KV8EIP2VaM+v2HOQAVvNoKSmFBd4z8dQ==
|
||||
dependencies:
|
||||
nan "^2.10.0"
|
||||
|
||||
mz@^2.6.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
|
||||
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
object-assign "^4.0.1"
|
||||
@@ -143,74 +158,89 @@ nan@2.10.0:
|
||||
nan@>=2.10.0, nan@^2.10.0:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
|
||||
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
opentype.js@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.8.0.tgz#acabcfa1642fbe894a3e4d759e43ba694e02bd35"
|
||||
integrity sha1-rKvPoWQvvolKPk11nkO6aU4CvTU=
|
||||
dependencies:
|
||||
tiny-inflate "^1.0.2"
|
||||
|
||||
promise-stream-reader@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
|
||||
integrity sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==
|
||||
|
||||
ps-node@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3"
|
||||
integrity sha1-mvZ6mdex0BMuUaUDCZ04qNKs4sM=
|
||||
dependencies:
|
||||
table-parser "^0.1.3"
|
||||
|
||||
pseudomap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
||||
|
||||
rage-edit@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/rage-edit/-/rage-edit-1.2.0.tgz#991860a60fef934d8a6d0f057e55786b02f94a2b"
|
||||
integrity sha512-0RspBRc2s6We4g7hRCvT5mu7YPEnfjvQK8Tt354a2uUNJCMC7MKLvo/1mLvHUCQ/zbP6siQyp5VRZN7UCpMFZg==
|
||||
|
||||
runes@^0.4.2:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355"
|
||||
integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==
|
||||
|
||||
table-parser@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0"
|
||||
integrity sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=
|
||||
dependencies:
|
||||
connected-domain "^1.0.0"
|
||||
|
||||
thenify-all@^1.0.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
|
||||
integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
|
||||
dependencies:
|
||||
thenify ">= 3.1.0 < 4"
|
||||
|
||||
"thenify@>= 3.1.0 < 4":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
|
||||
integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
tiny-inflate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.2.tgz#93d9decffc8805bd57eae4310f0b745e9b6fb3a7"
|
||||
integrity sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c=
|
||||
|
||||
uuid@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
windows-process-tree@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.3.tgz#6b781f0a320e8a0d6434c9399add4389c709cf6e"
|
||||
integrity sha512-SzPJSubVVsToz1g5lr2P+4mQT70gvJ9u/nlnpfkOeQcAhOuhKz5DiO1TARgR0OnVsv21LPzxbA2m/4JQkGh1wA==
|
||||
dependencies:
|
||||
nan "^2.10.0"
|
||||
|
||||
xterm-addon-ligatures-tmp@^0.1.0-beta-1:
|
||||
version "0.1.0-beta-2"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-ligatures-tmp/-/xterm-addon-ligatures-tmp-0.1.0-beta-2.tgz#1063a282b279b7586372dee7892cea59738c613e"
|
||||
integrity sha512-d+UoX5dfP7ZSEE/DnQlqubs7Bpw5UxLfTAibpo4pOU2KFw+lRlsLgHg5fcmhXoEvD9rj01enYTsIjedNwnwC5Q==
|
||||
dependencies:
|
||||
font-finder "^1.0.2"
|
||||
font-ligatures "^1.3.1"
|
||||
@@ -218,3 +248,4 @@ xterm-addon-ligatures-tmp@^0.1.0-beta-1:
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
|
||||
|
Reference in New Issue
Block a user