sessions.service.ts 5.3 KB
Newer Older
E
.  
Eugene Pankov 已提交
1
import * as nodePTY from 'node-pty'
E
Eugene Pankov 已提交
2
import * as fs from 'mz/fs'
E
.  
Eugene Pankov 已提交
3 4
import { Subject } from 'rxjs'
import { Injectable } from '@angular/core'
E
wip  
Eugene Pankov 已提交
5
import { Logger, LogService } from 'terminus-core'
E
.  
Eugene Pankov 已提交
6
import { exec } from 'mz/child_process'
E
wip  
Eugene Pankov 已提交
7

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

E
.  
Eugene Pankov 已提交
10 11 12
export class Session {
    open: boolean
    name: string
E
.  
Eugene Pankov 已提交
13 14 15
    output$ = new Subject<string>()
    closed$ = new Subject<void>()
    destroyed$ = new Subject<void>()
E
.  
Eugene Pankov 已提交
16
    recoveryId: string
E
.  
Eugene Pankov 已提交
17
    truePID: number
E
.  
Eugene Pankov 已提交
18 19 20
    private pty: any
    private initialDataBuffer = ''
    private initialDataBufferReleased = false
E
.  
Eugene Pankov 已提交
21 22 23

    constructor (options: SessionOptions) {
        this.name = options.name
E
.  
Eugene Pankov 已提交
24
        this.recoveryId = options.recoveryId
E
.  
Eugene Pankov 已提交
25 26 27 28 29 30

        let env = {
            ...process.env,
            ...options.env,
            TERM: 'xterm-256color',
        }
31 32 33 34 35 36 37 38 39 40 41 42

        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 已提交
43
        this.pty = nodePTY.spawn(options.command, options.args || [], {
E
.  
Eugene Pankov 已提交
44
            name: 'xterm-256color',
E
.  
Eugene Pankov 已提交
45 46
            cols: options.width || 80,
            rows: options.height || 30,
E
.  
Eugene Pankov 已提交
47
            cwd: options.cwd || process.env.HOME,
E
.  
Eugene Pankov 已提交
48
            env: env,
E
.  
Eugene Pankov 已提交
49 50
        })

E
.  
Eugene Pankov 已提交
51 52 53 54 55
        if (options.recoveredTruePID$) {
            options.recoveredTruePID$.subscribe(pid => {
                this.truePID = pid
            })
        } else {
E
lint  
Eugene Pankov 已提交
56
            this.truePID = (this.pty as any).pid
E
.  
Eugene Pankov 已提交
57
        }
E
.  
Eugene Pankov 已提交
58

E
.  
Eugene Pankov 已提交
59 60 61
        this.open = true

        this.pty.on('data', (data) => {
E
.  
Eugene Pankov 已提交
62 63 64
            if (!this.initialDataBufferReleased) {
                this.initialDataBuffer += data
            } else {
E
.  
Eugene Pankov 已提交
65
                this.output$.next(data)
E
.  
Eugene Pankov 已提交
66
            }
E
.  
Eugene Pankov 已提交
67 68
        })

E
Eugene Pankov 已提交
69 70 71 72 73 74
        this.pty.on('exit', () => {
            if (this.open) {
                this.destroy()
            }
        })

E
.  
Eugene Pankov 已提交
75
        this.pty.on('close', () => {
E
.  
Eugene Pankov 已提交
76 77 78
            if (this.open) {
                this.destroy()
            }
E
.  
Eugene Pankov 已提交
79 80 81
        })
    }

E
.  
Eugene Pankov 已提交
82 83
    releaseInitialDataBuffer () {
        this.initialDataBufferReleased = true
E
.  
Eugene Pankov 已提交
84
        this.output$.next(this.initialDataBuffer)
E
.  
Eugene Pankov 已提交
85 86 87
        this.initialDataBuffer = null
    }

E
.  
Eugene Pankov 已提交
88
    resize (columns, rows) {
E
Eugene Pankov 已提交
89 90 91
        if (this.pty.writable) {
            this.pty.resize(columns, rows)
        }
E
.  
Eugene Pankov 已提交
92 93 94
    }

