UNPKG

4.63 kBJavaScriptView Raw
1//
2
3const child_process = require('child_process') // eslint-disable-line camelcase
4const debug = require('debug')('observable-process')
5const extend = require('extend')
6const mergeStream = require('merge-stream')
7const stringArgv = require('string-argv')
8const TextStreamSearch = require('text-stream-search')
9
10// a list of environment variables
11
12
13
14
15
16
17
18// a stream that we can write into using write(string),
19// plus some boilerplate to make process.stdout fit in here
20
21
22
23
24
25
26
27
28// Runs the given command as a separate, parallel process
29// and allows to observe it.
30class ObservableProcess {
31
32
33
34
35
36
37 // eslint-disable-next-line no-undef
38 // eslint-disable-line camelcase
39
40
41
42
43
44
45 // command: "ls -la *.js"
46 // commands: ["ls", "-la", "*.js"]
47 // options.verbose: whether to log
48 // .stdout: the stdout stream to write output to
49 // .stderr: the stderr stream to write errors to
50 constructor (args
51
52
53
54
55
56
57
58 ) {
59 if (args.env != null) this.env = args.env
60 this.verbose = args.verbose || false
61 this.cwd = args.cwd != null ? args.cwd : process.cwd()
62 this.stdout = args.stdout || process.stdout
63 this.stderr = args.stderr || process.stderr
64 this.ended = false
65 this.endedListeners = []
66
67 // build up the options
68 // eslint-disable-next-line camelcase, no-undef
69 const options = {
70 env: {},
71 cwd: this.cwd
72 }
73 extend(options.env, process.env, this.env)
74 var runnable = ''
75 var params = []
76 if (args.command != null) {
77 ;[runnable, ...params] = this._splitCommand(args.command)
78 }
79 if (args.commands != null) {
80 runnable = args.commands[0]
81 params = args.commands.splice(1)
82 }
83 debug(`starting '${runnable}' with arguments [${params.join(',')}]`)
84 this.process = child_process.spawn(runnable, params, options)
85 this.process.on('close', this._onClose.bind(this))
86
87 this.textStreamSearch = new TextStreamSearch(
88 mergeStream(this.process.stdout, this.process.stderr)
89 )
90
91 if (this.stdout) {
92 this.process.stdout.on('data', data => {
93 this.stdout.write(data.toString())
94 })
95 }
96 if (this.stderr) {
97 this.process.stderr.on('data', data => {
98 this.stderr.write(data.toString())
99 })
100 }
101
102 // indicates whether this process has been officially killed
103 // (to avoid unnecessary panic if it is killed)
104 this.killed = false
105
106 this.stdin = this.process.stdin
107 }
108
109 // Enters the given text into the subprocess.
110 // Types the ENTER key automatically.
111 enter (text ) {
112 this.stdin.write(`${text}\n`)
113 }
114
115 fullOutput () {
116 return this.textStreamSearch.fullText()
117 }
118
119 kill () {
120 debug('killing the process')
121 this.killed = true
122 this.process.kill()
123 }
124
125 // notifies all registered listeners that this process has ended
126 notifyEnded () {
127 for (let resolver of this.endedListeners) {
128 resolver({ exitCode: this.exitCode, killed: this.killed })
129 }
130 }
131
132 _onClose (exitCode ) {
133 debug(`process has ended with code ${exitCode}`)
134 this.exitCode = exitCode
135 this.ended = true
136 if (this.verbose) {
137 if (this.stderr) this.stderr.write('PROCESS ENDED\n')
138 if (this.stderr) this.stderr.write(`\nEXIT CODE: ${this.exitCode}`)
139 }
140 this.notifyEnded()
141 }
142
143 pid () {
144 if (this.process) return this.process.pid
145 }
146
147 waitForEnd () {
148 return new Promise(resolve => {
149 this.endedListeners.push(resolve)
150 })
151 }
152
153 // Calls the given handler when the given text shows up in the output
154 async waitForText (text , timeout ) {
155 await this.textStreamSearch.waitForText(text, timeout)
156 }
157
158 resetOutputStreams () {
159 this.textStreamSearch.reset()
160 }
161
162 _splitCommand (command ) {
163 if (Array.isArray(command)) {
164 return command
165 } else {
166 return stringArgv(command)
167 }
168 }
169}
170
171module.exports = ObservableProcess