sessions.service.ts 9.1 KB
Newer Older
E
naming  
Eugene Pankov 已提交
1
import psNode = require('ps-node')
E
Eugene Pankov 已提交
2
let nodePTY
E
Eugene Pankov 已提交
3
import * as fs from 'mz/fs'
E
Eugene Pankov 已提交
4 5
import { Observable, Subject } from 'rxjs'
import { first } from 'rxjs/operators'
E
Eugene Pankov 已提交
6
import { Injectable, Inject } from '@angular/core'
E
naming  
Eugene Pankov 已提交
7
import { Logger, LogService, ConfigService } from 'terminus-core'
E
.  
Eugene Pankov 已提交
8
import { exec } from 'mz/child_process'
E
wip  
Eugene Pankov 已提交
9

E
.  
Eugene Pankov 已提交
10
import { SessionOptions, SessionPersistenceProvider } from '../api'
E
.  
Eugene Pankov 已提交
11

12 13 14 15 16 17 18 19 20 21 22 23 24
let macOSNativeProcessList
try {
    macOSNativeProcessList = require('macos-native-processlist')
} catch (e) { } // tslint:disable-line

let windowsProcessTree
try {
    windowsProcessTree = require('windows-process-tree')
} catch (e) {
console.error(e)
} // tslint:disable-line
console.error(windowsProcessTree)

E
Eugene Pankov 已提交
25 26 27 28 29 30
export interface IChildProcess {
    pid: number
    ppid: number
    command: string
}

E
Eugene Pankov 已提交
31
export abstract class BaseSession {
E
.  
Eugene Pankov 已提交
32 33
    open: boolean
    name: string
E
.  
Eugene Pankov 已提交
34
    recoveryId: string
E
.  
Eugene Pankov 已提交
35
    truePID: number
E
Eugene Pankov 已提交
36 37 38
    protected output = new Subject<string>()
    protected closed = new Subject<void>()
    protected destroyed = new Subject<void>()
E
.  
Eugene Pankov 已提交
39 40
    private initialDataBuffer = ''
    private initialDataBufferReleased = false
E
.  
Eugene Pankov 已提交
41

E
Eugene Pankov 已提交
42 43 44 45
    get output$ (): Observable<string> { return this.output }
    get closed$ (): Observable<void> { return this.closed }
    get destroyed$ (): Observable<void> { return this.destroyed }

E
Eugene Pankov 已提交
46 47 48 49
    emitOutput (data: string) {
        if (!this.initialDataBufferReleased) {
            this.initialDataBuffer += data
        } else {
E
Eugene Pankov 已提交
50
            this.output.next(data)
E
Eugene Pankov 已提交
51 52 53 54 55
        }
    }

    releaseInitialDataBuffer () {
        this.initialDataBufferReleased = true
E
Eugene Pankov 已提交
56
        this.output.next(this.initialDataBuffer)
E
Eugene Pankov 已提交
57 58 59
        this.initialDataBuffer = null
    }

60
    abstract start (options: SessionOptions)
E
Eugene Pankov 已提交
61 62 63 64 65 66 67 68 69 70
    abstract resize (columns, rows)
    abstract write (data)
    abstract kill (signal?: string)
    abstract async getChildProcesses (): Promise<IChildProcess[]>
    abstract async gracefullyKillProcess (): Promise<void>
    abstract async getWorkingDirectory (): Promise<string>

    async destroy (): Promise<void> {
        if (this.open) {
            this.open = false
E
Eugene Pankov 已提交
71 72 73
            this.closed.next()
            this.destroyed.next()
            this.output.complete()
E
Eugene Pankov 已提交
74 75 76 77 78 79 80
            await this.gracefullyKillProcess()
        }
    }
}

