From 22154488a2322aba19f6e2c1ca297c4ccd136cfd Mon Sep 17 00:00:00 2001 From: Isaac Murchie Date: Fri, 1 Mar 2019 08:16:28 -0500 Subject: [PATCH 1/5] Require from global if opencv4nodejs is not found locally --- lib/image-util.js | 44 +++++++++++++++++++++++++++++++++++++------- lib/logging.js | 1 + 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/image-util.js b/lib/image-util.js index 0325647f..0307c3bd 100644 --- a/lib/image-util.js +++ b/lib/image-util.js @@ -4,6 +4,10 @@ import { Buffer } from 'buffer'; import { PNG } from 'pngjs'; import B from 'bluebird'; import { hasValue } from './util'; +import { exec } from 'teen_process'; +import path from 'path'; +import fs from './fs'; + const { MIME_JPEG, MIME_PNG, MIME_BMP } = Jimp; let cv = null; @@ -87,18 +91,44 @@ async function getJimpImage (data) { }); } +/** + * Utility function to extend node functionality, allowing us to require + * modules that are installed globally + * + * @param {string} packageName - the name of the package to be required + */ +async function requirePackage (packageName) { + // see if we can get it locally + try { + return require(packageName); + } catch (ign) {} + + // find the npm global root + const {stdout} = await exec('npm', ['root', '-g']); + const globalNPMRoot = stdout.trim(); + + // get the global package root + const packageDir = path.join(globalNPMRoot, packageName); + if (!await fs.exists(packageDir)) { + // not installed + throw new Error(`Unable to find global '${packageName}' module`); + } + + return require(packageDir); +} + /** * @throws {Error} If opencv4nodejs module is not installed or cannot be loaded */ -function initOpenCV () { +async function initOpenCV () { if (!cv) { try { - cv = require('opencv4nodejs'); + cv = await requirePackage('opencv4nodejs'); } catch (ign) {} } if (!cv) { - throw new Error('opencv4nodejs module is required to use OpenCV features. ' + - 'Please install it first (npm i -g opencv4nodejs) and restart Appium. ' + + throw new Error(`'opencv4nodejs' module is required to use OpenCV features. ` + + `Please install it first ('npm i -g opencv4nodejs') and restart Appium. ` + 'Read https://github.com/justadudewhohacks/opencv4nodejs#how-to-install for more details on this topic.'); } } @@ -235,7 +265,7 @@ function highlightRegion (mat, region) { * @throws {Error} If `detectorName` value is unknown. */ async function getImagesMatches (img1Data, img2Data, options = {}) { - initOpenCV(); + await initOpenCV(); const {detectorName = 'ORB', visualize = false, goodMatchesFactor, matchFunc = 'BruteForce'} = options; @@ -340,7 +370,7 @@ async function getImagesMatches (img1Data, img2Data, options = {}) { * @throws {Error} If the given images have different resolution. */ async function getImagesSimilarity (img1Data, img2Data, options = {}) { - initOpenCV(); + await initOpenCV(); const {visualize = false} = options; let [template, reference] = await B.all([ @@ -422,7 +452,7 @@ async function getImagesSimilarity (img1Data, img2Data, options = {}) { * @throws {Error} If no occurences of the partial image can be found in the full image */ async function getImageOccurrence (fullImgData, partialImgData, options = {}) { - initOpenCV(); + await initOpenCV(); const {visualize = false, threshold = DEFAULT_MATCH_THRESHOLD} = options; const [fullImg, partialImg] = await B.all([ diff --git a/lib/logging.js b/lib/logging.js index 06177b63..47a53675 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -53,6 +53,7 @@ function getLogger (prefix = null) { enumerable: true, configurable: true }); + // This lambda function is necessary to workaround unexpected memory leaks // caused by NodeJS behavior described in https://bugs.chromium.org/p/v8/issues/detail?id=2869 const unleakIfString = (x) => _.isString(x) ? ` ${x}`.substr(1) : x; From 937289eae7ebe6d2552b15bae8144eefecf34a92 Mon Sep 17 00:00:00 2001 From: Isaac Murchie Date: Fri, 1 Mar 2019 09:46:12 -0500 Subject: [PATCH 2/5] Add windows to travis build --- .travis.yml | 3 +++ lib/image-util.js | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bef74968..f415ed23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ os: linux dist: xenial language: node_js +os: + - linux + - windows node_js: - "10" - "12" diff --git a/lib/image-util.js b/lib/image-util.js index 0307c3bd..4a3bbc1e 100644 --- a/lib/image-util.js +++ b/lib/image-util.js @@ -7,6 +7,7 @@ import { hasValue } from './util'; import { exec } from 'teen_process'; import path from 'path'; import fs from './fs'; +import log from './logger'; const { MIME_JPEG, MIME_PNG, MIME_BMP } = Jimp; @@ -96,6 +97,7 @@ async function getJimpImage (data) { * modules that are installed globally * * @param {string} packageName - the name of the package to be required + * @returns {object} - the package object */ async function requirePackage (packageName) { // see if we can get it locally @@ -109,6 +111,7 @@ async function requirePackage (packageName) { // get the global package root const packageDir = path.join(globalNPMRoot, packageName); + log.debug(`Loading package '${packageName}' from '${packageDir}'`); if (!await fs.exists(packageDir)) { // not installed throw new Error(`Unable to find global '${packageName}' module`); @@ -124,7 +127,9 @@ async function initOpenCV () { if (!cv) { try { cv = await requirePackage('opencv4nodejs'); - } catch (ign) {} + } catch (err) { + log.debug(`Unable to load 'opencv4nodejs': ${err.message}`); + } } if (!cv) { throw new Error(`'opencv4nodejs' module is required to use OpenCV features. ` + From b44a72d79ede2527c1c2d9afc791919a7f81ae15 Mon Sep 17 00:00:00 2001 From: Isaac Murchie Date: Fri, 1 Mar 2019 11:42:31 -0500 Subject: [PATCH 3/5] Experiment with Windows on Travis --- .travis.yml | 2 +- test/image-util-e2e-specs.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f415ed23..593d8c04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ dist: xenial language: node_js os: - linux - - windows node_js: - "10" - "12" @@ -17,6 +16,7 @@ env: global: - CXX=clang++-5.0 - CC=clang-5.0 + - _FORCE_LOGS=1 install: # on node 12 opencv4nodejs and mjpeg-consumer cannot be installed - if [[ `node --version` != v12* ]]; then diff --git a/test/image-util-e2e-specs.js b/test/image-util-e2e-specs.js index cb909339..dc3d70fd 100644 --- a/test/image-util-e2e-specs.js +++ b/test/image-util-e2e-specs.js @@ -73,7 +73,7 @@ describe('image-util', function () { }); describe('getImagesMatches', function () { - it('should calculate the number of matches between two images', async function () { + it.only('should calculate the number of matches between two images', async function () { // eslint-disable-line for (const detectorName of ['AKAZE', 'ORB']) { const {count, totalCount} = await getImagesMatches(fullImage, fullImage, {detectorName}); count.should.be.above(0); From 5676cdb2ed9d4673dc4eef0a78bbced0af47ee73 Mon Sep 17 00:00:00 2001 From: Isaac Murchie Date: Mon, 4 Mar 2019 11:45:17 -0500 Subject: [PATCH 4/5] See what the path is --- .travis.yml | 9 +++++++-- lib/image-util.js | 38 ++++++++++++++++++++++-------------- test/image-util-e2e-specs.js | 2 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 593d8c04..dccee4ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,9 @@ addons: - llvm-toolchain-r-test packages: - clang-5.0 +cache: + directories: + - "$(npm root -g)" env: global: - CXX=clang++-5.0 @@ -23,13 +26,15 @@ install: printf "while [ true ]; do\nsleep 30\necho 'Building OpenCV'\ndone" > ping.sh; bash ping.sh & echo $! > ping.pid; - npm i opencv4nodejs > /dev/null 2>&1; + npm install -g opencv4nodejs > /dev/null 2>&1; kill `cat ping.pid`; npm install --no-save mjpeg-consumer; fi - npm install script: - - npm run test && npm run e2e-test + - npm ls -g opencv4nodejs -j + - ls -la "C:\ProgramData\nvs\node\10.15.2\x64\node_modules\opencv4nodejs" + - _FORCE_LOGS=1 npm run e2e-test after_success: - npm run coverage diff --git a/lib/image-util.js b/lib/image-util.js index 4a3bbc1e..6f2f9894 100644 --- a/lib/image-util.js +++ b/lib/image-util.js @@ -6,8 +6,8 @@ import B from 'bluebird'; import { hasValue } from './util'; import { exec } from 'teen_process'; import path from 'path'; -import fs from './fs'; import log from './logger'; +import { isWindows } from './system'; const { MIME_JPEG, MIME_PNG, MIME_BMP } = Jimp; @@ -102,35 +102,43 @@ async function getJimpImage (data) { async function requirePackage (packageName) { // see if we can get it locally try { + log.debug(`Loading local package '${packageName}'`); return require(packageName); - } catch (ign) {} + } catch (err) { + log.debug(`Unable to load local package '${packageName}': ${err.message}`); + } // find the npm global root - const {stdout} = await exec('npm', ['root', '-g']); + const cmd = isWindows() ? 'npm.cmd' : 'npm'; + const {stdout} = await exec(cmd, ['root', '-g'], {timeout: 20000}); const globalNPMRoot = stdout.trim(); // get the global package root const packageDir = path.join(globalNPMRoot, packageName); - log.debug(`Loading package '${packageName}' from '${packageDir}'`); - if (!await fs.exists(packageDir)) { - // not installed - throw new Error(`Unable to find global '${packageName}' module`); - } + log.debug(`Loading global package '${packageName}' from '${packageDir}'`); - return require(packageDir); + try { + return require(packageDir); + } catch (err) { + throw new Error(`Unable to find global package '${packageName}' at '${packageDir}': ${err.message}`); + } } /** * @throws {Error} If opencv4nodejs module is not installed or cannot be loaded */ async function initOpenCV () { - if (!cv) { - try { - cv = await requirePackage('opencv4nodejs'); - } catch (err) { - log.debug(`Unable to load 'opencv4nodejs': ${err.message}`); - } + if (cv) { + return; + } + + log.debug(`Initializing opencv`); + try { + cv = await requirePackage('opencv4nodejs'); + } catch (err) { + log.debug(`Unable to load 'opencv4nodejs': ${err.message}`); } + if (!cv) { throw new Error(`'opencv4nodejs' module is required to use OpenCV features. ` + `Please install it first ('npm i -g opencv4nodejs') and restart Appium. ` + diff --git a/test/image-util-e2e-specs.js b/test/image-util-e2e-specs.js index dc3d70fd..c99fe82a 100644 --- a/test/image-util-e2e-specs.js +++ b/test/image-util-e2e-specs.js @@ -53,7 +53,7 @@ describe('image-util', function () { describe('OpenCV helpers', function () { // OpenCV needs several seconds for initialization - this.timeout(20000); + this.timeout(120000); let imgFixture = null; let fullImage = null; From 176b9c50fcf06e46fbf9ae27ad58bf8f39c380f3 Mon Sep 17 00:00:00 2001 From: Isaac Murchie Date: Mon, 26 Aug 2019 10:07:57 -0400 Subject: [PATCH 5/5] switch to linking package --- .travis.yml | 23 ++++++++++-------- index.js | 5 ++-- lib/image-util.js | 38 ++--------------------------- lib/mjpeg.js | 9 ++++--- lib/node.js | 54 +++++++++++++++++++++++++++++++++++++++++ test/.eslintrc | 5 ++++ test/mjpeg-e2e-specs.js | 4 +-- test/node-e2e-specs.js | 21 ++++++++++++++++ 8 files changed, 105 insertions(+), 54 deletions(-) create mode 100644 lib/node.js create mode 100644 test/.eslintrc create mode 100644 test/node-e2e-specs.js diff --git a/.travis.yml b/.travis.yml index dccee4ee..30571c29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -os: linux dist: xenial language: node_js os: @@ -22,19 +21,23 @@ env: - _FORCE_LOGS=1 install: # on node 12 opencv4nodejs and mjpeg-consumer cannot be installed + # also handle possible travis caching - if [[ `node --version` != v12* ]]; then - printf "while [ true ]; do\nsleep 30\necho 'Building OpenCV'\ndone" > ping.sh; - bash ping.sh & - echo $! > ping.pid; - npm install -g opencv4nodejs > /dev/null 2>&1; - kill `cat ping.pid`; + if [[ $(npm ls --depth 1 -g opencv4nodejs) =~ "── opencv4nodejs@" ]]; then + printf "while [ true ]; do\nsleep 30\necho 'Building OpenCV'\ndone" > ping.sh; + bash ping.sh & + echo $! > ping.pid; + npm install -g opencv4nodejs > /dev/null 2>&1; + kill `cat ping.pid`; + fi - npm install --no-save mjpeg-consumer; + if [[ $(npm ls --depth 1 -g mjpeg-consumer) =~ "── mjpeg-consumer@" ]]; then + npm install -g mjpeg-consumer; + fi fi - npm install script: - - npm ls -g opencv4nodejs -j - - ls -la "C:\ProgramData\nvs\node\10.15.2\x64\node_modules\opencv4nodejs" - - _FORCE_LOGS=1 npm run e2e-test + - npm run test + - npm run e2e-test after_success: - npm run coverage diff --git a/index.js b/index.js index 9f84948b..155810ee 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import * as process from './lib/process'; import * as zip from './lib/zip'; import * as imageUtil from './lib/image-util'; import * as mjpeg from './lib/mjpeg'; +import * as node from './lib/node'; const { fs } = fsIndex; @@ -18,9 +19,9 @@ const { mkdirp } = mkdirpIndex; export { tempDir, system, util, fs, cancellableDelay, plist, mkdirp, logger, process, - zip, imageUtil, net, mjpeg + zip, imageUtil, net, mjpeg, node, }; export default { tempDir, system, util, fs, cancellableDelay, plist, mkdirp, logger, process, - zip, imageUtil, net, mjpeg + zip, imageUtil, net, mjpeg, node, }; diff --git a/lib/image-util.js b/lib/image-util.js index 6f2f9894..f1108f88 100644 --- a/lib/image-util.js +++ b/lib/image-util.js @@ -4,10 +4,8 @@ import { Buffer } from 'buffer'; import { PNG } from 'pngjs'; import B from 'bluebird'; import { hasValue } from './util'; -import { exec } from 'teen_process'; -import path from 'path'; import log from './logger'; -import { isWindows } from './system'; +import { requirePackage } from './node'; const { MIME_JPEG, MIME_PNG, MIME_BMP } = Jimp; @@ -92,38 +90,6 @@ async function getJimpImage (data) { }); } -/** - * Utility function to extend node functionality, allowing us to require - * modules that are installed globally - * - * @param {string} packageName - the name of the package to be required - * @returns {object} - the package object - */ -async function requirePackage (packageName) { - // see if we can get it locally - try { - log.debug(`Loading local package '${packageName}'`); - return require(packageName); - } catch (err) { - log.debug(`Unable to load local package '${packageName}': ${err.message}`); - } - - // find the npm global root - const cmd = isWindows() ? 'npm.cmd' : 'npm'; - const {stdout} = await exec(cmd, ['root', '-g'], {timeout: 20000}); - const globalNPMRoot = stdout.trim(); - - // get the global package root - const packageDir = path.join(globalNPMRoot, packageName); - log.debug(`Loading global package '${packageName}' from '${packageDir}'`); - - try { - return require(packageDir); - } catch (err) { - throw new Error(`Unable to find global package '${packageName}' at '${packageDir}': ${err.message}`); - } -} - /** * @throws {Error} If opencv4nodejs module is not installed or cannot be loaded */ @@ -136,7 +102,7 @@ async function initOpenCV () { try { cv = await requirePackage('opencv4nodejs'); } catch (err) { - log.debug(`Unable to load 'opencv4nodejs': ${err.message}`); + log.warn(`Unable to load 'opencv4nodejs': ${err.message}`); } if (!cv) { diff --git a/lib/mjpeg.js b/lib/mjpeg.js index e131f70d..0e0f6218 100644 --- a/lib/mjpeg.js +++ b/lib/mjpeg.js @@ -6,6 +6,7 @@ import B from 'bluebird'; import { getJimpImage, MIME_PNG } from './image-util'; import mJpegServer from 'mjpeg-server'; import { Writable } from 'stream'; +import { requirePackage } from './node'; // lazy load this, as it might not be available @@ -14,10 +15,10 @@ let MJpegConsumer = null; /** * @throws {Error} If `mjpeg-consumer` module is not installed or cannot be loaded */ -function initMJpegConsumer () { +async function initMJpegConsumer () { if (!MJpegConsumer) { try { - MJpegConsumer = require('mjpeg-consumer'); + MJpegConsumer = await requirePackage('mjpeg-consumer'); } catch (ign) {} } if (!MJpegConsumer) { @@ -44,8 +45,6 @@ class MJpegStream extends Writable { constructor (mJpegUrl, errorHandler = _.noop, options = {}) { super(options); - initMJpegConsumer(); - this.errorHandler = errorHandler; this.url = mJpegUrl; this.clear(); @@ -107,6 +106,8 @@ class MJpegStream extends Writable { // ensure we're not started already this.stop(); + await initMJpegConsumer(); + this.consumer = new MJpegConsumer(); // use the deferred pattern so we can wait for the start of the stream diff --git a/lib/node.js b/lib/node.js new file mode 100644 index 00000000..d7e83047 --- /dev/null +++ b/lib/node.js @@ -0,0 +1,54 @@ +import { isWindows } from './system'; +import log from './logger'; +import { exec } from 'teen_process'; + + +/** + * Internal utility to link global package to local context + * + * @returns {string} - name of the package to link + * @throws {Error} If the command fails + */ +async function linkGlobalPackage (packageName) { + try { + log.debug(`Linking package '${packageName}'`); + const cmd = isWindows() ? 'npm.cmd' : 'npm'; + await exec(cmd, ['link', packageName], {timeout: 20000}); + } catch (err) { + const msg = `Unable to load package '${packageName}', linking failed: ${err.message}`; + log.debug(msg); + if (err.stderr) { + // log the stderr if there, but do not add to thrown error as it is + // _very_ verbose + log.debug(err.stderr); + } + throw new Error(msg); + } +} + +/** + * Utility function to extend node functionality, allowing us to require + * modules that are installed globally. If the package cannot be required, + * this will attempt to link the package and then re-require it + * + * @param {string} packageName - the name of the package to be required + * @returns {object} - the package object + * @throws {Error} If the package is not found locally or globally + */ +async function requirePackage (packageName) { + try { + log.debug(`Loading local package '${packageName}'`); + return require(packageName); + } catch (err) { + log.debug(`Failed to load package '${packageName}': ${err.message}`); + await linkGlobalPackage(packageName); + } + try { + log.debug(`Retrying load of local package '${packageName}'`); + return require(packageName); + } catch (err) { + log.errorAndThrow(`Unable to load package '${packageName}': ${err.message}`); + } +} + +export { requirePackage }; diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 00000000..6c8f75a1 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "func-names": 0 + } +} diff --git a/test/mjpeg-e2e-specs.js b/test/mjpeg-e2e-specs.js index 9fbdbcf7..11c97389 100644 --- a/test/mjpeg-e2e-specs.js +++ b/test/mjpeg-e2e-specs.js @@ -15,13 +15,13 @@ const MJPEG_SERVER_URL = `http://localhost:${MJPEG_SERVER_PORT}`; describe('MJpeg Stream (e2e)', function () { let mJpegServer, stream; - before(function () { + before(async function () { // TODO: remove when buffertools can handle v12 if (process.version.startsWith('v12')) { return this.skip(); } - mJpegServer = initMJpegServer(MJPEG_SERVER_PORT); + mJpegServer = await initMJpegServer(MJPEG_SERVER_PORT); }); after(function () { diff --git a/test/node-e2e-specs.js b/test/node-e2e-specs.js new file mode 100644 index 00000000..8fd11110 --- /dev/null +++ b/test/node-e2e-specs.js @@ -0,0 +1,21 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { node } from '..'; + + +chai.should(); +chai.use(chaiAsPromised); + +describe('node utilities', function () { + describe('requirePackage', function () { + it('should be able to require a local package', async function () { + await node.requirePackage('chai').should.not.be.rejected; + }); + it('should be able to require a global package', async function () { + await node.requirePackage('npm').should.not.be.rejected; + }); + it('should fail to find uninstalled package', async function () { + await node.requirePackage('appium-foo-driver').should.eventually.be.rejectedWith(/Unable to load package/); + }); + }); +});