1 |
|
2 |
|
3 | 'use strict'
|
4 |
|
5 |
|
6 | const debug = require('debug')('xvfb')
|
7 | const once = require('lodash.once')
|
8 | const fs = require('fs')
|
9 | const path = require('path')
|
10 | const spawn = require('child_process').spawn
|
11 | fs.exists = fs.exists || path.exists
|
12 | fs.existsSync = fs.existsSync || path.existsSync
|
13 |
|
14 | function Xvfb(options) {
|
15 | options = options || {}
|
16 | this._display = options.displayNum ? `:${options.displayNum}` : null
|
17 | this._reuse = options.reuse
|
18 | this._timeout = options.timeout || 2000
|
19 | this._silent = options.silent
|
20 | this._onStderrData = options.onStderrData || (() => {})
|
21 | this._xvfb_args = options.xvfb_args || []
|
22 | }
|
23 |
|
24 | Xvfb.prototype = {
|
25 | start(cb) {
|
26 | let self = this
|
27 |
|
28 | if (!self._process) {
|
29 | let lockFile = self._lockFile()
|
30 |
|
31 | self._setDisplayEnvVariable()
|
32 |
|
33 | fs.exists(lockFile, function(exists) {
|
34 | let didSpawnFail = false
|
35 | try {
|
36 | self._spawnProcess(exists, function(e) {
|
37 | debug('XVFB spawn failed')
|
38 | debug(e)
|
39 | didSpawnFail = true
|
40 | if (cb) cb(e)
|
41 | })
|
42 | } catch (e) {
|
43 | debug('spawn process error')
|
44 | debug(e)
|
45 | return cb && cb(e)
|
46 | }
|
47 |
|
48 | let totalTime = 0
|
49 | ;(function checkIfStarted() {
|
50 | debug('checking if started by looking for the lock file', lockFile)
|
51 | fs.exists(lockFile, function(exists) {
|
52 | if (didSpawnFail) {
|
53 |
|
54 |
|
55 | debug('while checking for lock file, saw that spawn failed')
|
56 | return
|
57 | }
|
58 | if (exists) {
|
59 | debug('lock file %s found after %d ms', lockFile, totalTime)
|
60 | return cb && cb(null, self._process)
|
61 | } else {
|
62 | totalTime += 10
|
63 | if (totalTime > self._timeout) {
|
64 | debug(
|
65 | 'could not start XVFB after %d ms (timeout %d ms)',
|
66 | totalTime,
|
67 | self._timeout
|
68 | )
|
69 | const err = new Error('Could not start Xvfb.')
|
70 | err.timedOut = true
|
71 | return cb && cb(err)
|
72 | } else {
|
73 | setTimeout(checkIfStarted, 10)
|
74 | }
|
75 | }
|
76 | })
|
77 | })()
|
78 | })
|
79 | }
|
80 | },
|
81 |
|
82 | stop(cb) {
|
83 | let self = this
|
84 |
|
85 | if (self._process) {
|
86 | self._killProcess()
|
87 | self._restoreDisplayEnvVariable()
|
88 |
|
89 | let lockFile = self._lockFile()
|
90 | debug('lock file', lockFile)
|
91 | let totalTime = 0
|
92 | ;(function checkIfStopped() {
|
93 | fs.exists(lockFile, function(exists) {
|
94 | if (!exists) {
|
95 | debug('lock file %s not found when stopping', lockFile)
|
96 | return cb && cb(null, self._process)
|
97 | } else {
|
98 | totalTime += 10
|
99 | if (totalTime > self._timeout) {
|
100 | debug('lock file %s is still there', lockFile)
|
101 | debug(
|
102 | 'after waiting for %d ms (timeout %d ms)',
|
103 | totalTime,
|
104 | self._timeout
|
105 | )
|
106 | const err = new Error('Could not stop Xvfb.')
|
107 | err.timedOut = true
|
108 | return cb && cb(err)
|
109 | } else {
|
110 | setTimeout(checkIfStopped, 10)
|
111 | }
|
112 | }
|
113 | })
|
114 | })()
|
115 | } else {
|
116 | return cb && cb(null)
|
117 | }
|
118 | },
|
119 |
|
120 | display() {
|
121 | if (!this._display) {
|
122 | let displayNum = 98
|
123 | let lockFile
|
124 | do {
|
125 | displayNum++
|
126 | lockFile = this._lockFile(displayNum)
|
127 | } while (!this._reuse && fs.existsSync(lockFile))
|
128 | this._display = `:${displayNum}`
|
129 | }
|
130 |
|
131 | return this._display
|
132 | },
|
133 |
|
134 | _setDisplayEnvVariable() {
|
135 | this._oldDisplay = process.env.DISPLAY
|
136 | process.env.DISPLAY = this.display()
|
137 | },
|
138 |
|
139 | _restoreDisplayEnvVariable() {
|
140 |
|
141 |
|
142 | if (this._oldDisplay) {
|
143 | process.env.DISPLAY = this._oldDisplay
|
144 | } else {
|
145 |
|
146 |
|
147 | delete process.env.DISPLAY
|
148 | }
|
149 | },
|
150 |
|
151 | _spawnProcess(lockFileExists, onAsyncSpawnError) {
|
152 | let self = this
|
153 |
|
154 | const onError = once(onAsyncSpawnError)
|
155 |
|
156 | let display = self.display()
|
157 | if (lockFileExists) {
|
158 | if (!self._reuse) {
|
159 | throw new Error(
|
160 | `Display ${display} is already in use and the "reuse" option is false.`
|
161 | )
|
162 | }
|
163 | } else {
|
164 | const stderr = []
|
165 |
|
166 | const allArguments = [display].concat(self._xvfb_args)
|
167 | debug('all Xvfb arguments', allArguments)
|
168 |
|
169 | self._process = spawn('Xvfb', allArguments)
|
170 | self._process.stderr.on('data', function(data) {
|
171 | stderr.push(data.toString())
|
172 |
|
173 | if (self._silent) {
|
174 | return
|
175 | }
|
176 |
|
177 | self._onStderrData(data)
|
178 | })
|
179 |
|
180 | self._process.on('close', (code, signal) => {
|
181 | if (code !== 0) {
|
182 | const str = stderr.join('\n')
|
183 | debug('xvfb closed with error code', code)
|
184 | debug('after receiving signal %s', signal)
|
185 | debug('and stderr output')
|
186 | debug(str)
|
187 | const err = new Error(str)
|
188 | err.nonZeroExitCode = true
|
189 | onError(err)
|
190 | }
|
191 | })
|
192 |
|
193 |
|
194 | self._process.once('error', function(e) {
|
195 | debug('xvfb spawn process error')
|
196 | debug(e)
|
197 | onError(e)
|
198 | })
|
199 | }
|
200 | },
|
201 |
|
202 | _killProcess() {
|
203 | this._process.kill()
|
204 | this._process = null
|
205 | },
|
206 |
|
207 | _lockFile(displayNum) {
|
208 | displayNum =
|
209 | displayNum ||
|
210 | this.display()
|
211 | .toString()
|
212 | .replace(/^:/, '')
|
213 | return `/tmp/.X${displayNum}-lock`
|
214 | },
|
215 | }
|
216 |
|
217 | module.exports = Xvfb
|