From 41c2fdf4ae32ce65955ddc24de370b024b53b363 Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Wed, 19 Jan 2022 13:07:49 +0100 Subject: [PATCH] fix: live tunnel arch issue (#4064) * fix(dev): use the correct binary for operating system architecture. * fix: fixes an issue with downloading the wrong arch for system one * chore: fix linux arch * chore: pr feedback and handle ia32 architecture * chore: remove querystring and use URL Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- bin/run | 19 +---- npm-shrinkwrap.json | 88 ++++++++++++++++++++++ package.json | 1 + src/commands/base-command.js | 14 ++-- src/lib/exec-fetcher.js | 66 +++++++++++++++-- src/lib/exec-fetcher.test.js | 140 +++++++++++++++++++++++------------ src/utils/command-helpers.js | 2 +- 7 files changed, 253 insertions(+), 77 deletions(-) diff --git a/bin/run b/bin/run index 910a3f3a6f8..c0f85b99e23 100755 --- a/bin/run +++ b/bin/run @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* eslint-disable promise/prefer-await-to-then,promise/prefer-await-to-callbacks,eslint-comments/disable-enable-pair */ const process = require('process') const updateNotifier = require('update-notifier') @@ -18,23 +17,13 @@ if (require.main === module) { pkg, updateCheckInterval: UPDATE_CHECK_INTERVAL, }).notify() - } catch (error) { + } catch (error_) { console.log('Error checking for updates:') - console.log(error) + console.log(error_) } - /** @type {Error} */ - let caughtError - const program = createMainCommand() - program.parseAsync(process.argv).catch((error) => { - caughtError = error - }) - - // long running commands like dev server cannot be caught by a post action hook - // they are running on the main command - process.on('exit', () => { - program.onEnd(caughtError) - }) + // eslint-disable-next-line promise/prefer-await-to-then + program.parseAsync(process.argv).catch((error_) => program.onEnd(error_)) } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8798c45a1f2..04aa6a43d14 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -134,6 +134,7 @@ "mock-fs": "^5.1.2", "mock-require": "^3.0.3", "p-timeout": "^4.0.0", + "proxyquire": "^2.1.3", "seedrandom": "^3.0.5", "serialize-javascript": "^6.0.0", "sinon": "^12.0.0", @@ -10432,6 +10433,19 @@ "node": ">=6" } }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -14854,6 +14868,12 @@ "node": ">=6.0" } }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, "node_modules/moize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/moize/-/moize-6.1.0.tgz", @@ -17488,6 +17508,34 @@ "node": ">= 0.10" } }, + "node_modules/proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, + "node_modules/proxyquire/node_modules/resolve": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ps-list": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-7.2.0.tgz", @@ -29792,6 +29840,16 @@ "trim-repeated": "^1.0.0" } }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -33098,6 +33156,12 @@ "node-source-walk": "^4.0.0" } }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, "moize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/moize/-/moize-6.1.0.tgz", @@ -35107,6 +35171,30 @@ "ipaddr.js": "1.9.1" } }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + }, + "dependencies": { + "resolve": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "dev": true, + "requires": { + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, "ps-list": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-7.2.0.tgz", diff --git a/package.json b/package.json index 9f7e2c5ceff..b72d52584d4 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,7 @@ "mock-fs": "^5.1.2", "mock-require": "^3.0.3", "p-timeout": "^4.0.0", + "proxyquire": "^2.1.3", "seedrandom": "^3.0.5", "serialize-javascript": "^6.0.0", "sinon": "^12.0.0", diff --git a/src/commands/base-command.js b/src/commands/base-command.js index fa9c11658ed..50411e19ba5 100644 --- a/src/commands/base-command.js +++ b/src/commands/base-command.js @@ -303,12 +303,14 @@ class BaseCommand extends Command { debug(`${this.name()}:onEnd`)(`Status: ${status}`) debug(`${this.name()}:onEnd`)(`Duration: ${duration}ms`) - await track('command', { - ...payload, - command: this.name(), - duration, - status, - }) + try { + await track('command', { + ...payload, + command: this.name(), + duration, + status, + }) + } catch {} if (error_ !== undefined) { error(error_ instanceof Error ? error_ : format(error_), { exit: false }) diff --git a/src/lib/exec-fetcher.js b/src/lib/exec-fetcher.js index 0505155a3af..bd4eaf21008 100644 --- a/src/lib/exec-fetcher.js +++ b/src/lib/exec-fetcher.js @@ -4,10 +4,11 @@ const process = require('process') const { fetchLatest, fetchVersion, newerVersion, updateAvailable } = require('gh-release-fetch') const isExe = require('isexe') +const terminalLink = require('terminal-link') // cannot directly import from ../utils as it would create a circular dependency. // the file `src/utils/live-tunnel.js` depends on this file -const { NETLIFYDEVWARN, log } = require('../utils/command-helpers') +const { NETLIFYDEVWARN, chalk, error, log } = require('../utils/command-helpers') const execa = require('../utils/execa') const isWindows = () => process.platform === 'win32' @@ -62,27 +63,80 @@ const shouldFetchLatestVersion = async ({ binPath, execArgs, execName, latestVer latestVersion, }) return outdated - } catch (error) { + } catch (error_) { if (exists) { log(NETLIFYDEVWARN, `failed checking for new version of '${packageName}'. Using existing version`) return false } - throw error + throw error_ } } +const getArch = () => { + switch (process.arch) { + case 'x64': + return 'amd64' + case 'ia32': + return '386' + default: + return process.arch + } +} + +/** + * Tries to get the latest release from the github releases to download the binary. + * Is throwing an error if there is no binary that matches the system os or arch + * @param {object} config + * @param {string} config.destination + * @param {string} config.execName + * @param {string} config.destination + * @param {string} config.extension + * @param {string} config.packageName + * @param {string} [config.latestVersion ] + */ const fetchLatestVersion = async ({ destination, execName, extension, latestVersion, packageName }) => { const win = isWindows() + const arch = getArch() const platform = win ? 'windows' : process.platform + const pkgName = `${execName}-${platform}-${arch}.${extension}` + const release = { repository: getRepository({ packageName }), - package: `${execName}-${platform}-amd64.${extension}`, + package: pkgName, destination, extract: true, } const options = getOptions() - await (latestVersion ? fetchVersion({ ...release, version: latestVersion }, options) : fetchLatest(release, options)) + const fetch = latestVersion + ? fetchVersion({ ...release, version: latestVersion }, options) + : fetchLatest(release, options) + + try { + await fetch + } catch (error_) { + if (typeof error_ === 'object' && 'statusCode' in error_ && error_.statusCode === 404) { + const createIssueLink = new URL('https://github.com/netlify/cli/issues/new') + createIssueLink.searchParams.set('assignees', '') + createIssueLink.searchParams.set('labels', 'type: bug') + createIssueLink.searchParams.set('template', 'bug_report.md') + createIssueLink.searchParams.set( + 'title', + `${execName} is not supported on ${platform} with CPU architecture ${arch}`, + ) + + const issueLink = terminalLink('Create a new CLI issue', createIssueLink.href) + + error(`The operating system ${chalk.cyan(platform)} with the CPU architecture ${chalk.cyan( + arch, + )} is currently not supported! + +Please open up an issue on our CLI repository so that we can support it: +${issueLink}`) + } + + error(error_) + } } -module.exports = { getExecName, shouldFetchLatestVersion, fetchLatestVersion } +module.exports = { getArch, getExecName, shouldFetchLatestVersion, fetchLatestVersion } diff --git a/src/lib/exec-fetcher.test.js b/src/lib/exec-fetcher.test.js index 7c85fcb22cb..ad1d4c2e1ac 100644 --- a/src/lib/exec-fetcher.test.js +++ b/src/lib/exec-fetcher.test.js @@ -1,71 +1,113 @@ // @ts-check -const { stat } = require('fs').promises -const path = require('path') -const process = require('process') +/** @type {import('ava').TestInterface} */ +// @ts-ignore const test = require('ava') -const tempDirectory = require('temp-dir') -const { v4: uuid } = require('uuid') +const proxyquire = require('proxyquire') +const sinon = require('sinon') -const { fetchLatestVersion, getExecName, shouldFetchLatestVersion } = require('./exec-fetcher') -const { rmdirRecursiveAsync } = require('./fs') +// is not a function therefore use Object.defineProperty to mock it +const processSpy = {} +const fetchLatestSpy = sinon.stub() -test.beforeEach((t) => { - const directory = path.join(tempDirectory, `netlify-cli-exec-fetcher`, uuid()) - t.context.binPath = directory +const { fetchLatestVersion, getArch, getExecName } = proxyquire('./exec-fetcher', { + 'gh-release-fetch': { + fetchLatest: fetchLatestSpy, + }, + process: processSpy, }) -test.afterEach(async (t) => { - await rmdirRecursiveAsync(t.context.binPath) +test(`should use 386 if process architecture is ia32`, (t) => { + Object.defineProperty(processSpy, 'arch', { value: 'ia32' }) + t.is(getArch(), '386') }) -test(`should postix exec with .exe on windows`, (t) => { +test(`should use amd64 if process architecture is x64`, (t) => { + Object.defineProperty(processSpy, 'arch', { value: 'x64' }) + t.is(getArch(), 'amd64') +}) + +test(`should append .exe on windows for the executable name`, (t) => { + Object.defineProperty(processSpy, 'platform', { value: 'win32' }) const execName = 'some-binary-file' - if (process.platform === 'win32') { - t.is(getExecName({ execName }), `${execName}.exe`) - } else { - t.is(getExecName({ execName }), execName) - } + t.is(getExecName({ execName }), `${execName}.exe`) }) -const packages = [ - // Disabled since failing on CI due to GitHub API limits when fetching releases - // TODO: Re-enabled when we can think of a solution - // { - // packageName: 'traffic-mesh-agent', - // execName: 'traffic-mesh', - // execArgs: ['--version'], - // pattern: '\\sv(.+)', - // extension: 'zip', - // }, -] - -packages.forEach(({ execArgs, execName, extension, packageName, pattern }) => { - test(`${packageName} - should return true on empty directory`, async (t) => { - const { binPath } = t.context - const actual = await shouldFetchLatestVersion({ binPath, packageName, execName, execArgs, pattern }) - t.is(actual, true) +test(`should not append anything on linux or darwin to executable`, (t) => { + Object.defineProperty(processSpy, 'platform', { value: 'darwin' }) + const execName = 'some-binary-file' + t.is(getExecName({ execName }), execName) + Object.defineProperty(processSpy, 'platform', { value: 'linux' }) + t.is(getExecName({ execName }), execName) +}) + +test('should test if an error is thrown if the cpu architecture and the os are not available', async (t) => { + Object.defineProperties(processSpy, { + platform: { value: 'windows' }, + arch: { value: 'amd64' }, }) - test(`${packageName} - should return false after latest version is fetched`, async (t) => { - const { binPath } = t.context + // eslint-disable-next-line prefer-promise-reject-errors + fetchLatestSpy.returns(Promise.reject({ statusCode: 404 })) - await fetchLatestVersion({ packageName, execName, destination: binPath, extension }) + const { message } = await t.throwsAsync( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: t.context.binPath, + extension: 'zip', + }), + ) - const actual = await shouldFetchLatestVersion({ binPath, packageName, execName, execArgs, pattern }) - t.is(actual, false) - }) + t.regex(message, /The operating system windows with the CPU architecture amd64 is currently not supported!/) +}) - test(`${packageName} - should download latest version on empty directory`, async (t) => { - const { binPath } = t.context +test('should provide the error if it is not a 404', async (t) => { + const error = new Error('Got Rate limited for example') - await fetchLatestVersion({ packageName, execName, destination: binPath, extension }) + fetchLatestSpy.returns(Promise.reject(error)) - const execPath = path.join(binPath, getExecName({ execName })) - const stats = await stat(execPath) - t.is(stats.size >= FILE_MIN_SIZE, true) + const { message } = await t.throwsAsync( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: t.context.binPath, + extension: 'zip', + }), + ) + + t.is(message, error.message) +}) + +test('should map linux x64 to amd64 arch', async (t) => { + Object.defineProperties(processSpy, { + platform: { value: 'linux' }, + arch: { value: 'x64' }, }) + // eslint-disable-next-line prefer-promise-reject-errors + fetchLatestSpy.returns(Promise.reject({ statusCode: 404 })) + + const { message } = await t.throwsAsync( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: t.context.binPath, + extension: 'zip', + }), + ) + + t.regex(message, /The operating system linux with the CPU architecture amd64 is currently not supported!/) }) -// 5 KiB -const FILE_MIN_SIZE = 5e3 +test('should not throw when the request passes', async (t) => { + fetchLatestSpy.returns(Promise.resolve()) + + await t.notThrowsAsync( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: t.context.binPath, + extension: 'zip', + }), + ) +}) diff --git a/src/utils/command-helpers.js b/src/utils/command-helpers.js index e6da2337aad..0afcb7b8b87 100644 --- a/src/utils/command-helpers.js +++ b/src/utils/command-helpers.js @@ -179,7 +179,7 @@ const error = (message = '', options = {}) => { if (process.env.DEBUG) { process.stderr.write(` ${bang} Warning: ${err.stack.split('\n').join(`\n ${bang} `)}`) } else { - process.stderr.write(` ${bang} ${err.name}: ${err.message}\n`) + process.stderr.write(` ${bang} ${chalk.red(`${err.name}:`)} ${err.message}\n`) } } else { throw err