sessions.service.ts 7.3 KB
Newer Older
E
Eugene Pankov 已提交
1 2
const psNode = require('ps-node')
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 7
import { Injectable, Inject } from '@angular/core'
import { Logger, LogService, ElectronService, 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

E
Eugene Pankov 已提交
12 13 14 15 16 17
export interface IChildProcess {
    pid: number
    ppid: number
    command: string
}

E
Eugene Pankov 已提交
18
export abstract class BaseSession {
E
.  
Eugene Pankov 已提交
19 20
    open: boolean
    name: string
E
.  
Eugene Pankov 已提交
21
    recoveryId: string
E
.  
Eugene Pankov 已提交
22
    truePID: number
E
Eugene Pankov 已提交
23 24 25
    protected output = new Subject<string>()
    protected closed = new Subject<void>()
    protected destroyed = new Subject<void>()
E
.  
Eugene Pankov 已提交
26 27
    private initialDataBuffer = ''
    private initialDataBufferReleased = false
E
.  
Eugene Pankov 已提交
28

E
Eugene Pankov 已提交
29 30 31 32
    get output$ (): Observable<string> { return this.output }
    get closed$ (): Observable<void> { return this.closed }
    get destroyed$ (): Observable<void> { return this.destroyed }

E
Eugene Pankov 已提交
33 34 35 36
    emitOutput (data: string) {
        if (!this.initialDataBufferReleased) {
            this.initialDataBuffer += data
        } else {
E
Eugene Pankov 已提交
37
            this.output.next(data)
E
Eugene Pankov 已提交
38 39 40 41 42
        }
    }

    releaseInitialDataBuffer () {
        this.initialDataBufferReleased = true
E
Eugene Pankov 已提交
43
        this.output.next(this.initialDataBuffer)
E
Eugene Pankov 已提交
44 45 46
        this.initialDataBuffer = null
    }

47
    abstract start (options: SessionOptions)
E
Eugene Pankov 已提交
48 49 50 51 52 53 54 55 56 57
    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 已提交
58 59 60
            this.closed.next()
            this.destroyed.next()
            this.output.complete()
E
Eugene Pankov 已提交
61 62 63 64 65 66 67 68
            await this.gracefullyKillProcess()
        }
    }
}

export class Session extends BaseSession {
    private pty: any

69
    start (options: SessionOptions) {
E
.  
Eugene Pankov 已提交
70
        this.name = options.name
E
.  
Eugene Pankov 已提交
71
        this.recoveryId = options.recoveryId
E
.  
Eugene Pankov 已提交
72 73 74 75

        let env = {
            ...process.env,
            TERM: 'xterm-256color',
76
            ...options.env,
E
.  
Eugene Pankov 已提交
77
        }
78 79 80 81 82 83 84 85 86 87 88 89

        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 已提交
90
        this.pty = nodePTY.spawn(options.command, options.args || [], {
E
.  
Eugene Pankov 已提交
91
            name: 'xterm-256color',
E
.  
Eugene Pankov 已提交
92 93
            cols: options.width || 80,
            rows: options.height || 30,
E
.  
Eugene Pankov 已提交
94
            cwd: options.cwd || process.env.HOME,
E
.  
Eugene Pankov 已提交
95
            env: env,
E
.  
Eugene Pankov 已提交
96 97
        })

E
.  
Eugene Pankov 已提交
98 99 100 101 102
        if (options.recoveredTruePID$) {
            options.recoveredTruePID$.subscribe(pid => {
                this.truePID = pid
            })
        } else {
E
lint  
Eugene Pankov 已提交
103
            this.truePID = (this.pty as any).pid
E
.  
Eugene Pankov 已提交
104
        }
E
.  
Eugene Pankov 已提交
105

E
.  
Eugene Pankov 已提交
106 107
        this.open = true

108
        this.pty.on('data-buffered', data => {
E
Eugene Pankov 已提交
109
            this.emitOutput(data)
E
.  
Eugene Pankov 已提交
110 111
        })

E
Eugene Pankov 已提交
112 113 114 115 116 117
        this.pty.on('exit', () => {
            if (this.open) {
                this.destroy()
            }
        })

E
.  
Eugene Pankov 已提交
118
        this.pty.on('close', () => {
E
.  
Eugene Pankov 已提交
119 120 121
            if (this.open) {
                this.destroy()
            }
E
.  
Eugene Pankov 已提交
122 123 124 125
        })
    }

    resize (columns, rows) {
E
Eugene Pankov 已提交
126
        if (this.pty._writable) {
E
Eugene Pankov 已提交
127 128
            this.pty.resize(columns, rows)
        }
E
.  
Eugene Pankov 已提交
129 130 131
    }

