Merge pull request #2717 from balena-io/1.5.20

1.5.20
This commit is contained in:
Alexis Svinartchouk 2019-04-01 18:00:27 +02:00 committed by GitHub
commit 58de7375a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 249 additions and 188 deletions

View File

@ -289,7 +289,7 @@ rules:
- error - error
- anonymous: always - anonymous: always
named: always named: always
asyncArrow: never asyncArrow: always
template-tag-spacing: template-tag-spacing:
- error - error
- always - always

View File

@ -119,7 +119,8 @@ exports.setProgressState = (state) => {
const data = _.assign({}, state, { const data = _.assign({}, state, {
percentage: _.isFinite(state.percentage) percentage: _.isFinite(state.percentage)
? Math.floor(state.percentage) ? Math.floor(state.percentage)
: state.percentage, // eslint-disable-next-line no-undefined
: undefined,
speed: _.attempt(() => { speed: _.attempt(() => {
if (_.isFinite(state.speed)) { if (_.isFinite(state.speed)) {

View File

@ -0,0 +1,122 @@
/*
* Copyright 2019 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
const cp = require('child_process')
const _ = require('lodash')
const os = require('os')
/**
* @summary Promisified child_process.execFile
* @function
*
* @param {String} file - command
* @param {String[]} args - arguments
* @param {Object} options - child_process.execFile options
*
* @returns {Promise<Object>} - { stdout, stderr }
*
* @example
* execFileAsync('ls', [ '.' ])
* .then(console.log);
*/
const execFileAsync = async (file, args, options) => {
return new Promise((resolve, reject) => {
cp.execFile(
file,
args,
options,
(error, stdout, stderr) => {
if (error) {
reject(error)
} else {
resolve({ stdout, stderr })
}
}
)
})
}
/**
* @summary returns a Map of drive letter -> network locations on Windows
* @function
*
* @returns {Promise<Map<String, String>>} - 'Z:' -> '\\\\192.168.0.1\\Public'
*
* @example
* getWindowsNetworkDrives()
* .then(console.log);
*/
const getWindowsNetworkDrives = async () => {
const result = await execFileAsync(
'wmic',
[ 'path', 'Win32_LogicalDisk', 'Where', 'DriveType="4"', 'get', 'DeviceID,ProviderName' ],
{ windowsHide: true, windowsVerbatimArguments: true }
)
const couples = _.chain(result.stdout)
.split('\n')
// Remove header line
// eslint-disable-next-line no-magic-numbers
.slice(1)
// Remove extra spaces / tabs / carriage returns
.invokeMap(String.prototype.trim)
// Filter out empty lines
.compact()
.map((str) => {
const colonPosition = str.indexOf(':')
// eslint-disable-next-line no-magic-numbers
if (colonPosition === -1) {
throw new Error(`Can't parse wmic output: ${result.stdout}`)
}
// eslint-disable-next-line no-magic-numbers
return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ]
})
.value()
return new Map(couples)
}
/**
* @summary Replaces network drive letter with network drive location in the provided filePath on Windows
* @function
*
* @param {String} filePath - file path
*
* @returns {String} - updated file path
*
* @example
* replaceWindowsNetworkDriveLetter('Z:\\some-file')
* .then(console.log);
*/
exports.replaceWindowsNetworkDriveLetter = async (filePath) => {
let result = filePath
if (os.platform() === 'win32') {
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath)
if (matches !== null) {
const [ , drive, relativePath ] = matches
const drives = await getWindowsNetworkDrives()
const location = drives.get(drive)
// eslint-disable-next-line no-undefined
if (location !== undefined) {
result = `${location}\\${relativePath}`
}
}
}
return result
}

View File

@ -31,7 +31,6 @@ const availableDrives = require('../../../models/available-drives')
const selection = require('../../../models/selection-state') const selection = require('../../../models/selection-state')
module.exports = function ( module.exports = function (
$q,
$state, $state,
$timeout, $timeout,
FlashErrorModalService, FlashErrorModalService,
@ -67,39 +66,34 @@ module.exports = function (
* *
* @param {Array<Object>} drives - list of drive objects * @param {Array<Object>} drives - list of drive objects
* @param {Object} image - image object * @param {Object} image - image object
* @returns {Promise} * @returns {Promise<Boolean>}
* *
* @example * @example
* displayTailoredWarning(drives, image).then(() => { * displayTailoredWarning(drives, image).then((ok) => {
* console.log('Continue pressed') * if (ok) {
* }).catch(() => { * console.log('No warning was shown or continue was pressed')
* console.log('Change pressed') * } else {
* console.log('Change was pressed')
* }
* }) * })
*/ */
const displayTailoredWarning = (drives, image) => { const displayTailoredWarning = async (drives, image) => {
const warningMessages = _.reduce(drives, (accumMessages, drive) => { const warningMessages = []
for (const drive of drives) {
if (constraints.isDriveSizeLarge(drive)) { if (constraints.isDriveSizeLarge(drive)) {
return accumMessages.concat(messages.warning.largeDriveSize(drive)) warningMessages.push(messages.warning.largeDriveSize(drive))
} else if (!constraints.isDriveSizeRecommended(drive, image)) { } else if (!constraints.isDriveSizeRecommended(drive, image)) {
return accumMessages.concat(messages.warning.unrecommendedDriveSize(image, drive)) warningMessages.push(messages.warning.unrecommendedDriveSize(image, drive))
} }
return accumMessages // TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
}, []) }
if (!warningMessages.length) { if (!warningMessages.length) {
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode return true
return $q.resolve()
} }
return confirmationWarningModal(warningMessages).then((value) => { return confirmationWarningModal(warningMessages)
if (!value) {
DriveSelectorService.open()
return $q.reject()
}
return $q.resolve()
})
} }
/** /**
@ -118,35 +112,41 @@ module.exports = function (
* '/dev/disk5' * '/dev/disk5'
* ]) * ])
*/ */
this.flashImageToDrive = () => { this.flashImageToDrive = async () => {
const image = selection.getImage()
const devices = selection.getSelectedDevices() const devices = selection.getSelectedDevices()
const image = selection.getImage()
const drives = _.filter(availableDrives.getDrives(), (drive) => {
return _.includes(devices, drive.device)
})
// eslint-disable-next-line no-magic-numbers
if (drives.length === 0) {
return
}
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image)
if (hasDangerStatus) {
if (!(await displayTailoredWarning(drives, image))) {
DriveSelectorService.open()
return
}
}
if (flashState.isFlashing()) { if (flashState.isFlashing()) {
return return
} }
const drives = _.filter(availableDrives.getDrives(), (drive) => {
return _.includes(devices, drive.device)
})
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image)
const userConfirm = hasDangerStatus ? _.partial(displayTailoredWarning, drives, image) : $q.resolve
// Trigger Angular digests along with store updates, as the flash state // Trigger Angular digests along with store updates, as the flash state
// updates. Without this there is essentially no progress to watch. // updates. Without this there is essentially no progress to watch.
const unsubscribe = store.observe($timeout) const unsubscribe = store.observe($timeout)
const iconPath = '../../../assets/icon.png'
userConfirm().then(() => {
// Stop scanning drives when flashing // Stop scanning drives when flashing
// otherwise Windows throws EPERM // otherwise Windows throws EPERM
driveScanner.stop() driveScanner.stop()
return imageWriter.flash(image.path, drives) const iconPath = '../../../assets/icon.png'
}).then(() => { try {
await imageWriter.flash(image.path, drives)
if (!flashState.wasLastFlashCancelled()) { if (!flashState.wasLastFlashCancelled()) {
const flashResults = flashState.getFlashResults() const flashResults = flashState.getFlashResults()
notification.send('Flash complete!', { notification.send('Flash complete!', {
@ -155,7 +155,7 @@ module.exports = function (
}) })
$state.go('success') $state.go('success')
} }
}).catch((error) => { } catch (error) {
// When flashing is cancelled before starting above there is no error // When flashing is cancelled before starting above there is no error
if (!error) { if (!error) {
return return
@ -183,11 +183,11 @@ module.exports = function (
FlashErrorModalService.show(messages.error.genericFlashError()) FlashErrorModalService.show(messages.error.genericFlashError())
exceptionReporter.report(error) exceptionReporter.report(error)
} }
}).finally(() => { } finally {
availableDrives.setDrives([]) availableDrives.setDrives([])
driveScanner.start() driveScanner.start()
unsubscribe() unsubscribe()
}) }
} }
/** /**

View File

@ -29,6 +29,7 @@ const analytics = require('../../../modules/analytics')
const settings = require('../../../models/settings') const settings = require('../../../models/settings')
const selectionState = require('../../../models/selection-state') const selectionState = require('../../../models/selection-state')
const osDialog = require('../../../os/dialog') const osDialog = require('../../../os/dialog')
const { replaceWindowsNetworkDriveLetter } = require('../../../os/windows-network-drives')
const exceptionReporter = require('../../../modules/exception-reporter') const exceptionReporter = require('../../../modules/exception-reporter')
module.exports = function ( module.exports = function (
@ -145,7 +146,13 @@ module.exports = function (
* @example * @example
* ImageSelectionController.selectImageByPath('path/to/image.img'); * ImageSelectionController.selectImageByPath('path/to/image.img');
*/ */
this.selectImageByPath = (imagePath) => { this.selectImageByPath = async (imagePath) => {
try {
// eslint-disable-next-line no-param-reassign
imagePath = await replaceWindowsNetworkDriveLetter(imagePath)
} catch (error) {
analytics.logException(error)
}
if (!supportedFormats.isSupportedImage(imagePath)) { if (!supportedFormats.isSupportedImage(imagePath)) {
const invalidImageError = errors.createUserError({ const invalidImageError = errors.createUserError({
title: 'Invalid image', title: 'Invalid image',
@ -158,12 +165,10 @@ module.exports = function (
} }
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read) const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
source.getInnerSource() try {
.then((innerSource) => { const innerSource = await source.getInnerSource()
return innerSource.getMetadata() const metadata = await innerSource.getMetadata()
.then((metadata) => { const partitionTable = await innerSource.getPartitionTable()
return innerSource.getPartitionTable()
.then((partitionTable) => {
if (partitionTable) { if (partitionTable) {
metadata.hasMBR = true metadata.hasMBR = true
metadata.partitions = partitionTable.partitions metadata.partitions = partitionTable.partitions
@ -173,20 +178,20 @@ module.exports = function (
metadata.extension = path.extname(imagePath).slice(1) metadata.extension = path.extname(imagePath).slice(1)
this.selectImage(metadata) this.selectImage(metadata)
$timeout() $timeout()
}) } catch (error) {
})
})
.catch((error) => {
const imageError = errors.createUserError({ const imageError = errors.createUserError({
title: 'Error opening image', title: 'Error opening image',
description: messages.error.openImage(path.basename(imagePath), error.message) description: messages.error.openImage(path.basename(imagePath), error.message)
}) })
osDialog.showError(imageError) osDialog.showError(imageError)
analytics.logException(error) analytics.logException(error)
}) } finally {
.finally(() => { try {
return source.close().catch(_.noop) await source.close()
}) } catch (error) {
// Noop
}
}
} }
/** /**

BIN
tests/data/wmic-output.txt Executable file

Binary file not shown.

View File

@ -126,23 +126,6 @@ describe('Model: flashState', function () {
}).to.not.throw('Missing flash fields: percentage') }).to.not.throw('Missing flash fields: percentage')
}) })
it('should throw if percentage is not a number', function () {
flashState.setFlashingFlag()
m.chai.expect(function () {
flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0,
type: 'write',
percentage: '50',
eta: 15,
speed: 100000000000,
totalSpeed: 200000000000
})
}).to.throw('Invalid state percentage: 50')
})
it('should throw if percentage is outside maximum bound', function () { it('should throw if percentage is outside maximum bound', function () {
flashState.setFlashingFlag() flashState.setFlashingFlag()
m.chai.expect(function () { m.chai.expect(function () {

View File

@ -0,0 +1,47 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
const { readFile } = require('fs')
const os = require('os')
const cp = require('child_process')
const m = require('mochainon')
const { promisify } = require('util')
const { replaceWindowsNetworkDriveLetter } = require('../../../lib/gui/app/os/windows-network-drives')
const readFileAsync = promisify(readFile)
describe('Network drives on Windows', () => {
before(async () => {
this.osPlatformStub = m.sinon.stub(os, 'platform')
this.osPlatformStub.returns('win32')
const wmicOutput = await readFileAsync('tests/data/wmic-output.txt', { encoding: 'ucs2' })
this.execFileStub = m.sinon.stub(cp, 'execFile')
this.execFileStub.callsArgWith(3, null, wmicOutput)
})
it('should parse network drive mapping on Windows', async () => {
m.chai.expect(await replaceWindowsNetworkDriveLetter('Z:\\some-folder\\some-file'))
.to.equal('\\\\192.168.1.1\\Public\\some-folder\\some-file')
})
after(() => {
this.osPlatformStub.restore()
this.execFileStub.restore()
})
})

View File

@ -1,97 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
module.exports = {
subjectParser: 'angular',
getGitReferenceFromVersion: 'v-prefix',
editChangelog: true,
editVersion: true,
addEntryToChangelog: {
preset: 'prepend',
fromLine: 5
},
includeCommitWhen: 'has-changelog-entry',
getIncrementLevelFromCommit: (commit) => {
if (/none/i.test(commit.footer['Change-type'])) {
return null
}
return commit.footer['Change-type'] &&
commit.footer['Change-type'].toLowerCase()
},
transformTemplateData: (data) => {
if (data.commits.length === 0) {
throw new Error('No commits annotated with Changelog-entry')
}
data.features = data.commits.filter((commit) => {
return commit.subject.type === 'feat'
})
data.fixes = data.commits.filter((commit) => {
return commit.subject.type === 'fix'
})
data.misc = data.commits.filter((commit) => {
return !([ 'fix', 'feat' ].includes(commit.subject.type))
})
return data
},
template: [
'## v{{version}} - {{moment date "Y-MM-DD"}}',
'{{#if features.length}}',
'',
'### Features',
'',
'{{#each features}}',
'{{#with footer}}',
'- {{capitalize Changelog-entry}}',
'{{/with}}',
'{{/each}}',
'{{/if}}',
'{{#if fixes.length}}',
'',
'### Fixes',
'',
'{{#each fixes}}',
'{{#with footer}}',
'- {{capitalize Changelog-entry}}',
'{{/with}}',
'{{/each}}',
'{{/if}}',
'{{#if misc.length}}',
'',
'### Misc',
'',
'{{#each misc}}',
'{{#with footer}}',
'- {{capitalize Changelog-entry}}',
'{{/with}}',
'{{/each}}',
'{{/if}}'
].join('\n')
}