Update lib/actions/local/flash.coffee

* switch to typescript
 * replace etcher-image-stream with etcher-sdk

Change-type: patch
This commit is contained in:
Alexis Svinartchouk 2018-12-11 17:33:26 +01:00
parent bc41ff0540
commit f9390ceb10
8 changed files with 206 additions and 122 deletions

View File

@ -1,120 +0,0 @@
###
Copyright 2017 Balena
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.
###
module.exports =
signature: 'local flash <image>'
description: 'Flash an image to a drive'
help: '''
Use this command to flash a balenaOS image to a drive.
Examples:
$ balena local flash path/to/balenaos.img
$ balena local flash path/to/balenaos.img --drive /dev/disk2
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
'''
options: [
signature: 'yes'
boolean: true
description: 'confirm non-interactively'
alias: 'y'
,
signature: 'drive'
parameter: 'drive'
description: 'drive'
alias: 'd'
]
root: true
action: (params, options, done) ->
_ = require('lodash')
os = require('os')
Promise = require('bluebird')
umountAsync = Promise.promisify(require('umount').umount)
fs = Promise.promisifyAll(require('fs'))
driveListAsync = Promise.promisify(require('drivelist').list)
chalk = require('chalk')
visuals = require('resin-cli-visuals')
form = require('resin-cli-form')
imageWrite = require('etcher-image-write')
form.run [
{
message: 'Select drive'
type: 'drive'
name: 'drive'
},
{
message: 'This will erase the selected drive. Are you sure?'
type: 'confirm'
name: 'yes'
default: false
}
],
override:
drive: options.drive
# If `options.yes` is `false`, pass `undefined`,
# otherwise the question will not be asked because
# `false` is a defined value.
yes: options.yes || undefined
# TODO: dedupe with the resin-device-operations
.then (answers) ->
if answers.yes isnt true
console.log(chalk.red.bold('Aborted image flash'))
process.exit(0)
driveListAsync().then (drives) ->
selectedDrive = _.find(drives, device: answers.drive)
if not selectedDrive?
throw new Error("Drive not found: #{answers.drive}")
return selectedDrive
.then (selectedDrive) ->
progressBars =
write: new visuals.Progress('Flashing')
check: new visuals.Progress('Validating')
umountAsync(selectedDrive.device).then ->
Promise.props
imageSize: fs.statAsync(params.image).get('size'),
imageStream: Promise.resolve(fs.createReadStream(params.image))
driveFileDescriptor: fs.openAsync(selectedDrive.raw, 'rs+')
.then (results) ->
imageWrite.write
fd: results.driveFileDescriptor
device: selectedDrive.raw
size: selectedDrive.size
,
stream: results.imageStream,
size: results.imageSize
,
check: true
.then (writer) ->
new Promise (resolve, reject) ->
writer.on 'progress', (state) ->
progressBars[state.type].update(state)
writer.on('error', reject)
writer.on('done', resolve)
.then ->
if (os.platform() is 'win32') and selectedDrive.mountpoint?
ejectAsync = Promise.promisify(require('removedrive').eject)
return ejectAsync(selectedDrive.mountpoint)
return umountAsync(selectedDrive.device)
.asCallback(done)

121
lib/actions/local/flash.ts Normal file
View File