    write (data) {
E
Eugene Pankov 已提交
132
        if (this.pty._writable) {
E
Eugene Pankov 已提交
133 134
            this.pty.write(Buffer.from(data, 'utf-8'))
        }
E
.  
Eugene Pankov 已提交
135 136
    }

E
.  
Eugene Pankov 已提交
137
    kill (signal?: string) {
E
.  
Eugene Pankov 已提交
138 139 140
        this.pty.kill(signal)
    }

E
Eugene Pankov 已提交
141 142 143 144 145 146 147 148 149 150 151 152 153 154
    async getChildProcesses (): Promise<IChildProcess[]> {
        if (!this.truePID) {
            return []
        }
        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 已提交
155
    async gracefullyKillProcess (): Promise<void> {
E
lint  
Eugene Pankov 已提交
156
        if (process.platform === 'win32') {
E
.  
Eugene Pankov 已提交
157 158 159
            this.kill()
        } else {
            await new Promise((resolve) => {
E
.  
Eugene Pankov 已提交
160
                this.kill('SIGTERM')
E
.  
Eugene Pankov 已提交
161 162 163 164 165 166 167 168 169 170
                setImmediate(() => {
                    if (!this.open) {
                        resolve()
                    } else {
                        setTimeout(() => {
                            if (this.open) {
                                this.kill('SIGKILL')
                            }
                            resolve()
                        }, 1000)
E
.  
Eugene Pankov 已提交
171
                    }
E
.  
Eugene Pankov 已提交
172 173 174
                })
            })
        }
E
.  
Eugene Pankov 已提交
175 176
    }

E
.  
Eugene Pankov 已提交
177
    async getWorkingDirectory (): Promise<string> {
E
Eugene Pankov 已提交
178 179 180
        if (!this.truePID) {
            return null
        }
E
lint  
Eugene Pankov 已提交
181
        if (process.platform === 'darwin') {
E
.  
Eugene Pankov 已提交
182
            let lines = (await exec(`lsof -p ${this.truePID} -Fn`))[0].toString().split('\n')
183 184 185 186 187
            if (lines[1] === 'fcwd') {
                return lines[2].substring(1)
            } else {
                return lines[1].substring(1)
            }
E
done  
Eugene Pankov 已提交
188
        }
E
lint  
Eugene Pankov 已提交
189
        if (process.platform === 'linux') {
E
done  
Eugene Pankov 已提交
190 191 192
            return await fs.readlink(`/proc/${this.truePID}/cwd`)
        }
        return null
E
.  
Eugene Pankov 已提交
193
    }
E
.  
Eugene Pankov 已提交
194 195 196 197
}

@Injectable()
export class SessionsService {
198
    sessions: {[id: string]: BaseSession} = {}
E
.  
Eugene Pankov 已提交
199 200 201
    logger: Logger
    private lastID = 0

E
lint  
Eugene Pankov 已提交
202
    constructor (
E
Eugene Pankov 已提交
203 204
        @Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
        private config: ConfigService,
E
Eugene Pankov 已提交
205
        electron: ElectronService,
E
.  
Eugene Pankov 已提交
206 207
        log: LogService,
    ) {
208 209
        nodePTY = require('node-pty-tmp')
        nodePTY = require('../bufferizedPTY')(nodePTY)
E
.  
Eugene Pankov 已提交
210
        this.logger = log.create('sessions')
E
Eugene Pankov 已提交
211
        this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
E
.  
Eugene Pankov 已提交
212 213
    }

E
.  
Eugene Pankov 已提交
214
    async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
E
Eugene Pankov 已提交
215 216 217 218
        let persistence = this.getPersistence()
        if (persistence) {
            let recoveryId = await persistence.startSession(options)
            options = await persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
219
        }
E
.  
Eugene Pankov 已提交
220
        return options
E
.  
Eugene Pankov 已提交
221 222
    }

223
    addSession (session: BaseSession, options: SessionOptions) {
E
.  
Eugene Pankov 已提交
224 225
        this.lastID++
        options.name = `session-${this.lastID}`
226
        session.start(options)
E
Eugene Pankov 已提交
227
        let persistence = this.getPersistence()
E
Eugene Pankov 已提交
228
        session.destroyed$.pipe(first()).subscribe(() => {
E
.  
Eugene Pankov 已提交
229
            delete this.sessions[session.name]
E
Eugene Pankov 已提交
230 231
            if (persistence) {
                persistence.terminateSession(session.recoveryId)
E
.  
Eugene Pankov 已提交
232
            }
E
.  
Eugene Pankov 已提交
233 234 235 236
        })
        this.sessions[session.name] = session
        return session
    }
E
.  
Eugene Pankov 已提交
237

E
.  
Eugene Pankov 已提交
238
    async recover (recoveryId: string): Promise<SessionOptions> {
E
Eugene Pankov 已提交
239 240 241
        let persistence = this.getPersistence()
        if (persistence) {
            return await persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
242
        }
E
Eugene Pankov 已提交
243 244 245 246
        return null
    }

    private getPersistence (): SessionPersistenceProvider {
247 248 249
        if (!this.config.store.terminal.persistence) {
            return null
        }
E
Eugene Pankov 已提交
250
        return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
E
.  
Eugene Pankov 已提交
251
    }
E
.  
Eugene Pankov 已提交
252
}