-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature/sc-126217/implement-update-cli-command-in-js #729
Merged
hugomontero
merged 15 commits into
feature/cli-installer-v2
from
feature/sc-126217/implement-update-cli-command-in-js
Apr 22, 2024
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
acb9b24
update settings for enable/disable auto updates
hugomontero b0f00b5
add version param for update-cli command
hugomontero 591580f
add update cli process to use manifest in order to download the lates…
hugomontero 8e3385e
change update check interval
hugomontero c200ab4
change update-check to call update-cli
hugomontero 0ed0e23
Update error messages for better user understanding
hugomontero f91d1ca
add platform mapping for win case
hugomontero cc94849
pick the installation path depending on the platform
hugomontero b37e234
fix comment regarding time for update check interval
hugomontero d85e48a
update s3 upload cache control to ensure be cached
hugomontero bf05dd4
add detached and cleanup to options in order to make independant the …
hugomontero 2f2c7b0
use process.pkg to verify if is being packaged or not
hugomontero a43da1b
fix: on linux/darwin the APPLOCAL is undefined and cannot be part of …
hugomontero f8c1e1c
add manifestHost to settings
hugomontero 7089080
add sha validation for downloaded file
hugomontero File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,78 +1,28 @@ | ||
const chalk = require('chalk'); | ||
const semver = require('semver'); | ||
const latestVersion = require('latest-version'); | ||
const settings = require('../../settings'); | ||
const pkg = require('../../package'); | ||
const ui = require('./ui'); | ||
|
||
const execa = require('execa'); | ||
|
||
module.exports = async (skip, force) => { | ||
const { displayVersionBanner } = module.exports.__internal__; | ||
|
||
if (skip) { | ||
return; | ||
} | ||
|
||
const now = Date.now(); | ||
const lastCheck = settings.profile_json.last_version_check || 0; | ||
const skipUpdates = !settings.profile_json.enableUpdates || settings.disableUpdateCheck; | ||
|
||
if ((now - lastCheck >= settings.updateCheckInterval) || force){ | ||
settings.profile_json.last_version_check = now; | ||
|
||
try { | ||
const version = await getPublishedVersion(pkg, settings); | ||
|
||
if (semver.gt(version, pkg.version)){ | ||
settings.profile_json.newer_version = version; | ||
} else { | ||
delete settings.profile_json.newer_version; | ||
} | ||
|
||
settings.saveProfileData(); | ||
|
||
if (settings.profile_json.newer_version){ | ||
displayVersionBanner(settings.profile_json.newer_version); | ||
} | ||
} catch (error){ | ||
settings.saveProfileData(); | ||
if (skipUpdates) { | ||
return; | ||
} | ||
return; | ||
execa('particle', ['update-cli'], { cleanup: false, detached: true }); | ||
} | ||
}; | ||
|
||
async function getPublishedVersion(pkgJSON, settings){ | ||
const { latestVersion } = module.exports.__internal__; | ||
|
||
try { | ||
const promise = withTimeout(latestVersion(pkgJSON.name), settings.updateCheckTimeout); | ||
return await ui.spin(promise, 'Checking for updates...'); | ||
} catch (error){ | ||
return pkgJSON.version; | ||
} | ||
} | ||
|
||
function displayVersionBanner(version){ | ||
console.error('particle-cli v' + pkg.version); | ||
console.error(); | ||
console.error(chalk.yellow('!'), 'A newer version (' + chalk.cyan(version) + ') of', chalk.bold.white('particle-cli'), 'is available.'); | ||
console.error(chalk.yellow('!'), 'Upgrade now by running:', chalk.bold.white('particle update-cli')); | ||
console.error(); | ||
} | ||
|
||
function withTimeout(promise, ms){ | ||
const timer = delay(ms).then(() => { | ||
throw new Error('The operation timed out'); | ||
}); | ||
return Promise.race([promise, timer]); | ||
} | ||
|
||
function delay(ms){ | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
|
||
|
||
module.exports.__internal__ = { | ||
latestVersion, | ||
displayVersionBanner | ||
latestVersion | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,15 @@ | ||
const os = require('os'); | ||
const path = require('path'); | ||
const fs = require('fs-extra'); | ||
const pkg = require('../../package'); | ||
const semver = require('semver'); | ||
const log = require('../lib/log'); | ||
const chalk = require('chalk'); | ||
const settings = require('../../settings'); | ||
const request = require('request'); | ||
const zlib = require('zlib'); | ||
const Spinner = require('cli-spinner').Spinner; | ||
const crypto = require('crypto'); | ||
|
||
/* | ||
* The update-cli command tells the CLI installer to reinstall the latest version of the CLI | ||
|
@@ -8,16 +18,186 @@ const chalk = require('chalk'); | |
* If the CLI was installed using npm, tell the user to update using npm | ||
*/ | ||
class UpdateCliCommand { | ||
update({ 'enable-updates': enableUpdates, 'disable-updates': disableUpdates }) { | ||
update({ 'enable-updates': enableUpdates, 'disable-updates': disableUpdates, version }) { | ||
if (enableUpdates) { | ||
log.info('Automatic update checks are now enabled'); | ||
return; | ||
return this.enableUpdates(); | ||
} | ||
if (disableUpdates) { | ||
log.info('Automatic update checks are now disabled'); | ||
return this.disableUpdates(); | ||
} | ||
if (!process.pkg) { | ||
log.info(`Update the CLI by running ${chalk.bold('npm install -g particle-cli')}`); | ||
log.info('To stay up to date with the latest features and improvements, please install the latest Particle Installer executable from our website: https://www.particle.io/cli'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good message! |
||
return; | ||
} | ||
return this.updateCli(version); | ||
} | ||
|
||
async enableUpdates() { | ||
// set the update flag to true | ||
settings.profile_json.enableUpdates = true; | ||
settings.saveProfileData(); | ||
log.info('Automatic update checks are now enabled'); | ||
} | ||
async disableUpdates() { | ||
// set the update flag to false | ||
settings.profile_json.enableUpdates = false; | ||
settings.saveProfileData(); | ||
log.info('Automatic update checks are now disabled'); | ||
} | ||
|
||
async updateCli(version) { | ||
log.info(`Updating the CLI to ${version ? version : 'latest'}`); | ||
const spinner = new Spinner('Updating CLI...'); | ||
spinner.start(); | ||
// download manifest | ||
const manifest = await this.downloadManifest(version); | ||
const upToDate = semver.gte(pkg.version, manifest.version) && !version; | ||
if (upToDate) { | ||
spinner.stop(true); | ||
log.info('CLI is already up to date'); | ||
return; | ||
} | ||
log.info(`Update the CLI by running ${chalk.bold('npm install -g particle-cli')}`); | ||
const cliPath = await this.downloadCLI(manifest); | ||
await this.replaceCLI(cliPath); | ||
spinner.stop(true); | ||
await this.configureProfileSettings(version); | ||
log.info('CLI updated successfully'); | ||
} | ||
|
||
async downloadManifest(version) { | ||
const fileName = version ? `manifest-${version}.json` : 'manifest.json'; | ||
const url = `https://${settings.manifestHost}/particle-cli/${fileName}`; | ||
return new Promise((resolve, reject ) => { | ||
return request(url, (error, response, body) => { | ||
if (error) { | ||
return this.logAndReject(error, reject, version); | ||
} | ||
if (response.statusCode !== 200) { | ||
return this.logAndReject(`Failed to download manifest: Status Code ${response.statusCode}`, reject, version); | ||
} | ||
try { | ||
resolve(JSON.parse(body)); | ||
} catch (error) { | ||
this.logAndReject(error, reject, version); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
logAndReject(error, reject, version) { | ||
const baseMessage = 'We were unable to check for updates'; | ||
const message = version ? `${baseMessage}: Version ${version} not found` : `${baseMessage} Please try again later`; | ||
log.error(error); | ||
reject(message); | ||
} | ||
|
||
async downloadCLI(manifest) { | ||
try { | ||
const { url, sha256: expectedHash } = this.getBuildDetailsFromManifest(manifest); | ||
const fileName = url.split('/').pop(); | ||
const fileNameWithoutLastExtension = path.basename(fileName, path.extname(fileName)); | ||
const filePath = path.join(os.tmpdir(), fileNameWithoutLastExtension); | ||
const tempFilePath = `${filePath}.gz`; | ||
|
||
const output = fs.createWriteStream(tempFilePath); | ||
|
||
return await new Promise((resolve, reject) => { | ||
request(url) | ||
.on('response', (response) => { | ||
if (response.statusCode !== 200) { | ||
log.debug(`Failed to download CLI: Status Code ${response.statusCode}`); | ||
return reject(new Error('No file found to download')); | ||
} | ||
}) | ||
.pipe(output) | ||
.on('finish', async () => { | ||
const fileHash = await this.getFileHash(tempFilePath); | ||
if (fileHash === expectedHash) { | ||
const unzipPath = await this.unzipFile(tempFilePath, filePath); | ||
resolve(unzipPath); | ||
} else { | ||
reject(new Error('Hash mismatch')); | ||
} | ||
}) | ||
.on('error', (error) => { | ||
reject(error); | ||
}); | ||
}); | ||
} catch (error) { | ||
log.debug(`Failed during download or verification: ${error}`); | ||
throw new Error('Failed to download or verify the CLI, please try again later'); | ||
} | ||
} | ||
|
||
async getFileHash(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const hash = crypto.createHash('sha256'); | ||
const stream = fs.createReadStream(filePath); | ||
stream.on('data', (data) => hash.update(data)); | ||
stream.on('end', () => resolve(hash.digest('hex'))); | ||
stream.on('error', (error) => reject(error)); | ||
}); | ||
} | ||
|
||
async unzipFile(sourcePath, targetPath) { | ||
return new Promise((resolve, reject) => { | ||
const gunzip = zlib.createGunzip(); | ||
const source = fs.createReadStream(sourcePath); | ||
const destination = fs.createWriteStream(targetPath); | ||
source | ||
.pipe(gunzip) | ||
.pipe(destination) | ||
.on('finish', () => resolve(targetPath)) | ||
.on('error', (error) => reject(error)); | ||
}); | ||
} | ||
|
||
getBuildDetailsFromManifest(manifest) { | ||
const platformMapping = { | ||
darwin: 'darwin', | ||
linux: 'linux', | ||
win32: 'win' | ||
}; | ||
const archMapping = { | ||
x64: 'amd64', | ||
arm64: 'arm64' | ||
}; | ||
const platform = os.platform(); | ||
const arch = os.arch(); | ||
const platformKey = platformMapping[platform] || platform; | ||
const archKey = archMapping[arch] || arch; | ||
const platformManifest = manifest.builds && manifest.builds[platformKey]; | ||
const archManifest = platformManifest && platformManifest[archKey]; | ||
if (!archManifest) { | ||
throw new Error(`No CLI build found for ${platform} ${arch}`); | ||
} | ||
return archManifest; | ||
} | ||
|
||
async replaceCLI(newCliPath) { | ||
// rename the original CLI | ||
const binPath = this.getBinaryPath(); | ||
const fileName = os.platform() === 'win32' ? 'particle.exe' : 'particle'; | ||
const cliPath = path.join(binPath, fileName); | ||
const oldCliPath = path.join(binPath, `${fileName}.old`); | ||
await fs.move(cliPath, oldCliPath, { overwrite: true }); | ||
await fs.move(newCliPath, cliPath); | ||
await fs.chmod(cliPath, 0o755); // add execute permissions | ||
} | ||
|
||
getBinaryPath() { | ||
if (os.platform() === 'win32') { | ||
return path.join(process.env.LOCALAPPDATA, 'particle', 'bin'); | ||
} | ||
return path.join(os.homedir(), 'bin'); | ||
} | ||
async configureProfileSettings(version) { | ||
settings.profile_json.last_version_check = new Date().getTime(); | ||
settings.saveProfileData(); | ||
if (version) { | ||
await this.disableUpdates(); // disable updates since we are installing a specific version | ||
} | ||
} | ||
} | ||
|
||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone doesn't set
enableUpdates
it will skip updates. I think that's ok if we assume most people will install through the installer. npm install wouldn't set enableUpdates to true so that's fine. You may need to add a call toparticle update-cli --enable-updates
when installing from Workbench.How about going back to the PRD and putting a little table of which installation methods enable updates, which ones don't and which ones have an option to enable/disable updates.