import * as fs from 'mz/fs' import { exec, spawn } from 'mz/child_process' import { exec as execCallback } from 'child_process' import { AsyncSubject } from 'rxjs' import { Injectable } from '@angular/core' import { Logger, LogService } from 'terminus-core' import { SessionOptions, SessionPersistenceProvider } from './api' declare function delay (ms: number): Promise interface IChildProcess { pid: number ppid: number command: string } async function listProcesses (): Promise { 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 { private logger: Logger constructor ( log: LogService, ) { super() this.logger = log.create('main') } async attachSession (recoveryId: any): Promise { let lines = await new Promise(resolve => { execCallback('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() 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 { 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 { 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 { try { await exec(`screen -S ${recoveryId} -X quit`) } catch (_) { // screen has already quit } } private async prepareConfig (): Promise { let configPath = '/tmp/.termScreenConfig' 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 } }