mirror of
https://github.com/Eugeny/tabby.git
synced 2025-10-05 14:34:54 +00:00
simpler tab recovery system
This commit is contained in:
@@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user