diff --git a/build/actions/preload.js b/build/actions/preload.js index 33b18e0a..060d9480 100644 --- a/build/actions/preload.js +++ b/build/actions/preload.js @@ -127,8 +127,8 @@ offerToDisableAutomaticUpdates = function(application, commit) { module.exports = { signature: 'preload ', - description: '(beta) preload an app on a disk image', - help: 'Warning: "resin preload" requires Docker to be correctly installed in\nyour shell environment. For more information (including Windows support)\nplease check the README here: https://github.com/resin-io/resin-cli .\n\nUse this command to preload an application to a local disk image with a\nbuilt commit from Resin.io.\nThis can be used with cloud builds, or images deployed with resin deploy.\n\nExamples:\n $ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png\n $ resin preload resin.img', + description: '(beta) preload an app on a disk image (or Edison zip archive)', + help: 'Warning: "resin preload" requires Docker to be correctly installed in\nyour shell environment. For more information (including Windows support)\nplease check the README here: https://github.com/resin-io/resin-cli .\n\nUse this command to preload an application to a local disk image (or\nEdison zip archive) with a built commit from Resin.io.\nThis can be used with cloud builds, or images deployed with resin deploy.\n\nExamples:\n $ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png\n $ resin preload resin.img', permission: 'user', primary: true, options: dockerUtils.appendConnectionOptions([ @@ -158,7 +158,7 @@ module.exports = { } ]), action: function(params, options, done) { - var Promise, _, errors, expectedError, form, imageInfoSpinner, preload, resin, streamToPromise, visuals; + var Promise, _, errors, expectedError, form, nodeCleanup, preload, progressBars, progressHandler, resin, spinnerHandler, spinners, streamToPromise, visuals; _ = require('lodash'); Promise = require('bluebird'); resin = require('resin-sdk-preconfigured'); @@ -167,8 +167,33 @@ module.exports = { preload = require('resin-preload'); errors = require('resin-errors'); visuals = require('resin-cli-visuals'); + nodeCleanup = require('node-cleanup'); expectedError = require('../utils/patterns').expectedError; - imageInfoSpinner = new visuals.Spinner('Reading image device type and preloaded builds.'); + progressBars = {}; + progressHandler = function(event) { + var progressBar; + progressBar = progressBars[event.name]; + if (!progressBar) { + progressBar = progressBars[event.name] = new visuals.Progress(event.name); + } + return progressBar.update({ + percentage: event.percentage + }); + }; + spinners = {}; + spinnerHandler = function(event) { + var spinner; + spinner = spinners[event.name]; + if (!spinner) { + spinner = spinners[event.name] = new visuals.Spinner(event.name); + } + if (event.action === 'start') { + return spinner.start(); + } else { + console.log(); + return spinner.stop(); + } + }; options.image = params.image; options.appId = options.app; delete options.app; @@ -176,72 +201,79 @@ module.exports = { delete options['dont-detect-flasher-type-images']; options.splashImage = options['splash-image']; delete options['splash-image']; + if (options['dont-check-device-type'] && !options.appId) { + expectedError('You need to specify an app id if you disable the device type check.'); + } return dockerUtils.getDocker(options).then(function(docker) { - var buildOutputStream; - buildOutputStream = preload.build(docker); - if (process.env.DEBUG) { - buildOutputStream.pipe(process.stdout); - } - return streamToPromise(buildOutputStream).then(resin.settings.getAll).then(function(settings) { - options.proxy = settings.proxy; - options.apiHost = settings.apiUrl; - imageInfoSpinner.start(); - return preload.getDeviceTypeSlugAndPreloadedBuilds(docker, options)["catch"](preload.errors.ResinError, expectedError); - }).then(function(arg) { - var builds, slug; - slug = arg.slug, builds = arg.builds; - imageInfoSpinner.stop(); - return Promise["try"](function() { - if (options['dont-check-device-type'] && !options.appId) { - expectedError('You need to specify an app id if you disable the device type check.'); - } - if (options.appId) { - return preload.getApplication(resin, options.appId)["catch"](errors.ResinApplicationNotFound, expectedError); - } - return selectApplication(slug); - }).then(function(application) { - options.application = application; - if (slug !== application.device_type) { - expectedError("Image device type (" + application.device_type + ") and application device type (" + slug + ") do not match"); - } - return Promise["try"](function() { - if (options.commit) { - if (!_.find(application.build, { - commit_hash: options.commit - })) { - expectedError('There is no build matching this commit'); - } - return options.commit; - } - return selectApplicationCommit(application.build); - }).then(function(commit) { - if (commit === LATEST) { - options.commit = application.commit; - } else { - options.commit = commit; - } - return offerToDisableAutomaticUpdates(application, commit); + var gotSignal, preloader; + preloader = new preload.Preloader(resin, docker, options.appId, options.commit, options.image, options.splashImage, options.proxy, options.dontDetectFlasherTypeImages); + gotSignal = false; + nodeCleanup(function(exitCode, signal) { + if (signal) { + gotSignal = true; + nodeCleanup.uninstall(); + preloader.cleanup().then(function() { + return process.kill(process.pid, signal); }); - }).then(function() { - var ref; - builds = builds.map(function(build) { - return build.slice(-preload.BUILD_HASH_LENGTH); - }); - if (ref = options.commit, indexOf.call(builds, ref) >= 0) { - console.log('This build is already preloaded in this image.'); - process.exit(0); - } - return preload.run(resin, docker, options)["catch"](preload.errors.ResinError, expectedError); - }); + return false; + } }); - }).then(function(info) { - info.stdout.pipe(process.stdout); - info.stderr.pipe(process.stderr); - return info.statusCodePromise; - }).then(function(statusCode) { - if (statusCode !== 0) { - return process.exit(statusCode); + if (process.env.DEBUG) { + preloader.stderr.pipe(process.stderr); } - }).then(done); + preloader.on('progress', progressHandler); + preloader.on('spinner', spinnerHandler); + return new Promise(function(resolve, reject) { + preloader.on('error', reject); + return preloader.build().then(function() { + return preloader.prepare(); + }).then(function() { + return preloader.getDeviceTypeAndPreloadedBuilds(); + }).then(function(info) { + return Promise["try"](function() { + if (options.appId) { + return preloader.fetchApplication()["catch"](errors.ResinApplicationNotFound, expectedError); + } + return selectApplication(info.device_type); + }).then(function(application) { + preloader.setApplication(application); + if (info.device_type !== application.device_type) { + expectedError("Image device type (" + application.device_type + ") and application device type (" + slug + ") do not match"); + } + return Promise["try"](function() { + if (options.commit) { + if (!_.find(application.build, { + commit_hash: options.commit + })) { + expectedError('There is no build matching this commit'); + } + return options.commit; + } + return selectApplicationCommit(application.build); + }).then(function(commit) { + if (commit === LATEST) { + preloader.commit = application.commit; + } else { + preloader.commit = commit; + } + return offerToDisableAutomaticUpdates(application, commit); + }); + }).then(function() { + var builds, ref; + builds = info.preloaded_builds.map(function(build) { + return build.slice(-preload.BUILD_HASH_LENGTH); + }); + if (ref = preloader.commit, indexOf.call(builds, ref) >= 0) { + throw new preload.errors.ResinError('This build is already preloaded in this image.'); + } + return preloader.preload()["catch"](preload.errors.ResinError, expectedError); + }); + }).then(resolve)["catch"](reject); + }).then(done)["finally"](function() { + if (!gotSignal) { + return preloader.cleanup(); + } + }); + }); } }; diff --git a/lib/actions/preload.coffee b/lib/actions/preload.coffee index 803306cb..7dd91ec3 100644 --- a/lib/actions/preload.coffee +++ b/lib/actions/preload.coffee @@ -102,14 +102,14 @@ offerToDisableAutomaticUpdates = (application, commit) -> module.exports = signature: 'preload ' - description: '(beta) preload an app on a disk image' + description: '(beta) preload an app on a disk image (or Edison zip archive)' help: ''' Warning: "resin preload" requires Docker to be correctly installed in your shell environment. For more information (including Windows support) please check the README here: https://github.com/resin-io/resin-cli . - Use this command to preload an application to a local disk image with a - built commit from Resin.io. + Use this command to preload an application to a local disk image (or + Edison zip archive) with a built commit from Resin.io. This can be used with cloud builds, or images deployed with resin deploy. Examples: @@ -157,9 +157,28 @@ module.exports = preload = require('resin-preload') errors = require('resin-errors') visuals = require('resin-cli-visuals') + nodeCleanup = require('node-cleanup') { expectedError } = require('../utils/patterns') - imageInfoSpinner = new visuals.Spinner('Reading image device type and preloaded builds.') + progressBars = {} + + progressHandler = (event) -> + progressBar = progressBars[event.name] + if not progressBar + progressBar = progressBars[event.name] = new visuals.Progress(event.name) + progressBar.update(percentage: event.percentage) + + spinners = {} + + spinnerHandler = (event) -> + spinner = spinners[event.name] + if not spinner + spinner = spinners[event.name] = new visuals.Spinner(event.name) + if event.action == 'start' + spinner.start() + else + console.log() + spinner.stop() options.image = params.image options.appId = options.app @@ -171,79 +190,92 @@ module.exports = options.splashImage = options['splash-image'] delete options['splash-image'] + if options['dont-check-device-type'] and not options.appId + expectedError('You need to specify an app id if you disable the device type check.') + # Get a configured dockerode instance dockerUtils.getDocker(options) .then (docker) -> - # Build the preloader image - buildOutputStream = preload.build(docker) + preloader = new preload.Preloader( + resin, + docker, + options.appId, + options.commit, + options.image, + options.splashImage, + options.proxy, + options.dontDetectFlasherTypeImages + ) + + gotSignal = false + + nodeCleanup (exitCode, signal) -> + if signal + gotSignal = true + nodeCleanup.uninstall() # don't call cleanup handler again + preloader.cleanup() + .then -> + # calling process.exit() won't inform parent process of signal + process.kill(process.pid, signal) + return false if process.env.DEBUG - buildOutputStream.pipe(process.stdout) + preloader.stderr.pipe(process.stderr) - streamToPromise(buildOutputStream) + preloader.on('progress', progressHandler) + preloader.on('spinner', spinnerHandler) - # Get resin sdk settings so we can pass them to the preloader - .then(resin.settings.getAll) - .then (settings) -> - options.proxy = settings.proxy - options.apiHost = settings.apiUrl + return new Promise (resolve, reject) -> + preloader.on('error', reject) - # Use the preloader docker image to extract the deviceType of the image - imageInfoSpinner.start() - preload.getDeviceTypeSlugAndPreloadedBuilds(docker, options) - .catch(preload.errors.ResinError, expectedError) - .then ({ slug, builds }) -> - imageInfoSpinner.stop() - # Use the appId given as --app or show an interactive app selection menu - Promise.try -> - if options['dont-check-device-type'] and not options.appId - expectedError('You need to specify an app id if you disable the device type check.') - if options.appId - return preload.getApplication(resin, options.appId) - .catch(errors.ResinApplicationNotFound, expectedError) - selectApplication(slug) - .then (application) -> - options.application = application - - # Check that the app device type and the image device type match - if slug != application.device_type - expectedError( - "Image device type (#{application.device_type}) and application device type (#{slug}) do not match" - ) - - # Use the commit given as --commit or show an interactive commit selection menu - Promise.try -> - if options.commit - if not _.find(application.build, commit_hash: options.commit) - expectedError('There is no build matching this commit') - return options.commit - selectApplicationCommit(application.build) - .then (commit) -> - - # No commit specified => use the latest commit - if commit == LATEST - options.commit = application.commit - else - options.commit = commit - - # Propose to disable automatic app updates if the commit is not the latest - offerToDisableAutomaticUpdates(application, commit) + preloader.build() .then -> + preloader.prepare() + .then -> + preloader.getDeviceTypeAndPreloadedBuilds() + .then (info) -> + Promise.try -> + if options.appId + return preloader.fetchApplication() + .catch(errors.ResinApplicationNotFound, expectedError) + selectApplication(info.device_type) + .then (application) -> + preloader.setApplication(application) + # Check that the app device type and the image device type match + if info.device_type != application.device_type + expectedError( + "Image device type (#{application.device_type}) and application device type (#{slug}) do not match" + ) - builds = builds.map (build) -> - build.slice(-preload.BUILD_HASH_LENGTH) - if options.commit in builds - console.log('This build is already preloaded in this image.') - process.exit(0) - # All options are ready: preload the image. - preload.run(resin, docker, options) - .catch(preload.errors.ResinError, expectedError) - .then (info) -> - info.stdout.pipe(process.stdout) - info.stderr.pipe(process.stderr) - info.statusCodePromise - .then (statusCode) -> - if statusCode != 0 - process.exit(statusCode) - .then(done) + # Use the commit given as --commit or show an interactive commit selection menu + Promise.try -> + if options.commit + if not _.find(application.build, commit_hash: options.commit) + expectedError('There is no build matching this commit') + return options.commit + selectApplicationCommit(application.build) + .then (commit) -> + + # No commit specified => use the latest commit + if commit == LATEST + preloader.commit = application.commit + else + preloader.commit = commit + + # Propose to disable automatic app updates if the commit is not the latest + offerToDisableAutomaticUpdates(application, commit) + .then -> + builds = info.preloaded_builds.map (build) -> + build.slice(-preload.BUILD_HASH_LENGTH) + if preloader.commit in builds + throw new preload.errors.ResinError('This build is already preloaded in this image.') + # All options are ready: preload the image. + preloader.preload() + .catch(preload.errors.ResinError, expectedError) + .then(resolve) + .catch(reject) + .then(done) + .finally -> + if not gotSignal + preloader.cleanup() diff --git a/package.json b/package.json index 4ee04a64..05101766 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "mixpanel": "^0.4.0", "moment": "^2.12.0", "mz": "^2.6.0", + "node-cleanup": "^2.1.2", "nplugm": "^3.0.0", "president": "^2.0.1", "prettyjson": "^1.1.3", @@ -85,7 +86,7 @@ "resin-doodles": "0.0.1", "resin-image-fs": "^2.3.0", "resin-image-manager": "^4.1.1", - "resin-preload": "^3.1.4", + "resin-preload": "^4.0.2", "resin-sdk-preconfigured": "^6.9.0", "resin-settings-client": "^3.6.1", "resin-stream-logger": "^0.0.4", @@ -103,4 +104,4 @@ "optionalDependencies": { "removedrive": "^1.0.0" } -} \ No newline at end of file +}