    write (data) {
E
Eugene Pankov 已提交
95 96 97
        if (this.pty.writable) {
            this.pty.write(Buffer.from(data, 'utf-8'))
        }
E
.  
Eugene Pankov 已提交
98 99
    }

E
.  
Eugene Pankov 已提交
100
    kill (signal?: string) {
E
.  
Eugene Pankov 已提交
101 102 103
        this.pty.kill(signal)
    }

E
.  
Eugene Pankov 已提交
104
    async gracefullyKillProcess (): Promise<void> {
E
lint  
Eugene Pankov 已提交
105
        if (process.platform === 'win32') {
E
.  
Eugene Pankov 已提交
106 107 108
            this.kill()
        } else {
            await new Promise((resolve) => {
E
.  
Eugene Pankov 已提交
109
                this.kill('SIGTERM')
E
.  
Eugene Pankov 已提交
110 111 112 113 114 115 116 117 118 119
                setImmediate(() => {
                    if (!this.open) {
                        resolve()
                    } else {
                        setTimeout(() => {
                            if (this.open) {
                                this.kill('SIGKILL')
                            }
                            resolve()
                        }, 1000)
E
.  
Eugene Pankov 已提交
120
                    }
E
.  
Eugene Pankov 已提交
121 122 123
                })
            })
        }
E
.  
Eugene Pankov 已提交
124 125
    }

E
.  
Eugene Pankov 已提交
126 127 128 129 130 131 132
    async destroy (): Promise<void> {
        if (this.open) {
            this.open = false
            this.closed$.next()
            this.destroyed$.next()
            this.output$.complete()
            await this.gracefullyKillProcess()
E
.  
Eugene Pankov 已提交
133 134
        }
    }
E
.  
Eugene Pankov 已提交
135 136

    async getWorkingDirectory (): Promise<string> {
E
lint  
Eugene Pankov 已提交
137
        if (process.platform === 'darwin') {
E
.  
Eugene Pankov 已提交
138
            let lines = (await exec(`lsof -p ${this.truePID} -Fn`))[0].toString().split('\n')
139 140 141 142 143
            if (lines[1] === 'fcwd') {
                return lines[2].substring(1)
            } else {
                return lines[1].substring(1)
            }
E
done  
Eugene Pankov 已提交
144
        }
E
lint  
Eugene Pankov 已提交
145
        if (process.platform === 'linux') {
E
done  
Eugene Pankov 已提交
146 147 148
            return await fs.readlink(`/proc/${this.truePID}/cwd`)
        }
        return null
E
.  
Eugene Pankov 已提交
149
    }
E
.  
Eugene Pankov 已提交
150 151 152 153 154 155 156 157
}

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

E
lint  
Eugene Pankov 已提交
158
    constructor (
E
.  
Eugene Pankov 已提交
159
        private persistence: SessionPersistenceProvider,
E
.  
Eugene Pankov 已提交
160 161 162
        log: LogService,
    ) {
        this.logger = log.create('sessions')
E
.  
Eugene Pankov 已提交
163 164
    }

E
.  
Eugene Pankov 已提交
165
    async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
E
.  
Eugene Pankov 已提交
166 167 168 169
        if (this.persistence) {
            let recoveryId = await this.persistence.startSession(options)
            options = await this.persistence.attachSession(recoveryId)
        }
E
.  
Eugene Pankov 已提交
170
        return options
E
.  
Eugene Pankov 已提交
171 172
    }

E
lint  
Eugene Pankov 已提交
173
    addSession (options: SessionOptions): Session {
E
.  
Eugene Pankov 已提交
174 175 176
        this.lastID++
        options.name = `session-${this.lastID}`
        let session = new Session(options)
E
.  
Eugene Pankov 已提交
177
        session.destroyed$.first().subscribe(() => {
E
.  
Eugene Pankov 已提交
178
            delete this.sessions[session.name]
E
.  
Eugene Pankov 已提交
179 180 181
            if (this.persistence) {
                this.persistence.terminateSession(session.recoveryId)
            }
E
.  
Eugene Pankov 已提交
182 183 184 185
        })
        this.sessions[session.name] = session
        return session
    }
E
.  
Eugene Pankov 已提交
186

E
.  
Eugene Pankov 已提交
187
    async recover (recoveryId: string): Promise<SessionOptions> {
E
.  
Eugene Pankov 已提交
188 189 190
        if (!this.persistence) {
            return null
        }
E
.  
Eugene Pankov 已提交
191
        return await this.persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
192
    }
E
.  
Eugene Pankov 已提交
193
}