├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── COPYING ├── README.md ├── build.sh ├── package.json └── src ├── find-actual-executable.js └── xvfb-maybe.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "indent": [ 6 | 2, 7 | 2 8 | ], 9 | "semi": [ 10 | 2, 11 | "always" 12 | ], 13 | "no-console": 0 14 | }, 15 | "env": { 16 | "es6": true, 17 | "node": true 18 | }, 19 | "extends": "eslint:recommended" 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 27 | node_modules 28 | 29 | lib 30 | test-dist 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | vendor/jobber/Jobber 2 | vendor/jobber/Jobber.sln 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | 5 | addons: 6 | apt: 7 | sources: 8 | - ubuntu-toolchain-r-test 9 | packages: 10 | - gcc-4.9 11 | - g++-4.9 12 | - lcov 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at paul@paulbetts.org. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Paul Betts 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xvfb-maybe 2 | 3 | This package runs an arbitrary executable / args under `xvfb-run` if the 4 | platform is Linux and DISPLAY isn't set. This is super useful for making 5 | Electron unit tests run correctly in CI environments while still working 6 | locally 7 | 8 | ## Usage: 9 | 10 | ```sh 11 | ## On Windows or OS X, this just invokes electron-mocha 12 | ## On Linux, if we are in a headless environment, this will be equivalent 13 | ## to xvfb-run electron-mocha ./test/*.js 14 | xvfb-maybe electron-mocha ./test/*.js 15 | ``` 16 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm i && npm t 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xvfb-maybe", 3 | "version": "0.2.1", 4 | "description": "Runs xvfb-run only if you need to, useful for Electron unit tests", 5 | "bin": { 6 | "xvfb-maybe": "./src/xvfb-maybe.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/paulcbetts/xvfb-maybe" 11 | }, 12 | "keywords": [ 13 | "xvfb", 14 | "electron" 15 | ], 16 | "author": "Paul Betts ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/paulcbetts/xvfb-maybe/issues" 20 | }, 21 | "homepage": "https://github.com/paulcbetts/xvfb-maybe", 22 | "dependencies": { 23 | "debug": "^2.2.0", 24 | "which": "^1.2.4" 25 | }, 26 | "devDependencies": { 27 | "babel-eslint": "^5.0.0", 28 | "eslint": "^2.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/find-actual-executable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const sfs = require('fs'); 5 | const isWindows = process.platform === 'win32'; 6 | 7 | function statSyncNoException(file) { 8 | try { 9 | return sfs.statSync(file); 10 | } catch (e) { 11 | return null; 12 | } 13 | } 14 | 15 | function runDownPath(exe) { 16 | // NB: Windows won't search PATH looking for executables in spawn like 17 | // Posix does 18 | 19 | // Files with any directory path don't get this applied 20 | if (exe.match(/[\\\/]/)) { 21 | return exe; 22 | } 23 | 24 | let target = path.join('.', exe); 25 | if (statSyncNoException(target)) { 26 | return target; 27 | } 28 | 29 | let haystack = process.env.PATH.split(isWindows ? ';' : ':'); 30 | for (let p of haystack) { 31 | let needle = path.join(p, exe); 32 | if (statSyncNoException(needle)) return needle; 33 | } 34 | 35 | return exe; 36 | } 37 | 38 | function findActualExecutable(fullPath, args) { 39 | // POSIX can just execute scripts directly, no need for silly goosery 40 | if (process.platform !== 'win32') return { cmd: fullPath, args: args }; 41 | 42 | // NB: When you write something like `surf-client ... -- surf-build` on Windows, 43 | // a shell would normally convert that to surf-build.cmd, but since it's passed 44 | // in as an argument, it doesn't happen 45 | const possibleExts = ['.exe', '.bat', '.cmd', '.ps1']; 46 | let extToUse = possibleExts.find((x) => sfs.existsSync(fullPath + x)); 47 | 48 | if (extToUse) { 49 | let realExecutable = fullPath + extToUse; 50 | return findActualExecutable(realExecutable, args); 51 | } 52 | 53 | if (fullPath.match(/\.ps1$/i)) { 54 | let cmd = path.join(process.env.SYSTEMROOT, 'System32', 'WindowsPowerShell', 'v1.0', 'PowerShell.exe'); 55 | let psargs = ['-ExecutionPolicy', 'Unrestricted', '-NoLogo', '-NonInteractive', '-File', fullPath]; 56 | 57 | return { cmd: cmd, args: psargs.concat(args) }; 58 | } 59 | 60 | if (fullPath.match(/\.(bat|cmd)$/i)) { 61 | let cmd = path.join(process.env.SYSTEMROOT, 'System32', 'cmd.exe'); 62 | let cmdArgs = ['/C', fullPath]; 63 | 64 | return { cmd: cmd, args: cmdArgs.concat(args) }; 65 | } 66 | 67 | if (fullPath.match(/\.(js)$/i)) { 68 | let cmd = process.execPath; 69 | let nodeArgs = [fullPath]; 70 | 71 | return { cmd: cmd, args: nodeArgs.concat(args) }; 72 | } 73 | 74 | // Dunno lol 75 | return { cmd: fullPath, args: args }; 76 | } 77 | 78 | module.exports = { findActualExecutable: findActualExecutable, runDownPath: runDownPath }; 79 | -------------------------------------------------------------------------------- /src/xvfb-maybe.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const spawnOg = require('child_process').spawn; 6 | const which = require('which'); 7 | const fae = require('./find-actual-executable'); 8 | 9 | const d = require('debug')('xvfb-maybe'); 10 | 11 | function spawn(exe, params, opts) { 12 | opts = opts || null; 13 | 14 | let fullExe = fae.runDownPath(exe); 15 | let info = fae.findActualExecutable(fullExe, params); 16 | 17 | return new Promise((resolve, reject) => { 18 | let proc = null; 19 | 20 | d(`Spawning ${info.cmd} ${info.args.join(' ')}`); 21 | if (!opts) { 22 | proc = spawnOg(info.cmd, info.args); 23 | } else { 24 | proc = spawnOg(info.cmd, info.args, opts); 25 | } 26 | 27 | if (!proc) { 28 | reject(new Error("Failed to spawn process")); 29 | return; 30 | } 31 | 32 | // We need to wait until all three events have happened: 33 | // * stdout's pipe is closed 34 | // * stderr's pipe is closed 35 | // * We've got an exit code 36 | let rejected = false; 37 | let refCount = 3; 38 | let release = () => { 39 | if (--refCount <= 0 && !rejected) resolve(stdout); 40 | }; 41 | 42 | let stdout = ''; 43 | let bufHandler = (b) => { 44 | stdout += b.toString(); 45 | }; 46 | 47 | if (proc.stdout) { 48 | proc.stdout.on('data', bufHandler); 49 | proc.stdout.once('close', release); 50 | } else { 51 | release(); 52 | } 53 | 54 | if (proc.stderr) { 55 | proc.stderr.on('data', bufHandler); 56 | proc.stderr.once('close', release); 57 | } else { 58 | release(); 59 | } 60 | 61 | proc.on('error', (e) => reject(e)); 62 | 63 | proc.on('close', (code) => { 64 | if (code === 0) { 65 | release(); 66 | } else { 67 | rejected = true; 68 | reject(new Error(`Failed with exit code: ${code}\nOutput:\n${stdout}`)); 69 | } 70 | }); 71 | }); 72 | } 73 | 74 | function showHelp() { 75 | console.log("Usage: xvfb-maybe command args...\n"); 76 | console.log("Runs the given command under xvfb-run under Linux if DISPLAY isn't set\n"); 77 | } 78 | 79 | function main(args) { 80 | if (args.length < 1) { 81 | showHelp(); 82 | process.exit(-1); 83 | 84 | return Promise.resolve(true); 85 | } 86 | 87 | const dblDashPos = args.indexOf('--'), 88 | xvfbArgs = dblDashPos === -1 ? [] : args.splice(0, dblDashPos), 89 | exeCmd = args[dblDashPos === -1 ? 0 : 1], 90 | exeArgs = args.splice(dblDashPos === -1 ? 1 : 2); 91 | 92 | if (process.platform === 'win32' || process.platform === 'darwin') { 93 | d("Platform doesn't match, leaving"); 94 | return spawn(exeCmd, exeArgs, {cwd: undefined, env: process.env, stdio: 'inherit'}); 95 | } 96 | 97 | if (process.env.DISPLAY) { 98 | d("DISPLAY is set, using local X Server"); 99 | return spawn(exeCmd, exeArgs, {cwd: undefined, env: process.env, stdio: 'inherit'}); 100 | } 101 | 102 | let xvfbRun = null; 103 | try { 104 | xvfbRun = which.sync('xvfb-run'); 105 | } catch (e) { 106 | return Promise.reject(new Error("Failed to find xvfb-run in PATH. Use your distro's package manager to install it.")); 107 | } 108 | 109 | return spawn(xvfbRun, [].concat(xvfbArgs, [exeCmd], exeArgs), {cwd: undefined, env: process.env, stdio: 'inherit'}); 110 | } 111 | 112 | if (process.mainModule === module) { 113 | main(process.argv.splice(2)) 114 | .then(() => process.exit(0)) 115 | .catch((e) => { 116 | console.error(e.message); 117 | process.exit(-1); 118 | }); 119 | } 120 | --------------------------------------------------------------------------------