diff --git a/README.md b/README.md index a3e889f8..38e2d45f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ By default, Google Chrome, Firefox and Microsoft Edge are available when install Starting from `v6.22` chrome, edgechromium, and geckodriver support `latest` as version. -Starting from `v9.0.6` support changes regarding new storage for `latest` versions of chromedriver. +Starting from `v9.0.6` supported changes regarding new storage for `latest` versions of chromedriver. + +Starting from `v9.2.0` added new feature 'onlyDriver' ## Install & Run diff --git a/bin/selenium-standalone b/bin/selenium-standalone index 8706aa87..0f5168b3 100755 --- a/bin/selenium-standalone +++ b/bin/selenium-standalone @@ -22,12 +22,15 @@ const actions = { const killEvents = ['exit', 'SIGTERM', 'SIGINT']; const cp = await selenium.start(options); - console.log('Selenium started'); - - killEvents.forEach((evName) => { - process.once(evName, () => cp.kill('SIGTERM')); - }); - + if (!options.onlyDriver) { + console.log('Selenium started'); + + killEvents.forEach((evName) => { + process.once(evName, () => cp.kill('SIGTERM')); + }); + } else if (cp._handle) { + console.log(`started driver path ${cp.spawnfile}`); + } return cp; }, install: async function (options) { @@ -56,8 +59,9 @@ const actions = { bar.tick(chunk); } + const paths = await selenium.install(options); - await selenium.install(options); + return paths; }, }; diff --git a/docs/API.md b/docs/API.md index 74157d76..04645dac 100644 --- a/docs/API.md +++ b/docs/API.md @@ -26,7 +26,7 @@ async function myFn() { await selenium.install({ // check for more recent versions of selenium here: // https://selenium-release.storage.googleapis.com/index.html - version: process.env.SELENIUM_VERSION || '4.4.0', + version: process.env.SELENIUM_VERSION || '4.9.0', baseURL: 'https://selenium-release.storage.googleapis.com', drivers: { chrome: { @@ -86,6 +86,8 @@ arch [sometimes](https://code.google.com/p/selenium/issues/detail?id=5116#c9). `opts.requestOpts` can be any valid [`got` options object](https://www.npmjs.com/package/got#proxies). You can use this for example to set a timeout. +`opts.onlyDriver` can be any valid 'chrome' | 'firefox' | 'chromiumedge' it allow to install any driver without selenium server + returns `Promise` ## selenium.start([opts]) @@ -113,6 +115,8 @@ By default all drivers are loaded, you only control and change the versions or a `opts.processKiller` set to falsy value, for preventing killing selenium server port. +`opts.onlyDriver` can be any valid 'chrome' | 'firefox' | 'chromiumedge' it allow to start any driver directly without selenium server + returns `Promise` ## Error: Port 4444 is already in use. @@ -123,8 +127,6 @@ If you're getting this error, it means that you didn't shut down the server succ pkill -f selenium-standalone ``` -or use truthy `opts.processKiller` in config - ## Set `selenium-standalone` Version as NodeJS environment parameter You can set any version by `process.env.SELENIUM_VERSION=3.141.59` before starting selenium-standalone. Default values are here: [lib/default-config.js](../lib/default-config.js) diff --git a/docs/CLI.md b/docs/CLI.md index 452307de..0a698cd6 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -52,6 +52,10 @@ selenium-standalone start --config=./config/seleniumConfig.js # prevent killing selenium process before start selenium-standalone start --processKiller=false +# install or start only certain driver +selenium-standalone install --onlyDriver=chrome +selenium-standalone start --onlyDriver=chrome + ``` Config file can be a JSON file or a [module file](https://nodejs.org/api/modules.html#modules_file_modules) that exports options as an object: diff --git a/lib/compute-fs-paths.js b/lib/compute-fs-paths.js index 4b209002..d7cb50b4 100644 --- a/lib/compute-fs-paths.js +++ b/lib/compute-fs-paths.js @@ -48,7 +48,7 @@ const computeFsPaths = async (options) => { opts.basePath, 'iedriver', `${opts.drivers.ie.version}-${detectBrowserPlatformCustom(opts.drivers.ie.arch)}`, - 'IEDriverServer.exe' + 'IEDriverServer' ), }; } @@ -96,6 +96,10 @@ const computeFsPaths = async (options) => { downloadPath = acc[name].installPath + '.gz'; } else { downloadPath = acc[name].installPath + '.zip'; + + if (process.platform === 'win32') { + acc[name].installPath = `${acc[name].installPath}.exe`; + } } acc[name].downloadPath = downloadPath; return acc; diff --git a/lib/default-config.js b/lib/default-config.js index aa2911bb..92baaba5 100644 --- a/lib/default-config.js +++ b/lib/default-config.js @@ -7,6 +7,7 @@ module.exports = () => { version: 'latest', channel: 'stable', arch: process.arch, + onlyDriverArgs: [], baseURL: 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing', }, ie: { @@ -18,6 +19,7 @@ module.exports = () => { version: 'latest', fallbackVersion: '0.30.0', arch: process.arch, + onlyDriverArgs: [], baseURL: 'https://github.com/mozilla/geckodriver/releases/download', }, edge: { @@ -27,6 +29,7 @@ module.exports = () => { version: 'latest', fallbackVersion: '96.0.1054.34', arch: process.arch, + onlyDriverArgs: [], baseURL: 'https://msedgedriver.azureedge.net', }, }, diff --git a/lib/driver-starter.js b/lib/driver-starter.js new file mode 100644 index 00000000..310f52d5 --- /dev/null +++ b/lib/driver-starter.js @@ -0,0 +1,48 @@ +const ChildProcess = require('child_process'); +const delay = require('./delay'); + +async function startDriver(pathToDriver, args) { + const options = { + cwd: process.cwd(), + env: process.env, + stdio: 'inherit', + }; + let driverProcess; + + if (process.platform === 'win32' && !pathToDriver.endsWith('.exe')) { + driverProcess = ChildProcess.spawn('powershell', [ + `Start-Process -FilePath "${pathToDriver}"`, + '-Wait', + '-NoNewWindow', + ]); + } else { + driverProcess = ChildProcess.spawn(pathToDriver, args, options); + } + await delay.sleep(3000); + + driverProcess.on('close', (code) => { + if (code !== null && code !== 0 && code !== 1) { + throw new Error(`Chromedriver exited with error code: ${code}`); + } + }); + + driverProcess.on('error', (error) => { + throw new Error(error); + }); + + const killChromeDriver = () => { + try { + driverProcess.kill(); + } catch (_) { + // eslint-disable-next-line no-empty + } + }; + process.on('exit', killChromeDriver); + process.on('SIGTERM', killChromeDriver); + + return driverProcess; +} + +module.exports = { + startDriver, +}; diff --git a/lib/install-utils.js b/lib/install-utils.js index 36e34699..fe74ee57 100644 --- a/lib/install-utils.js +++ b/lib/install-utils.js @@ -128,7 +128,9 @@ async function uncompressDownloadedFile(zipFilePath) { } const extractPath = path.join( path.dirname(zipFilePath), - isBrowserDriver(entry.fileName) ? path.basename(zipFilePath, '.zip') : path.basename(entry.fileName) + isBrowserDriver(entry.fileName) + ? path.basename(zipFilePath, '.zip') + `${process.platform === 'win32' ? '.exe' : ''}` + : path.basename(entry.fileName) ); const extractWriteStream = fs .createWriteStream(extractPath) diff --git a/lib/install.js b/lib/install.js index e3bf8809..a2a84b41 100644 --- a/lib/install.js +++ b/lib/install.js @@ -54,11 +54,13 @@ async function install(_opts) { opts.drivers = defaultConfig.drivers; } - if (opts.singleDriverInstall) { - const singleDriver = opts.drivers[opts.singleDriverInstall]; + if (opts.singleDriverInstall || opts.onlyDriver) { + const driver = opts.singleDriverInstall || opts.onlyDriver; + const singleDriver = opts.drivers[driver]; + if (singleDriver) { opts.drivers = {}; - opts.drivers[opts.singleDriverInstall] = singleDriver; + opts.drivers[driver] = singleDriver; } } @@ -85,6 +87,9 @@ async function install(_opts) { basePath: opts.basePath, }); + if (opts.onlyDriver) { + delete fsPaths.selenium; + } const urls = await computeDownloadUrls({ seleniumVersion: opts.version, seleniumBaseURL: opts.baseURL, @@ -99,6 +104,7 @@ async function install(_opts) { download.bind(null, { urls: urls, fsPaths: fsPaths, + opts: opts, }), asyncLogEnd.bind(null, logger), ]; @@ -138,13 +144,15 @@ async function install(_opts) { } async function download(opts) { - const installers = [ - { + const installers = []; + + if (!opts.opts.onlyDriver) { + installers.push({ installer: installSelenium, from: opts.urls.selenium, to: opts.fsPaths.selenium.downloadPath, - }, - ]; + }); + } if (opts.fsPaths.chrome) { installers.push({ @@ -185,7 +193,6 @@ async function install(_opts) { to: opts.fsPaths.chromiumedge.downloadPath, }); } - return Promise.all(installers.map((i) => onlyInstallMissingFiles(i))); } diff --git a/lib/processKiller.js b/lib/processKiller.js index 9a0eabad..d863018d 100644 --- a/lib/processKiller.js +++ b/lib/processKiller.js @@ -18,23 +18,27 @@ function getConfigProcessesName(drivers) { processesName.push('IEDriverServer'); } else if (driverName === 'safari') { processesName.push('safaridriver'); + } else { + processesName.push(driverName); } } } return processesName; } -async function processKiller(drivers, portValue) { - if (portValue) { - if (!Number.isNaN(Number(`${portValue}`.startsWith(':') ? `${portValue}`.substring(1) : `${portValue}`))) { - const portCast = `${portValue}`.startsWith(':') ? portValue : `:${portValue}`; +async function processKiller(ports, processesName) { + if (ports && ports.length) { + for (const port of ports) { + if (port) { + const portCast = `${port}`.startsWith(':') ? port : `:${port}`; - await killProcessByFkill([portCast]); - await killProcessByCmd([`${portValue}`.startsWith(':') ? `${portValue}`.substring(1) : portValue], 'port'); + await killProcessByFkill([portCast]); + await killProcessByCmd([`${port}`.startsWith(':') ? `${port}`.substring(1) : port], 'port'); + } } } - if (drivers && typeof drivers === 'object' && Object.keys(drivers).length) { - await killProcess(getConfigProcessesName(drivers), 'name'); + if (processesName && processesName.length) { + await killProcess(getConfigProcessesName(processesName), 'name'); } } @@ -81,6 +85,4 @@ async function killProcessByFkill(processes) { } } -module.exports = { - processKiller, -}; +module.exports = processKiller; diff --git a/lib/start.js b/lib/start.js index dec5d2cc..653af6e0 100644 --- a/lib/start.js +++ b/lib/start.js @@ -15,7 +15,8 @@ const defaultConfig = require('./default-config')(); const { checkArgs } = require('./check-args'); const { isSelenium4 } = require('./isSelenium4'); const noop = require('./noop'); -const { processKiller } = require('./processKiller.js'); +const processKiller = require('./processKiller.js'); +const { startDriver } = require('./driver-starter.js'); async function start(_opts) { const opts = checkArgs('Start API', _opts); @@ -55,11 +56,13 @@ async function start(_opts) { opts.drivers = defaultConfig.drivers; } - if (opts.singleDriverStart) { - const singleDriver = opts.drivers[opts.singleDriverStart]; + if (opts.singleDriverStart || opts.onlyDriver) { + const driver = opts.singleDriverStart || opts.onlyDriver; + const singleDriver = opts.drivers[driver]; + if (singleDriver) { opts.drivers = {}; - opts.drivers[opts.singleDriverStart] = singleDriver; + opts.drivers[opts.singleDriverStart || opts.onlyDriver] = singleDriver; } } @@ -69,6 +72,10 @@ async function start(_opts) { basePath: opts.basePath, }); + if (opts.onlyDriver) { + delete fsPaths.selenium; + } + // programmatic use, did not give javaPath if (!opts.javaPath) { opts.javaPath = which.sync('java'); @@ -143,21 +150,42 @@ async function start(_opts) { opts.seleniumArgs.push('safari'); } - args.push(...opts.javaArgs, '-jar', fsPaths.selenium.installPath, ...opts.seleniumArgs); + let seleniumStatusUrl; + if (!opts.onlyDriver) { + args.push(...opts.javaArgs, '-jar', fsPaths.selenium.installPath, ...opts.seleniumArgs); + seleniumStatusUrl = statusUrl.getSeleniumStatusUrl(args, opts); + } await checkPathsExistence(Object.keys(fsPaths).map((name) => fsPaths[name].installPath)); - const seleniumStatusUrl = statusUrl.getSeleniumStatusUrl(args, opts); + if ( + (await isPortReachable((seleniumStatusUrl && seleniumStatusUrl.port) || 4444, { timeout: 100 })) || + opts.onlyDriver + ) { + const seleniumPort = (seleniumStatusUrl && seleniumStatusUrl.port) || 4444; - if (await isPortReachable(seleniumStatusUrl.port, { timeout: 100 })) { if (!('processKiller' in opts) || ('processKiller' in opts && opts.processKiller)) { - await processKiller(opts.drivers, seleniumStatusUrl.port); - - if (await isPortReachable(seleniumStatusUrl.port, { timeout: 100 })) { - throw new Error(`Port ${seleniumStatusUrl.port} is already in use.`); + if (opts.onlyDriver) { + const drivers = Object.keys(opts.drivers); + const ports = []; + + for (const driver of drivers) { + if (driver === 'firefox' && (await isPortReachable(4444, { timeout: 1000, host: '127.0.0.1' }))) { + ports.push(4444); + } + if ((driver === 'chrome' || driver === 'chromiumedge') && (await isPortReachable(9515, { timeout: 100 }))) { + ports.push(9515); + } + } + await processKiller([...ports, seleniumPort], Object.keys(opts.drivers)); + } else { + await processKiller([seleniumPort], Object.keys(opts.drivers)); + } + if (await isPortReachable(seleniumPort, { timeout: 100 })) { + throw new Error(`Port ${seleniumPort} is already in use.`); } } else { - throw new Error(`Port ${seleniumStatusUrl.port} is already in use.`); + throw new Error(`Port ${seleniumPort} is already in use.`); } } @@ -166,18 +194,29 @@ async function start(_opts) { } debug('Spawning Selenium Server process', opts.javaPath, args); - const selenium = spawn(opts.javaPath, args, opts.spawnOptions); - await checkStarted(selenium, seleniumStatusUrl.toString()); - if (selenium.stdout) { - selenium.stdout.on('data', noop); - } + if (opts.onlyDriver && opts.drivers[opts.onlyDriver]) { + const chromeDriverProcess = await startDriver( + fsPaths[opts.onlyDriver].installPath, + opts.drivers[opts.onlyDriver].onlyDriverArgs + ); - if (selenium.stderr) { - selenium.stderr.on('data', noop); - } + return chromeDriverProcess; + // eslint-disable-next-line no-else-return + } else { + const selenium = spawn(opts.javaPath, args, opts.spawnOptions); + await checkStarted(selenium, seleniumStatusUrl.toString()); + + if (selenium.stdout) { + selenium.stdout.on('data', noop); + } + + if (selenium.stderr) { + selenium.stderr.on('data', noop); + } - return selenium; + return selenium; + } } function hasParam(list, param) { diff --git a/test/only-driver-tests.js b/test/only-driver-tests.js new file mode 100644 index 00000000..290ea6bf --- /dev/null +++ b/test/only-driver-tests.js @@ -0,0 +1,164 @@ +const assert = require('assert'); +const start = require('../lib/start'); +const defaultConfig = require('../lib/default-config')(); +const install = require('../lib/install'); +const isPortReachable = require('is-port-reachable'); +const processKiller = require('../lib/processKiller'); +const checkPathsExistence = require('../lib/check-paths-existence'); +const computeFsPaths = require('../lib/compute-fs-paths'); +const path = require('path'); + +const opts = { + seleniumVersion: defaultConfig.version, + seleniumBaseURL: defaultConfig.baseURL, + drivers: defaultConfig.drivers, +}; + +describe('check onlyDriver downloading only driver without selenium server and others drivers', () => { + it('check onlyDriver', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chrome' }, ...opts }; + const paths = await install(testOpt); + + testOpt.drivers = {}; + testOpt.drivers.chrome = opts.drivers.chrome; + + const fsPaths = await computeFsPaths({ + seleniumVersion: opts.seleniumVersion, + drivers: testOpt.drivers, + basePath: path.join(__dirname, '..', '.selenium'), + }); + + try { + await checkPathsExistence(Object.keys(fsPaths).map((name) => fsPaths[name].installPath)); + + assert(false); + } catch { + await checkPathsExistence(Object.keys(paths.fsPaths).map((name) => fsPaths[name].installPath)); + } + }); + + it('check without onlyDriver', async () => { + await processKiller([9515, 4444]); + const testOpt = opts; + const paths = await install(testOpt); + + await checkPathsExistence(Object.keys(paths.fsPaths).map((name) => paths.fsPaths[name].installPath)); + }); +}); + +describe('check onlyDriver with certain name of driver', () => { + it('check "install" method with onlyDriver chromiumedge should return path with only certain driver', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chromiumedge' }, ...opts }; + const paths = await install(testOpt); + + assert(Object.keys(paths.fsPaths).length === 1 && Object.keys(paths.fsPaths).every((i) => i === 'chromiumedge')); + }); + + it('check "install" method with onlyDriver chrome should return path with only certain driver', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chrome' }, ...opts }; + const paths = await install(testOpt); + + assert(Object.keys(paths.fsPaths).length === 1 && Object.keys(paths.fsPaths).every((i) => i === 'chrome')); + }); + + it('check "install" method with onlyDriver firefox should return path with only certain driver', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'firefox' }, ...opts }; + const paths = await install(testOpt); + + assert(Object.keys(paths.fsPaths).length === 1 && Object.keys(paths.fsPaths).every((i) => i === 'firefox')); + }); + + it('check "install" method with onlyDriver firefox should return path with only certain driver', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'unknown' }, ...opts }; + + try { + await install(testOpt); + + assert(false); + } catch (_) { + // eslint-disable-next-line no-empty + } + }); +}); + +describe('check staring drivers twice with onlyDriver option', () => { + it('check staring twice chromedriver', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chrome' }, ...opts }; + const process1 = await start(testOpt); + + assert(await isPortReachable(9515)); + assert(process1._handle); + + const process2 = await start(testOpt); + + assert(await isPortReachable(9515)); + assert(!process1._handle); + assert(process2._handle); + }); + + it('check staring twice chromiumedge', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chromiumedge' }, ...opts }; + const process1 = await start(testOpt); + + assert(await isPortReachable(9515)); + assert(process1._handle); + + const process2 = await start(testOpt); + + assert(await isPortReachable(9515)); + assert(!process1._handle); + assert(process2._handle); + }); + + it('check staring twice firefox', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'firefox' }, ...opts }; + const process1 = await start(testOpt); + + assert(await isPortReachable(4444, { timeout: 1000, host: '127.0.0.1' })); + assert(process1._handle); + + const process2 = await start(testOpt); + + assert(await isPortReachable(4444, { timeout: 1000, host: '127.0.0.1' })); + assert(!process1._handle); + assert(process2._handle); + }); +}); + +describe('check staring drivers port existence', () => { + after(async () => { + await processKiller([9515, 4444], ['chromiumedge', 'chromedriver', 'firefox']); + }); + + it('check staring drivers port chrome', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chrome' }, ...opts }; + await start(testOpt); + + assert(await isPortReachable(9515)); + }); + + it('check staring drivers port firefox', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'firefox' }, ...opts }; + await start(testOpt); + + assert(await isPortReachable(4444, { timeout: 1000, host: '127.0.0.1' })); + }); + + it('check staring drivers port chromiumedge', async () => { + await processKiller([9515, 4444]); + const testOpt = { ...{ onlyDriver: 'chromiumedge' }, ...opts }; + await start(testOpt); + + assert(await isPortReachable(9515)); + }); +}); diff --git a/test/process-killer-test.js b/test/process-killer-test.js index 1078c63f..2b1ef91f 100644 --- a/test/process-killer-test.js +++ b/test/process-killer-test.js @@ -2,7 +2,7 @@ const assert = require('assert'); const merge = require('lodash.merge'); const start = require('../lib/start'); const defaultConfig = require('../lib/default-config')(); -const { processKiller } = require('../lib/processKiller'); +const processKiller = require('../lib/processKiller'); let opts = { seleniumVersion: defaultConfig.version, @@ -12,11 +12,11 @@ let opts = { describe('check usual a start by default', () => { before(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); after(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); it('check usual a start', async () => { @@ -30,11 +30,11 @@ describe('check usual a start by default', () => { describe('check killing before starting by default', () => { before(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); after(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); it('start selenium server twice with processKiller', async () => { @@ -53,11 +53,11 @@ describe('check killing before starting by default', () => { describe('check killing before when started twice', () => { before(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); after(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); it('start selenium server twice', async () => { @@ -72,11 +72,11 @@ describe('check killing before when started twice', () => { describe('check killing before starting with falsy processKiller property', () => { before(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); after(async () => { - await processKiller({}, ':4444'); + await processKiller([4444]); }); it('start selenium server twice with falsy processKiller property', async () => {