index.js 5.9 KB

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