UNPKG

5.82 kBJavaScriptView Raw
1/* eslint-disable node/no-deprecated-api */
2
3'use strict'
4
5// our debug log messages
6const debug = require('debug')('xvfb')
7const once = require('lodash.once')
8const fs = require('fs')
9const path = require('path')
10const spawn = require('child_process').spawn
11fs.exists = fs.exists || path.exists
12fs.existsSync = fs.existsSync || path.existsSync
13
14function 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
24Xvfb.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 // When spawn fails, the callback will immediately be called.
54 // So we don't have to check whether the lock file exists.
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 // https://github.com/cypress-io/xvfb/issues/1
141 // only reset truthy backed' up values
142 if (this._oldDisplay) {
143 process.env.DISPLAY = this._oldDisplay
144 } else {
145 // else delete the values to get back
146 // to undefined
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 // Bind an error listener to prevent an error from crashing node.
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
217module.exports = Xvfb