2017-12-20 21:46:01 +00:00
|
|
|
/*
|
2018-10-19 14:38:50 +00:00
|
|
|
Copyright 2016-2017 Balena
|
2017-12-20 21:46:01 +00:00
|
|
|
|
|
|
|
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 os = require('os');
|
|
|
|
import Promise = require('bluebird');
|
|
|
|
import _ = require('lodash');
|
|
|
|
import chalk from 'chalk';
|
|
|
|
import rindle = require('rindle');
|
|
|
|
import visuals = require('resin-cli-visuals');
|
2018-10-19 14:38:50 +00:00
|
|
|
import BalenaSdk = require('balena-sdk');
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-01-04 14:07:55 +00:00
|
|
|
import { InitializeEmitter, OperationState } from 'resin-device-init';
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-01-04 16:17:43 +00:00
|
|
|
const waitStreamAsync = Promise.promisify(rindle.wait);
|
|
|
|
|
2018-10-19 14:38:50 +00:00
|
|
|
const balena = BalenaSdk.fromSharedOptions();
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-01-09 15:05:24 +00:00
|
|
|
export function getGroupDefaults(group: {
|
|
|
|
options: { name: string; default?: string }[];
|
|
|
|
}): { [name: string]: string | undefined } {
|
2018-06-12 15:43:15 +00:00
|
|
|
return _.chain(group)
|
2017-12-20 21:46:01 +00:00
|
|
|
.get('options')
|
2018-01-09 15:05:24 +00:00
|
|
|
.map(question => [question.name, question.default])
|
2017-12-20 21:46:01 +00:00
|
|
|
.fromPairs()
|
|
|
|
.value();
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
|
|
|
export function stateToString(state: OperationState) {
|
2018-01-04 16:17:43 +00:00
|
|
|
const percentage = _.padStart(`${state.percentage}`, 3, '0');
|
2018-01-09 15:05:24 +00:00
|
|
|
const result = `${chalk.blue(percentage + '%')} ${chalk.cyan(
|
|
|
|
state.operation.command,
|
|
|
|
)}`;
|
2017-12-20 21:46:01 +00:00
|
|
|
|
|
|
|
switch (state.operation.command) {
|
|
|
|
case 'copy':
|
2018-01-09 15:05:24 +00:00
|
|
|
return `${result} ${state.operation.from.path} -> ${
|
|
|
|
state.operation.to.path
|
|
|
|
}`;
|
2017-12-20 21:46:01 +00:00
|
|
|
case 'replace':
|
2018-01-09 15:05:24 +00:00
|
|
|
return `${result} ${state.operation.file.path}, ${
|
|
|
|
state.operation.copy
|
|
|
|
} -> ${state.operation.replace}`;
|
2017-12-20 21:46:01 +00:00
|
|
|
case 'run-script':
|
|
|
|
return `${result} ${state.operation.script}`;
|
|
|
|
default:
|
|
|
|
throw new Error(`Unsupported operation: ${state.operation.command}`);
|
|
|
|
}
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-05-22 15:12:51 +00:00
|
|
|
export function sudo(
|
|
|
|
command: string[],
|
2018-08-07 17:06:15 +00:00
|
|
|
{ stderr, msg }: { stderr?: NodeJS.WritableStream; msg?: string } = {},
|
2018-05-22 15:12:51 +00:00
|
|
|
) {
|
|
|
|
const { executeWithPrivileges } = require('./sudo');
|
|
|
|
|
2017-12-20 21:46:01 +00:00
|
|
|
if (os.platform() !== 'win32') {
|
2018-05-22 15:12:51 +00:00
|
|
|
console.log(
|
|
|
|
msg || 'If asked please type your computer password to continue',
|
|
|
|
);
|
2017-12-20 21:46:01 +00:00
|
|
|
}
|
|
|
|
|
2018-05-22 15:12:51 +00:00
|
|
|
return executeWithPrivileges(command, stderr);
|
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-05-22 15:12:51 +00:00
|
|
|
export function runCommand(command: string): Promise<void> {
|
|
|
|
const capitano = require('capitano');
|
|
|
|
return Promise.fromCallback(resolver => capitano.run(command, resolver));
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-01-09 15:05:24 +00:00
|
|
|
export function getManifest(
|
|
|
|
image: string,
|
|
|
|
deviceType: string,
|
2018-10-19 14:38:50 +00:00
|
|
|
): Promise<BalenaSdk.DeviceType> {
|
2018-07-20 10:04:26 +00:00
|
|
|
const imagefs = require('resin-image-fs');
|
2017-12-20 21:46:01 +00:00
|
|
|
// Attempt to read manifest from the first
|
|
|
|
// partition, but fallback to the API if
|
|
|
|
// we encounter any errors along the way.
|
2018-01-09 15:05:24 +00:00
|
|
|
return imagefs
|
2018-04-30 08:24:43 +00:00
|
|
|
.readFile({
|
2018-01-09 15:05:24 +00:00
|
|
|
image,
|
2018-04-30 08:24:43 +00:00
|
|
|
partition: 1,
|
2018-01-09 15:05:24 +00:00
|
|
|
path: '/device-type.json',
|
|
|
|
})
|
|
|
|
.then(JSON.parse)
|
2018-10-19 14:38:50 +00:00
|
|
|
.catch(() => balena.models.device.getManifestBySlug(deviceType));
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
|
|
|
export function osProgressHandler(step: InitializeEmitter) {
|
|
|
|
step.on('stdout', process.stdout.write.bind(process.stdout));
|
|
|
|
step.on('stderr', process.stderr.write.bind(process.stderr));
|
|
|
|
|
|
|
|
step.on('state', function(state) {
|
2018-01-09 15:05:24 +00:00
|
|
|
if (state.operation.command === 'burn') {
|
|
|
|
return;
|
|
|
|
}
|
2018-01-04 16:17:43 +00:00
|
|
|
console.log(exports.stateToString(state));
|
2017-12-20 21:46:01 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const progressBars = {
|
|
|
|
write: new visuals.Progress('Writing Device OS'),
|
2018-01-04 14:07:55 +00:00
|
|
|
check: new visuals.Progress('Validating Device OS'),
|
2017-12-20 21:46:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
step.on('burn', state => progressBars[state.type].update(state));
|
|
|
|
|
2018-01-04 16:17:43 +00:00
|
|
|
return waitStreamAsync(step);
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-01-09 15:05:24 +00:00
|
|
|
export function getArchAndDeviceType(
|
|
|
|
applicationName: string,
|
|
|
|
): Promise<{ arch: string; device_type: string }> {
|
2017-12-20 21:46:01 +00:00
|
|
|
return Promise.join(
|
|
|
|
getApplication(applicationName),
|
2018-10-19 14:38:50 +00:00
|
|
|
balena.models.config.getDeviceTypes(),
|
2018-01-09 15:05:24 +00:00
|
|
|
function(app, deviceTypes) {
|
2018-01-04 16:17:43 +00:00
|
|
|
const config = _.find(deviceTypes, { slug: app.device_type });
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2018-01-04 16:17:43 +00:00
|
|
|
if (!config) {
|
2017-12-20 21:46:01 +00:00
|
|
|
throw new Error('Could not read application information!');
|
|
|
|
}
|
|
|
|
|
|
|
|
return { device_type: app.device_type, arch: config.arch };
|
2018-01-04 14:07:55 +00:00
|
|
|
},
|
2017-12-20 21:46:01 +00:00
|
|
|
);
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2017-12-11 22:37:56 +00:00
|
|
|
export function getApplication(applicationName: string) {
|
2017-12-20 21:46:01 +00:00
|
|
|
// Check for an app of the form `user/application`, and send
|
2018-01-04 16:17:43 +00:00
|
|
|
// that off to a special handler (before importing any modules)
|
2018-05-22 15:12:51 +00:00
|
|
|
const match = applicationName.split('/');
|
2018-01-04 16:17:43 +00:00
|
|
|
|
2018-03-19 18:58:05 +00:00
|
|
|
const extraOptions = {
|
|
|
|
$expand: {
|
|
|
|
application_type: {
|
2018-03-20 11:06:44 +00:00
|
|
|
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
|
2018-03-19 18:58:05 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2018-05-22 15:12:51 +00:00
|
|
|
if (match.length > 1) {
|
2018-10-19 14:38:50 +00:00
|
|
|
return balena.models.application.getAppByOwner(
|
2018-03-20 11:06:44 +00:00
|
|
|
match[1],
|
2018-05-22 15:12:51 +00:00
|
|
|
match[0],
|
2018-03-20 11:06:44 +00:00
|
|
|
extraOptions,
|
|
|
|
);
|
2017-12-20 21:46:01 +00:00
|
|
|
}
|
|
|
|
|
2018-10-19 14:38:50 +00:00
|
|
|
return balena.models.application.get(applicationName, extraOptions);
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
|
|
|
// A function to reliably execute a command
|
|
|
|
// in all supported operating systems, including
|
|
|
|
// different Windows environments like `cmd.exe`
|
|
|
|
// and `Cygwin`.
|
|
|
|
export function getSubShellCommand(command: string) {
|
|
|
|
if (os.platform() === 'win32') {
|
|
|
|
return {
|
|
|
|
program: 'cmd.exe',
|
2018-01-09 15:05:24 +00:00
|
|
|
args: ['/s', '/c', command],
|
2017-12-20 21:46:01 +00:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
program: '/bin/sh',
|
2018-01-09 15:05:24 +00:00
|
|
|
args: ['-c', command],
|
2017-12-20 21:46:01 +00:00
|
|
|
};
|
|
|
|
}
|
2018-01-04 14:07:55 +00:00
|
|
|
}
|