sessions.service.ts 4.5 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
done  
Eugene Pankov 已提交
6
const { exec } = require('child-process-promise')
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
        this.truePID = options.recoveredTruePID || (<any>this.pty).pid

E
.  
Eugene Pankov 已提交
48 49 50
        this.open = true

        this.pty.on('data', (data) => {
E
.  
Eugene Pankov 已提交
51 52 53
            if (!this.initialDataBufferReleased) {
                this.initialDataBuffer += data
            } else {
E
.  
Eugene Pankov 已提交
54
                this.output$.next(data)
E
.  
Eugene Pankov 已提交
55
            }
E
.  
Eugene Pankov 已提交
56 57 58
        })

        this.pty.on('close', () => {
E
.  
Eugene Pankov 已提交
59
            this.close()
E
.  
Eugene Pankov 已提交
60 61 62
        })
    }

E
.  
Eugene Pankov 已提交
63 64
    releaseInitialDataBuffer () {
        this.initialDataBufferReleased = true
E
.  
Eugene Pankov 已提交
65
        this.output$.next(this.initialDataBuffer)
E
.  
Eugene Pankov 已提交
66 67 68
        this.initialDataBuffer = null
    }

E
.  
Eugene Pankov 已提交
69 70 71 72 73 74 75 76 77 78 79 80 81 82
    resize (columns, rows) {
        this.pty.resize(columns, rows)
    }

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

    sendSignal (signal) {
        this.pty.kill(signal)
    }

    close () {
        this.open = false
E
.  
Eugene Pankov 已提交
83
        this.closed$.next()
E
.  
Eugene Pankov 已提交
84 85 86 87 88 89
        this.pty.end()
    }

    gracefullyDestroy () {
        return new Promise((resolve) => {
            this.sendSignal('SIGTERM')
E
.  
Eugene Pankov 已提交
90
            if (!this.open) {
E
.  
Eugene Pankov 已提交
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
                resolve()
                this.destroy()
            } else {
                setTimeout(() => {
                    if (this.open) {
                        this.sendSignal('SIGKILL')
                        this.destroy()
                    }
                    resolve()
                }, 1000)
            }
        })
    }

    destroy () {
        if (open) {
            this.close()
        }
E
.  
Eugene Pankov 已提交
109
        this.destroyed$.next()
E
.  
Eugene Pankov 已提交
110
        this.pty.destroy()
E
.  
Eugene Pankov 已提交
111
        this.output$.complete()
E
.  
Eugene Pankov 已提交
112
    }
E
.  
Eugene Pankov 已提交
113 114

    async getWorkingDirectory (): Promise<string> {
E
done  
Eugene Pankov 已提交
115 116 117 118 119 120 121 122
        if (process.platform == 'darwin') {
            let lines = (await exec(`lsof -p ${this.truePID} -Fn`)).split('\n')
            return lines[2]
        }
        if (process.platform == 'linux') {
            return await fs.readlink(`/proc/${this.truePID}/cwd`)
        }
        return null
E
.  
Eugene Pankov 已提交
123
    }
E
.  
Eugene Pankov 已提交
124 125
}

E
.  
Eugene Pankov 已提交
126

E
.  
Eugene Pankov 已提交
127 128 129 130 131 132 133
@Injectable()
export class SessionsService {
    sessions: {[id: string]: Session} = {}
    logger: Logger
    private lastID = 0

    constructor(
E
.  
Eugene Pankov 已提交
134
        private persistence: SessionPersistenceProvider,
E
.  
Eugene Pankov 已提交
135 136 137
        log: LogService,
    ) {
        this.logger = log.create('sessions')
E
.  
Eugene Pankov 已提交
138 139
    }

E
.  
Eugene Pankov 已提交
140
    async createNewSession (options: SessionOptions) : Promise<Session> {
E
.  
Eugene Pankov 已提交
141 142 143 144
        if (this.persistence) {
            let recoveryId = await this.persistence.startSession(options)
            options = await this.persistence.attachSession(recoveryId)
        }
E
.  
Eugene Pankov 已提交
145
        let session = this.addSession(options)
E
.  
Eugene Pankov 已提交
146
        return session
E
.  
Eugene Pankov 已提交
147 148
    }

E
.  
Eugene Pankov 已提交
149
    addSession (options: SessionOptions) : Session {
E
.  
Eugene Pankov 已提交
150 151 152
        this.lastID++
        options.name = `session-${this.lastID}`
        let session = new Session(options)
E
.  
Eugene Pankov 已提交
153
        session.destroyed$.first().subscribe(() => {
E
.  
Eugene Pankov 已提交
154
            delete this.sessions[session.name]
E
.  
Eugene Pankov 已提交
155 156 157
            if (this.persistence) {
                this.persistence.terminateSession(session.recoveryId)
            }
E
.  
Eugene Pankov 已提交
158 159 160 161
        })
        this.sessions[session.name] = session
        return session
    }
E
.  
Eugene Pankov 已提交
162

E
.  
Eugene Pankov 已提交
163
    async recover (recoveryId: string) : Promise<Session> {
E
.  
Eugene Pankov 已提交
164 165 166
        if (!this.persistence) {
            return null
        }
E
.  
Eugene Pankov 已提交
167
        const options = await this.persistence.attachSession(recoveryId)
E
.  
Eugene Pankov 已提交
168 169 170 171
        if (!options) {
            return null
        }
        return this.addSession(options)
E
.  
Eugene Pankov 已提交
172
    }
E
.  
Eugene Pankov 已提交
173
}