export class Session extends BaseSession {
    private pty: any
81
    private pauseAfterExit = false
E
Eugene Pankov 已提交
82

83
    start (options: SessionOptions) {
E
.  
Eugene Pankov 已提交
84
        this.name = options.name
E
.  
Eugene Pankov 已提交
85
        this.recoveryId = options.recoveryId
E
.  
Eugene Pankov 已提交
86 87 88 89

        let env = {
            ...process.env,
            TERM: 'xterm-256color',
90
            ...options.env,
E
.  
Eugene Pankov 已提交
91
        }
92 93 94 95 96 97 98 99 100 101 102 103

        if (process.platform === 'darwin' && !process.env.LC_ALL) {
            let locale = process.env.LC_CTYPE || 'en_US.UTF-8'
            Object.assign(env, {
                LANG: locale,
                LC_ALL: locale,
                LC_MESSAGES: locale,
                LC_NUMERIC: locale,
                LC_COLLATE: locale,
                LC_MONETARY: locale,
            })
        }
E
.  
Eugene Pankov 已提交
104
        this.pty = nodePTY.spawn(options.command, options.args || [], {
E
.  
Eugene Pankov 已提交
105
            name: 'xterm-256color',
E
.  
Eugene Pankov 已提交
106 107
            cols: options.width || 80,
            rows: options.height || 30,
E
.  
Eugene Pankov 已提交
108
            cwd: options.cwd || process.env.HOME,
E
.  
Eugene Pankov 已提交
109
            env: env,
E
.  
Eugene Pankov 已提交
110 111
        })

E
.  
Eugene Pankov 已提交
112 113 114 115 116
        if (options.recoveredTruePID$) {
            options.recoveredTruePID$.subscribe(pid => {
                this.truePID = pid
            })
        } else {
E
lint  
Eugene Pankov 已提交
117
            this.truePID = (this.pty as any).pid
E
.  
Eugene Pankov 已提交
118
        }
E
.  
Eugene Pankov 已提交
119

120 121 122 123 124 125 126 127 128
        setTimeout(async () => {
            // Retrieve any possible single children now that shell has fully started
            let processes = await this.getChildProcesses()
            while (processes.length === 1) {
                this.truePID = processes[0].pid
                processes = await this.getChildProcesses()
            }
        }, 2000)

E
.  
Eugene Pankov 已提交
129 130
        this.open = true

131
        this.pty.on('data-buffered', data => {
E
Eugene Pankov 已提交
132
            this.emitOutput(data)
E
.  
Eugene Pankov 已提交
133 134
        })

E
Eugene Pankov 已提交
135
        this.pty.on('exit', () => {
136 137 138 139
            console.log('session exit')
            if (this.pauseAfterExit) {
                return
            } else if (this.open) {
E
Eugene Pankov 已提交
140 141 142 143
                this.destroy()
            }
        })

E
.  
Eugene Pankov 已提交
144
        this.pty.on('close', () => {
145 146 147 148
            console.log('session close')
            if (this.pauseAfterExit) {
                this.emitOutput('\r\nPress any key to close\r\n')
            } else if (this.open) {
E
.  
Eugene Pankov 已提交
149 150
                this.destroy()
            }
E
.  
Eugene Pankov 已提交
151
        })
152 153

        this.pauseAfterExit = options.pauseAfterExit
E
.  
Eugene Pankov 已提交
154 155 156
    }

    resize (columns, rows) {
E
Eugene Pankov 已提交
157
        if (this.pty._writable) {
E
Eugene Pankov 已提交
158 159
            this.pty.resize(columns, rows)
        }
E
.  
Eugene Pankov 已提交
160 161 162
    }

