sessions.service.ts 4.9 KB
Newer Older
E
.  
Eugene Pankov 已提交
1
import * as nodePTY from 'node-pty'
E
.  
Eugene Pankov 已提交
2
import * as fs from 'fs-promise'
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 13

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

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

        let env = {
            ...process.env,
            ...options.env,
            TERM: 'xterm-256color',
        }
E
.  
Eugene Pankov 已提交
32 33 34 35 36
        if (options.command.includes(' ')) {
            options.args = ['-c', options.command]
            options.command = 'sh'
        }
        this.pty = nodePTY.spawn(options.command, options.args || [], {
E
.  
Eugene Pankov 已提交
37 38
            //name: 'screen-256color',
            name: 'xterm-256color',
E
.  
Eugene Pankov 已提交
39
            //name: 'xterm-color',
E
.  
Eugene Pankov 已提交
40 41 42
            cols: 80,
            rows: 30,
            cwd: options.cwd || process.env.HOME,
E
.  
Eugene Pankov 已提交
43
            env: env,
E
.  
Eugene Pankov 已提交
44 45
        })

E
.  
Eugene Pankov 已提交
46 47 48 49 50 51 52
        if (options.recoveredTruePID$) {
            options.recoveredTruePID$.subscribe(pid => {
                this.truePID = pid
            })
        } else {
            this.truePID = (<any>this.pty).pid
        }
E
.  
Eugene Pankov 已提交
53

E
.  
Eugene Pankov 已提交
54 55 56
        this.open = true

        this.pty.on('data', (data) => {
E
.  
Eugene Pankov 已提交
57 58 59
            if (!this.initialDataBufferReleased) {
                this.initialDataBuffer += data
            } else {
E
.  
Eugene Pankov 已提交
60
                this.output$.next(data)
E
.  
Eugene Pankov 已提交
61
            }
E
.  
Eugene Pankov 已提交
62 63 64
        })

        this.pty.on('close', () => {
E
.  
Eugene Pankov 已提交
65 66 67
            if (this.open) {
                this.destroy()
            }
E
.  
Eugene Pankov 已提交
68 69 70
        })
    }

E
.  
Eugene Pankov 已提交
71 72
    releaseInitialDataBuffer () {
        this.initialDataBufferReleased = true
E
.  
Eugene Pankov 已提交
73
        this.output$.next(this.initialDataBuffer)
E
.  
Eugene Pankov 已提交
74 75 76
        this.initialDataBuffer = null
    }

E
.  
Eugene Pankov 已提交
77 78 79 80 81 82 83 84
    resize (columns, rows) {
        this.pty.resize(columns, rows)
    }

    write (data) {
        this.pty.write(data)
    }

E
.  
Eugene Pankov 已提交
85
    kill (signal?: string) {
E
.  
Eugene Pankov 已提交
86 87 88
        this.pty.kill(signal)
    }

E
.  
Eugene Pankov 已提交
89 90 91 92 93
    async gracefullyKillProcess (): Promise<void> {
        if (process.platform == 'win32') {
            this.kill()
        } else {
            await new Promise((resolve) => {
E
.  
Eugene Pankov 已提交
94
                this.kill('SIGTERM')
E
.  
Eugene Pankov 已提交
95 96 97 98 99 100 101 102 103 104
                setImmediate(() => {
                    if (!this.open) {
                        resolve()
                    } else {
                        setTimeout(() => {
                            if (this.open) {
                                this.kill('SIGKILL')
                            }
                            resolve()
                        }, 1000)
E
.  
Eugene Pankov 已提交
105
                    }
E
.  
Eugene Pankov 已提交
106 107 108
                })
            })
        }
E
.  
Eugene Pankov 已提交
109 110
    }

E
.  
Eugene Pankov 已提交
111 112 113 114 115 116 117
    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 已提交
118 119
        }
    }
E
.  
Eugene Pankov 已提交
120 121

    async getWorkingDirectory (): Promise<string> {
E
done  
Eugene Pankov 已提交
122
        if (process.platform == 'darwin') {
E
.  
Eugene Pankov 已提交
123
            let lines = (await exec(`lsof -p ${this.truePID} -Fn`))[0].toString().split('\n')
E
.  
Eugene Pankov 已提交
124
            return lines[2].substring(1)
E
done  
Eugene Pankov 已提交
125 126 127 128 129
        }
        if (process.platform == 'linux') {
            return await fs.readlink(`/proc/${this.truePID}/cwd`)
        }
        return null
E
.  
Eugene Pankov 已提交
130
    }
E
.  
Eugene Pankov 已提交
131 132
}

E
.  
Eugene Pankov 已提交
133

E
.  
Eugene Pankov 已提交
134 135 136 137 138 139 140
@Injectable()
export class SessionsService {
    sessions: {[id: string]: Session} = {}
    logger: Logger
    private lastID = 0

    constructor(
E
.  
Eugene Pankov 已提交
141
        private persistence: SessionPersistenceProvider,
E
.  
Eugene Pankov 已提交
142 143 144
        log: LogService,
    ) {
        this.logger = log.create('sessions')
E
.  
Eugene Pankov 已提交
145 146
    }

E
.  
Eugene Pankov 已提交
147
    async createNewSession (options: SessionOptions) : Promise<Session> {
E
.  
Eugene Pankov 已提交
148 149 150 151
        if (this.persistence) {
            let recoveryId = await this.persistence.startSession(options)
            options = await this.persistence.attachSession(recoveryId)
        }
E
.  
Eugene Pankov 已提交
152
        let session = this.addSession(options)
E
.  
Eugene Pankov 已提交
153
        return session
E
.  
Eugene Pankov 已提交
154 155
    }

E
.  
Eugene Pankov 已提交
156
    addSession (options: SessionOptions) : Session {
E
.  
Eugene Pankov 已提交
157 158 159
        this.lastID++
        options.name = `session-${this.lastID}`
        let session = new Session(options)
E
.  
Eugene Pankov 已提交
160
        session.destroyed$.first().subscribe(() => {
E
.  
Eugene Pankov 已提交
161
            delete this.sessions[session.name]
E
.  
Eugene Pankov 已提交
162 163 164
            if (this.persistence) {
                this.persistence.terminateSession(session.recoveryId)
            }
E
.  
Eugene Pankov 已提交
165 166 167 168
        })
        this.sessions[session.name] = session
        return session
    }
E
.  
Eugene Pankov 已提交
169

E
.  
Eugene Pankov 已提交
170
    async recover (recoveryId: string) : Promise<Session> {
E
.  
Eugene Pankov 已提交
171 172 173
        if (!this.persistence) {
            return null
        }
E
.  
Eugene Pankov 已提交
174
        const options = await this.persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
175 176 177 178
        if (!options) {
            return null
        }
        return this.addSession(options)
E
.  
Eugene Pankov 已提交
179
    }
E
.  
Eugene Pankov 已提交
180
}