@ -0,0 +1,121 @@
/*
Copyright 2017 Balena
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.
*/
import { CommandDefinition } from 'capitano';
import chalk from 'chalk';
import { stripIndent } from 'common-tags';
import * as sdk from 'etcher-sdk';
import { DriveList } from '../../utils/visuals/drive-list';
async function getDrive(options: {
drive?: string;
}): Promise<sdk.sourceDestination.BlockDevice> {
const sdk = await import('etcher-sdk');
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
const scanner = new sdk.scanner.Scanner([adapter]);
await scanner.start();
let drive: sdk.sourceDestination.BlockDevice;
if (options.drive !== undefined) {
const d = scanner.getBy('device', options.drive);
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
throw new Error(`Drive not found: ${options.drive}`);
}
drive = d;
} else {
const driveList = new DriveList(scanner);
drive = await driveList.run();
}
scanner.stop();
return drive;
}
export const flash: CommandDefinition<
{ image: string },
{ drive: string; yes: boolean }
> = {
signature: 'local flash <image>',
description: 'Flash an image to a drive',
//root: true,
help: stripIndent`
Use this command to flash a balenaOS image to a drive.
Examples:
$ balena local flash path/to/balenaos.img
$ balena local flash path/to/balenaos.img --drive /dev/disk2
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
`,
options: [
{
signature: 'yes',
boolean: true,
description: 'confirm non-interactively',
alias: 'y',
},
{
signature: 'drive',
parameter: 'drive',
description: 'drive',
alias: 'd',
},
],
async action(params, options) {
const visuals = await import('resin-cli-visuals');
const form = await import('resin-cli-form');
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await getDrive(options);
const yes =
options.yes ||
(await form.ask({
message: 'This will erase the selected drive. Are you sure?',
type: 'confirm',
name: 'yes',
default: false,
}));
if (yes !== true) {
console.log(chalk.red.bold('Aborted image flash'));
process.exit(0);
}
const source = new sourceDestination.File(
params.image,
sourceDestination.File.OpenFlags.Read,
);
const progressBars: { [key: string]: any } = {
flashing: new visuals.Progress('Flashing'),
verifying: new visuals.Progress('Validating'),
};
await multiWrite.pipeSourceToDestinations(
source,
[drive],
(_, error) => {
// onFail
console.log(chalk.red.bold(error.message));
},
(progress: sdk.multiWrite.MultiDestinationProgress) => {
// onProgress
progressBars[progress.type].update(progress);
},
true, // verify
);
},
};

View File

@ -15,7 +15,7 @@ limitations under the License.
###
exports.configure = require('./configure')
exports.flash = require('./flash')
exports.flash = require('./flash').flash
exports.logs = require('./logs')
exports.scan = require('./scan')
exports.ssh = require('./ssh')

View File

@ -0,0 +1,25 @@
import DynamicList = require('inquirer-dynamic-list');
export abstract class CustomDynamicList<T> extends DynamicList {
constructor(message: string, emptyMessage: string) {
super({ message, emptyMessage, choices: [] });
}
protected abstract getThings(): Iterable<T>;
protected abstract format(thing: T): string;
refresh(): void {
this.opt.choices.choices = [];
this.opt.choices.realChoices = [];
for (const thing of this.getThings()) {
this.addChoice({ name: this.format(thing), value: thing });
}
this.render();
}
async run(): Promise<T> {
this.refresh();
return await super.run();
}
}

View File

@ -0,0 +1,31 @@
import chalk from 'chalk';
import * as sdk from 'etcher-sdk';
import { CustomDynamicList } from './custom-dynamic-list';
export class DriveList extends CustomDynamicList<
sdk.sourceDestination.BlockDevice
> {
constructor(private scanner: sdk.scanner.Scanner) {
super(
'Select a drive',
`${chalk.red('x')} No available drives were detected, plug one in!`,
);
const refresh = this.refresh.bind(this);
scanner.on('attach', refresh);
scanner.on('detach', refresh);
}
protected *getThings() {
for (const drive of this.scanner.drives) {
if (drive instanceof sdk.sourceDestination.BlockDevice) {
yield drive;
}
}
}
protected format(drive: sdk.sourceDestination.BlockDevice) {
const size = drive.size / 1e9;
return `${drive.device} (${size.toFixed(1)} GB) - ${drive.description}`;
}
}

View File

@ -128,7 +128,7 @@
"dockerode-options": "^0.2.1",
"drivelist": "^5.0.22",
"ejs": "^2.5.7",
"etcher-image-write": "^9.0.3",
"etcher-sdk": "^0.2.0",
"event-stream": "3.3.4",
"express": "^4.13.3",
"global-tunnel-ng": "^2.1.1",

View File

@ -24,6 +24,7 @@
},
"include": [
"./typings/*.d.ts",
"./node_modules/etcher-sdk/typings/**/*.d.ts",
"./lib/**/*.ts"
]
}

26
typings/inquire-dynamic-list.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
declare module 'inquirer-dynamic-list' {
interface Choice {
name: string;
value: any;
}
class DynamicList {
opt: {
choices: {
choices: Choice[];
realChoices: Choice[];
};
};
constructor(options: {
message?: string;
emptyMessage?: string;
choices: Choice[];
});
addChoice(choice: Choice): void;
render(): void;
run(): Promise<any>;
}
export = DynamicList;
}