    write (data) {
163 164 165 166 167 168
        if (this.open) {
            if (this.pty._writable) {
                this.pty.write(Buffer.from(data, 'utf-8'))
            } else {
                this.destroy()
            }
E
Eugene Pankov 已提交
169
        }
E
.  
Eugene Pankov 已提交
170 171
    }

E
.  
Eugene Pankov 已提交
172
    kill (signal?: string) {
E
.  
Eugene Pankov 已提交
173 174 175
        this.pty.kill(signal)
    }

E
Eugene Pankov 已提交
176 177 178 179
    async getChildProcesses (): Promise<IChildProcess[]> {
        if (!this.truePID) {
            return []
        }
E
Eugene Pankov 已提交
180
        if (process.platform === 'darwin') {
181
            let processes = await macOSNativeProcessList.getProcessList()
E
Eugene Pankov 已提交
182 183 184 185 186 187
            return processes.filter(x => x.ppid === this.truePID).map(p => ({
                pid: p.pid,
                ppid: p.ppid,
                command: p.name,
            }))
        }
188 189 190 191 192 193 194 195 196 197 198
        if (process.platform === 'win32') {
            return await new Promise<IChildProcess[]>(resolve => {
                windowsProcessTree.getProcessTree(this.truePID, tree => {
                    resolve(tree ? tree.children.map(child => ({
                        pid: child.pid,
                        ppid: tree.pid,
                        command: child.name,
                    })) : [])
                })
            })
        }
E
Eugene Pankov 已提交
199 200 201 202 203 204 205 206 207 208
        return new Promise<IChildProcess[]>((resolve, reject) => {
            psNode.lookup({ ppid: this.truePID }, (err, processes) => {
                if (err) {
                    return reject(err)
                }
                resolve(processes as IChildProcess[])
            })
        })
    }

E
.  
Eugene Pankov 已提交
209
    async gracefullyKillProcess (): Promise<void> {
E
lint  
Eugene Pankov 已提交
210
        if (process.platform === 'win32') {
E
.  
Eugene Pankov 已提交
211 212 213
            this.kill()
        } else {
            await new Promise((resolve) => {
E
.  
Eugene Pankov 已提交
214
                this.kill('SIGTERM')
E
.  
Eugene Pankov 已提交
215 216 217 218 219 220 221 222 223 224
                setImmediate(() => {
                    if (!this.open) {
                        resolve()
                    } else {
                        setTimeout(() => {
                            if (this.open) {
                                this.kill('SIGKILL')
                            }
                            resolve()
                        }, 1000)
E
.  
Eugene Pankov 已提交
225
                    }
E
.  
Eugene Pankov 已提交
226 227 228
                })
            })
        }
E
.  
Eugene Pankov 已提交
229 230
    }

E
.  
Eugene Pankov 已提交
231
    async getWorkingDirectory (): Promise<string> {
E
Eugene Pankov 已提交
232 233 234
        if (!this.truePID) {
            return null
        }
E
lint  
Eugene Pankov 已提交
235
        if (process.platform === 'darwin') {
E
.  
Eugene Pankov 已提交
236
            let lines = (await exec(`lsof -p ${this.truePID} -Fn`))[0].toString().split('\n')
237 238 239 240 241
            if (lines[1] === 'fcwd') {
                return lines[2].substring(1)
            } else {
                return lines[1].substring(1)
            }
E
done  
Eugene Pankov 已提交
242
        }
E
lint  
Eugene Pankov 已提交
243
        if (process.platform === 'linux') {
E
done  
Eugene Pankov 已提交
244 245 246
            return await fs.readlink(`/proc/${this.truePID}/cwd`)
        }
        return null
E
.  
Eugene Pankov 已提交
247
    }
E
.  
Eugene Pankov 已提交
248 249 250 251
}

@Injectable()
export class SessionsService {
252
    sessions: {[id: string]: BaseSession} = {}
E
.  
Eugene Pankov 已提交
253 254 255
    logger: Logger
    private lastID = 0

E
lint  
Eugene Pankov 已提交
256
    constructor (
E
Eugene Pankov 已提交
257 258
        @Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
        private config: ConfigService,
E
.  
Eugene Pankov 已提交
259 260
        log: LogService,
    ) {
261 262
        nodePTY = require('node-pty-tmp')
        nodePTY = require('../bufferizedPTY')(nodePTY)
E
.  
Eugene Pankov 已提交
263
        this.logger = log.create('sessions')
E
Eugene Pankov 已提交
264
        this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
E
.  
Eugene Pankov 已提交
265 266
    }

E
.  
Eugene Pankov 已提交
267
    async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
E
Eugene Pankov 已提交
268 269 270 271
        let persistence = this.getPersistence()
        if (persistence) {
            let recoveryId = await persistence.startSession(options)
            options = await persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
272
        }
E
.  
Eugene Pankov 已提交
273
        return options
E
.  
Eugene Pankov 已提交
274 275
    }

276
    addSession (session: BaseSession, options: SessionOptions) {
E
.  
Eugene Pankov 已提交
277 278
        this.lastID++
        options.name = `session-${this.lastID}`
279
        session.start(options)
E
Eugene Pankov 已提交
280
        let persistence = this.getPersistence()
E
Eugene Pankov 已提交
281
        session.destroyed$.pipe(first()).subscribe(() => {
E
.  
Eugene Pankov 已提交
282
            delete this.sessions[session.name]
E
Eugene Pankov 已提交
283 284
            if (persistence) {
                persistence.terminateSession(session.recoveryId)
E
.  
Eugene Pankov 已提交
285
            }
E
.  
Eugene Pankov 已提交
286 287 288 289
        })
        this.sessions[session.name] = session
        return session
    }
E
.  
Eugene Pankov 已提交
290

E
.  
Eugene Pankov 已提交
291
    async recover (recoveryId: string): Promise<SessionOptions> {
E
Eugene Pankov 已提交
292 293 294
        let persistence = this.getPersistence()
        if (persistence) {
            return await persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
295
        }
E
Eugene Pankov 已提交
296 297 298 299
        return null
    }

    private getPersistence (): SessionPersistenceProvider {
300 301 302
        if (!this.config.store.terminal.persistence) {
            return null
        }
E
Eugene Pankov 已提交
303
        return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
E
.  
Eugene Pankov 已提交
304
    }
E
.  
Eugene Pankov 已提交
305
}