sessions.service.ts 7.0 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
import { Subject } from 'rxjs'
E
Eugene Pankov 已提交
5 6
import { Injectable, Inject } from '@angular/core'
import { Logger, LogService, ElectronService, ConfigService } from 'terminus-core'
E
.  
Eugene Pankov 已提交
7
import { exec } from 'mz/child_process'
E
wip  
Eugene Pankov 已提交
8

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

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

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

E
Eugene Pankov 已提交
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
    emitOutput (data: string) {
        if (!this.initialDataBufferReleased) {
            this.initialDataBuffer += data
        } else {
            this.output$.next(data)
        }
    }

    releaseInitialDataBuffer () {
        this.initialDataBufferReleased = true
        this.output$.next(this.initialDataBuffer)
        this.initialDataBuffer = null
    }

    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
            this.closed$.next()
            this.destroyed$.next()
            this.output$.complete()
            await this.gracefullyKillProcess()
        }
    }
}

export class Session extends BaseSession {
    private pty: any

E
.  
Eugene Pankov 已提交
63
    constructor (options: SessionOptions) {
E
Eugene Pankov 已提交
64
        super()
E
.  
Eugene Pankov 已提交
65
        this.name = options.name
E
.  
Eugene Pankov 已提交
66
        this.recoveryId = options.recoveryId
E
.  
Eugene Pankov 已提交
67 68 69 70

        let env = {
            ...process.env,
            TERM: 'xterm-256color',
71
            ...options.env,
E
.  
Eugene Pankov 已提交
72
        }
73 74 75 76 77 78 79 80 81 82 83 84

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

E
.  
Eugene Pankov 已提交
93 94 95 96 97
        if (options.recoveredTruePID$) {
            options.recoveredTruePID$.subscribe(pid => {
                this.truePID = pid
            })
        } else {
E
lint  
Eugene Pankov 已提交
98
            this.truePID = (this.pty as any).pid
E
.  
Eugene Pankov 已提交
99
        }
E
.  
Eugene Pankov 已提交
100

E
.  
Eugene Pankov 已提交
101 102
        this.open = true

E
Eugene Pankov 已提交
103 104
        this.pty.on('data', data => {
            this.emitOutput(data)
E
.  
Eugene Pankov 已提交
105 106
        })

E
Eugene Pankov 已提交
107 108 109 110 111 112
        this.pty.on('exit', () => {
            if (this.open) {
                this.destroy()
            }
        })

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

    resize (columns, rows) {
E
Eugene Pankov 已提交
121
        if (this.pty._writable) {
E
Eugene Pankov 已提交
122 123
            this.pty.resize(columns, rows)
        }
E
.  
Eugene Pankov 已提交
124 125 126
    }

    write (data) {
E
Eugene Pankov 已提交
127
        if (this.pty._writable) {
E
Eugene Pankov 已提交
128 129
            this.pty.write(Buffer.from(data, 'utf-8'))
        }
E
.  
Eugene Pankov 已提交
130 131
    }

E
.  
Eugene Pankov 已提交
132
    kill (signal?: string) {
E
.  
Eugene Pankov 已提交
133 134 135
        this.pty.kill(signal)
    }

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

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

@Injectable()
export class SessionsService {
    sessions: {[id: string]: Session} = {}
    logger: Logger
    private lastID = 0

E
lint  
Eugene Pankov 已提交
197
    constructor (
E
Eugene Pankov 已提交
198 199
        @Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
        private config: ConfigService,
E
Eugene Pankov 已提交
200
        electron: ElectronService,
E
.  
Eugene Pankov 已提交
201 202
        log: LogService,
    ) {
E
Eugene Pankov 已提交
203
        nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
E
.  
Eugene Pankov 已提交
204
        this.logger = log.create('sessions')
E
Eugene Pankov 已提交
205
        this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
E
.  
Eugene Pankov 已提交
206 207
    }

E
.  
Eugene Pankov 已提交
208
    async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
E
Eugene Pankov 已提交
209 210 211 212
        let persistence = this.getPersistence()
        if (persistence) {
            let recoveryId = await persistence.startSession(options)
            options = await persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
213
        }
E
.  
Eugene Pankov 已提交
214
        return options
E
.  
Eugene Pankov 已提交
215 216
    }

E
lint  
Eugene Pankov 已提交
217
    addSession (options: SessionOptions): Session {
E
.  
Eugene Pankov 已提交
218 219 220
        this.lastID++
        options.name = `session-${this.lastID}`
        let session = new Session(options)
E
Eugene Pankov 已提交
221
        let persistence = this.getPersistence()
E
.  
Eugene Pankov 已提交
222
        session.destroyed$.first().subscribe(() => {
E
.  
Eugene Pankov 已提交
223
            delete this.sessions[session.name]
E
Eugene Pankov 已提交
224 225
            if (persistence) {
                persistence.terminateSession(session.recoveryId)
E
.  
Eugene Pankov 已提交
226
            }
E
.  
Eugene Pankov 已提交
227 228 229 230
        })
        this.sessions[session.name] = session
        return session
    }
E
.  
Eugene Pankov 已提交
231

E
.  
Eugene Pankov 已提交
232
    async recover (recoveryId: string): Promise<SessionOptions> {
E
Eugene Pankov 已提交
233 234 235
        let persistence = this.getPersistence()
        if (persistence) {
            return await persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
236
        }
E
Eugene Pankov 已提交
237 238 239 240
        return null
    }

    private getPersistence (): SessionPersistenceProvider {
241 242 243
        if (!this.config.store.terminal.persistence) {
            return null
        }
E
Eugene Pankov 已提交
244
        return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
E
.  
Eugene Pankov 已提交
245
    }
E
.  
Eugene Pankov 已提交
246
}