Auto-merge for PR #895 via VersionBot

Add join/leave commands to promote and move devices between platforms
This commit is contained in:
resin-io-versionbot[bot] 2018-07-20 12:36:20 +00:00 committed by GitHub
commit 171632f83f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 649 additions and 38 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## v7.8.0 - 2018-07-20
* Add join/leave commands to promote and move devices between platforms #895 [Akis Kesoglou]
## v7.7.4 - 2018-07-17
* Update TypeScript to 2.8.1 #923 [Tim Perry]

View File

@ -102,8 +102,8 @@ exports.login =
return patterns.askLoginType().then (loginType) ->
if loginType is 'register'
capitanoRunAsync = Promise.promisify(require('capitano').run)
return capitanoRunAsync('signup')
{ runCommand } = require('../utils/helpers')
return runCommand('signup')
options[loginType] = true
return login(options)

View File

@ -198,7 +198,7 @@ exports.reconfigure =
Promise = require('bluebird')
config = require('resin-config-json')
visuals = require('resin-cli-visuals')
capitanoRunAsync = Promise.promisify(require('capitano').run)
{ runCommand } = require('../utils/helpers')
umountAsync = Promise.promisify(require('umount').umount)
Promise.try ->
@ -212,7 +212,7 @@ exports.reconfigure =
configureCommand = "os configure #{drive} --device #{uuid}"
if options.advanced
configureCommand += ' --advanced'
return capitanoRunAsync(configureCommand)
return runCommand(configureCommand)
.then ->
console.info('Done')
.nodeify(done)

View File

@ -415,7 +415,6 @@ exports.init =
permission: 'user'
action: (params, options, done) ->
Promise = require('bluebird')
capitanoRunAsync = Promise.promisify(require('capitano').run)
rimraf = Promise.promisify(require('rimraf'))
tmp = require('tmp')
tmpNameAsync = Promise.promisify(tmp.tmpName)
@ -423,6 +422,7 @@ exports.init =
resin = require('resin-sdk-preconfigured')
patterns = require('../utils/patterns')
{ runCommand } = require('../utils/helpers')
Promise.try ->
return options.application if options.application?
@ -433,12 +433,12 @@ exports.init =
download = ->
tmpNameAsync().then (tempPath) ->
osVersion = options['os-version'] or 'default'
capitanoRunAsync("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
.disposer (tempPath) ->
return rimraf(tempPath)
Promise.using download(), (tempPath) ->
capitanoRunAsync("device register #{application.app_name}")
runCommand("device register #{application.app_name}")
.then(resin.models.device.get)
.tap (device) ->
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
@ -446,14 +446,14 @@ exports.init =
configureCommand += " --config '#{options.config}'"
else if options.advanced
configureCommand += ' --advanced'
capitanoRunAsync(configureCommand)
runCommand(configureCommand)
.then ->
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
if options.yes
osInitCommand += ' --yes'
if options.drive
osInitCommand += " --drive #{options.drive}"
capitanoRunAsync(osInitCommand)
runCommand(osInitCommand)
# Make sure the device resource is removed if there is an
# error when configuring or initializing a device image
.catch (error) ->

View File

@ -38,3 +38,5 @@ module.exports =
util: require('./util')
preload: require('./preload')
push: require('./push')
join: require('./join')
leave: require('./leave')

View File

@ -35,3 +35,53 @@ exports.osInit =
init.initialize(params.image, params.type, config)
.then(helpers.osProgressHandler)
.nodeify(done)
exports.scanDevices =
signature: 'internal scandevices'
description: 'scan for local resin-enabled devices and show a picker to choose one'
help: '''
Don't use this command directly!
'''
hidden: true
root: true
action: (params, options, done) ->
Promise = require('bluebird')
{ forms } = require('resin-sync')
return Promise.try ->
forms.selectLocalResinOsDevice()
.then (hostnameOrIp) ->
console.error("==> Selected device: #{hostnameOrIp}")
.nodeify(done)
exports.sudo =
signature: 'internal sudo <command>'
description: 'execute arbitrary commands in a privileged subprocess'
help: '''
Don't use this command directly!
<command> must be passed as a single argument. That means, you need to make sure
you enclose <command> in quotes (eg. resin internal sudo 'ls -alF') if for
whatever reason you invoke the command directly or, typically, pass <command>
as a single argument to spawn (eg. `spawn('resin', [ 'internal', 'sudo', 'ls -alF' ])`).
Furthermore, this command will naively split <command> on whitespace and directly
forward the parts as arguments to `sudo`, so be careful.
'''
hidden: true
action: (params, options, done) ->
os = require('os')
Promise = require('bluebird')
return Promise.try ->
if os.platform() is 'win32'
windosu = require('windosu')
windosu.exec(params.command, {})
else
{ spawn } = require('child_process')
{ wait } = require('rindle')
cmd = params.command.split(' ')
ps = spawn('sudo', cmd, stdio: 'inherit', env: process.env)
wait(ps)
.nodeify(done)

62
lib/actions/join.ts Normal file
View File

@ -0,0 +1,62 @@
/*
Copyright 2016-2017 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.
*/
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
interface Args {
deviceIp?: string;
}
interface Options {
application?: string;
}
export const join: CommandDefinition<Args, Options> = {
signature: 'join [deviceIp]',
description:
'Promote a local device running unmanaged resinOS to join a resin.io application',
help: stripIndent`
Examples:
$ resin join
$ resin join resin.local
$ resin join resin.local --application MyApp
$ resin join 192.168.1.25
$ resin join 192.168.1.25 --application MyApp
`,
options: [
{
signature: 'application',
parameter: 'application',
alias: 'a',
description: 'The name of the application the device should join',
},
],
primary: true,
async action(params, options, done) {
const resin = await import('resin-sdk');
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = resin.fromSharedOptions();
const logger = new Logger();
return Bluebird.try(() => {
return promote.join(logger, sdk, params.deviceIp, options.application);
}).nodeify(done);
},
};

49
lib/actions/leave.ts Normal file
View File

@ -0,0 +1,49 @@
/*
Copyright 2016-2017 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.
*/
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
interface Args {
deviceIp?: string;
}
export const leave: CommandDefinition<Args, {}> = {
signature: 'leave [deviceIp]',
description: 'Detach a local device from its resin.io application',
help: stripIndent`
Examples:
$ resin leave
$ resin leave resin.local
$ resin leave 192.168.1.25
`,
options: [],
permission: 'user',
primary: true,
async action(params, _options, done) {
const resin = await import('resin-sdk');
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = resin.fromSharedOptions();
const logger = new Logger();
return Bluebird.try(() => {
return promote.leave(logger, sdk, params.deviceIp);
}).nodeify(done);
},
};

View File

@ -34,29 +34,28 @@ exports.wizard =
'''
primary: true
action: (params, options, done) ->
Promise = require('bluebird')
capitanoRunAsync = Promise.promisify(require('capitano').run)
resin = require('resin-sdk-preconfigured')
patterns = require('../utils/patterns')
{ runCommand } = require('../utils/helpers')
resin.auth.isLoggedIn().then (isLoggedIn) ->
return if isLoggedIn
console.info('Looks like you\'re not logged in yet!')
console.info('Lets go through a quick wizard to get you started.\n')
return capitanoRunAsync('login')
console.info("Let's go through a quick wizard to get you started.\n")
return runCommand('login')
.then ->
return if params.name?
patterns.selectOrCreateApplication().tap (applicationName) ->
resin.models.application.has(applicationName).then (hasApplication) ->
return applicationName if hasApplication
capitanoRunAsync("app create #{applicationName}")
runCommand("app create #{applicationName}")
.then (applicationName) ->
params.name = applicationName
.then ->
return capitanoRunAsync("device init --application #{params.name}")
return runCommand("device init --application #{params.name}")
.tap(patterns.awaitDevice)
.then (uuid) ->
return capitanoRunAsync("device #{uuid}")
return runCommand("device #{uuid}")
.then ->
return resin.models.application.get(params.name)
.then (application) ->

View File

@ -207,6 +207,8 @@ capitano.command(actions.util.availableDrives)
# ---------- Internal utils ----------
capitano.command(actions.internal.osInit)
capitano.command(actions.internal.scanDevices)
capitano.command(actions.internal.sudo)
#------------ Local build and deploy -------
capitano.command(actions.build)
@ -215,6 +217,10 @@ capitano.command(actions.deploy)
#------------ Push/remote builds -------
capitano.command(actions.push.push)
#------------ Join/Leave -------
capitano.command(actions.join.join)
capitano.command(actions.leave.leave)
update.notify()
cli = capitano.parse(process.argv)

View File

@ -13,6 +13,8 @@ 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 * as fs from 'fs';
import Promise = require('bluebird');
import ResinSdk = require('resin-sdk');
import deviceConfig = require('resin-device-config');
@ -20,6 +22,18 @@ import * as semver from 'resin-semver';
const resin = ResinSdk.fromSharedOptions();
function readRootCa(): Promise<string | void> {
const caFile = process.env.NODE_EXTRA_CA_CERTS;
if (!caFile) {
return Promise.resolve();
}
return Promise.fromCallback(cb =>
fs.readFile(caFile, { encoding: 'utf8' }, cb),
)
.then(pem => Buffer.from(pem).toString('base64'))
.catch({ code: 'ENOENT' }, () => {});
}
export function generateBaseConfig(
application: ResinSdk.Application,
options: { version?: string; appUpdatePollInterval?: number },
@ -39,6 +53,9 @@ export function generateBaseConfig(
registryUrl: resin.settings.get('registryUrl'),
deltaUrl: resin.settings.get('deltaUrl'),
apiConfig: resin.models.config.getAll(),
rootCA: readRootCa().catch(() => {
console.warn('Could not read root CA');
}),
}).then(results => {
return deviceConfig.generate(
{
@ -57,6 +74,7 @@ export function generateBaseConfig(
mixpanel: {
token: results.apiConfig.mixpanelToken,
},
balenaRootCA: results.rootCA,
},
options,
);

View File

@ -19,15 +19,12 @@ import Promise = require('bluebird');
import _ = require('lodash');
import chalk from 'chalk';
import rindle = require('rindle');
import imagefs = require('resin-image-fs');
import visuals = require('resin-cli-visuals');
import ResinSdk = require('resin-sdk');
import { execute } from 'president';
import { InitializeEmitter, OperationState } from 'resin-device-init';
const waitStreamAsync = Promise.promisify(rindle.wait);
const presidentExecuteAsync = Promise.promisify(execute);
const resin = ResinSdk.fromSharedOptions();
@ -63,20 +60,31 @@ export function stateToString(state: OperationState) {
}
}
export function sudo(command: string[]) {
export function sudo(
command: string[],
{ stderr, msg }: { stderr?: NodeJS.WritableStream; msg?: string },
) {
const { executeWithPrivileges } = require('./sudo');
if (os.platform() !== 'win32') {
console.log('If asked please type your computer password to continue');
console.log(
msg || 'If asked please type your computer password to continue',
);
}
command = _.union(_.take(process.argv, 2), command);
return executeWithPrivileges(command, stderr);
}
return presidentExecuteAsync(command);
export function runCommand(command: string): Promise<void> {
const capitano = require('capitano');
return Promise.fromCallback(resolver => capitano.run(command, resolver));
}
export function getManifest(
image: string,
deviceType: string,
): Promise<ResinSdk.DeviceType> {
const imagefs = require('resin-image-fs');
// Attempt to read manifest from the first
// partition, but fallback to the API if
// we encounter any errors along the way.
@ -132,7 +140,7 @@ export function getArchAndDeviceType(
export function getApplication(applicationName: string) {
// Check for an app of the form `user/application`, and send
// that off to a special handler (before importing any modules)
const match = /(\w+)\/(\w+)/.exec(applicationName);
const match = applicationName.split('/');
const extraOptions = {
$expand: {
@ -142,10 +150,10 @@ export function getApplication(applicationName: string) {
},
};
if (match) {
if (match.length > 1) {
return resin.models.application.getAppByOwner(
match[2],
match[1],
match[0],
extraOptions,
);
}

View File

@ -23,11 +23,6 @@ import chalk from 'chalk';
import validation = require('./validation');
import messages = require('./messages');
export interface ListSelectionEntry {
name: string;
extra: any;
}
export function authenticate(options: {}): Promise<void> {
return form
.run(
@ -250,14 +245,14 @@ export function inferOrSelectDevice(preferredUuid: string) {
});
}
export function selectFromList(
export function selectFromList<T>(
message: string,
selections: ListSelectionEntry[],
): Promise<ListSelectionEntry> {
choices: Array<T & { name: string }>,
): Promise<T> {
return form.ask({
message,
type: 'list',
choices: _.map(selections, s => ({
choices: _.map(choices, s => ({
name: s.name,
value: s,
})),

323
lib/utils/promote.ts Normal file
View File

@ -0,0 +1,323 @@
import { stripIndent } from 'common-tags';
import { ResinSDK, Application } from 'resin-sdk';
import Logger = require('./logger');
import { runCommand } from './helpers';
import { exec, execBuffered } from './ssh';
const MIN_RESINOS_VERSION = 'v2.14.0';
export async function join(
logger: Logger,
sdk: ResinSDK,
deviceHostnameOrIp?: string,
appName?: string,
): Promise<void> {
logger.logDebug('Checking login...');
const isLoggedIn = await sdk.auth.isLoggedIn();
if (!isLoggedIn) {
logger.logInfo("Looks like you're not logged in yet!");
logger.logInfo("Let's go through a quick wizard to get you started.\n");
await runCommand('login');
}
logger.logDebug('Determining device...');
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
await assertDeviceIsCompatible(deviceIp);
logger.logDebug(`Using device: ${deviceIp}`);
logger.logDebug('Determining device type...');
const deviceType = await getDeviceType(deviceIp);
logger.logDebug(`Device type: ${deviceType}`);
logger.logDebug('Determining application...');
const app = await getOrSelectApplication(sdk, deviceType, appName);
logger.logDebug(`Using application: ${app.app_name} (${app.device_type})`);
if (app.device_type != deviceType) {
logger.logDebug(`Forcing device type to: ${deviceType}`);
app.device_type = deviceType;
}
logger.logDebug('Generating application config...');
const config = await generateApplicationConfig(sdk, app);
logger.logDebug(`Using config: ${JSON.stringify(config, null, 2)}`);
logger.logDebug('Configuring...');
await configure(deviceIp, config);
logger.logDebug('All done.');
const platformUrl = await sdk.settings.get('resinUrl');
logger.logSuccess(`Device successfully joined ${platformUrl}!`);
}
export async function leave(
logger: Logger,
_sdk: ResinSDK,
deviceHostnameOrIp?: string,
): Promise<void> {
logger.logDebug('Determining device...');
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
await assertDeviceIsCompatible(deviceIp);
logger.logDebug(`Using device: ${deviceIp}`);
logger.logDebug('Deconfiguring...');
await deconfigure(deviceIp);
logger.logDebug('All done.');
logger.logSuccess('Device successfully left the platform.');
}
async function execCommand(
deviceIp: string,
cmd: string,
msg: string,
): Promise<void> {
const through = await import('through2');
const visuals = await import('resin-cli-visuals');
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
const innerSpinner = spinner.spinner;
const stream = through(function(data, _enc, cb) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
cb(null, data);
});
spinner.start();
await exec(deviceIp, cmd, stream);
spinner.stop();
}
async function configure(deviceIp: string, config: any): Promise<void> {
// Passing the JSON is slightly tricky due to the many layers of indirection
// so we just base64-encode it here and decode it at the other end, when invoking
// os-config.
const json = JSON.stringify(config);
const b64 = Buffer.from(json).toString('base64');
const str = `"$(base64 -d <<< ${b64})"`;
await execCommand(deviceIp, `os-config join '${str}'`, 'Configuring...');
}
async function deconfigure(deviceIp: string): Promise<void> {
await execCommand(deviceIp, 'os-config leave', 'Configuring...');
}
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
const { exitWithExpectedError } = await import('../utils/patterns');
try {
await execBuffered(deviceIp, 'os-config --version');
} catch (err) {
exitWithExpectedError(stripIndent`
Device "${deviceIp}" is incompatible and cannot join or leave an application.
Please select or provision device with resinOS newer than ${MIN_RESINOS_VERSION}.`);
}
}
async function getDeviceType(deviceIp: string): Promise<string> {
const output = await execBuffered(deviceIp, 'cat /etc/os-release');
const match = /^SLUG="([^"]+)"$/m.exec(output);
if (!match) {
throw new Error('Failed to determine device type');
}
return match[1];
}
async function getOrSelectLocalDevice(deviceIp?: string): Promise<string> {
if (deviceIp) {
return deviceIp;
}
const through = await import('through2');
let ip: string | null = null;
const stream = through(function(data, _enc, cb) {
const match = /^==> Selected device: (.*)$/m.exec(data.toString());
if (match) {
ip = match[1];
cb();
} else {
cb(null, data);
}
});
stream.pipe(process.stderr);
const { sudo } = await import('../utils/helpers');
const command = process.argv.slice(0, 2).concat(['internal', 'scandevices']);
await sudo(command, {
stderr: stream,
msg:
'Scanning for local devices. If asked, please type your computer password.',
});
if (!ip) {
throw new Error('No device selected');
}
return ip;
}
async function getOrSelectApplication(
sdk: ResinSDK,
deviceType: string,
appName?: string,
): Promise<Application> {
const _ = await import('lodash');
const form = await import('resin-cli-form');
const { selectFromList } = await import('../utils/patterns');
const allDeviceTypes = await sdk.models.config.getDeviceTypes();
const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType });
if (!deviceTypeManifest) {
throw new Error(`"${deviceType}" is not a valid device type`);
}
const compatibleDeviceTypes = _(allDeviceTypes)
.filter({ arch: deviceTypeManifest.arch })
.map(type => type.slug)
.value();
const options: any = {
$expand: { user: { $select: ['username'] } },
$filter: { device_type: { $in: compatibleDeviceTypes } },
};
if (!appName) {
// No application specified, show a list to select one.
const applications = await sdk.models.application.getAll(options);
if (applications.length === 0) {
const shouldCreateApp = await form.ask({
message:
'You have no applications this device can join.\n' +
'Would you like to create one now?',
type: 'confirm',
default: true,
});
if (shouldCreateApp) {
return createApplication(sdk, deviceType);
}
process.exit(1);
}
return selectFromList(
'Select application',
_.map(applications, app => _.merge({ name: app.app_name }, app)),
);
}
// We're given an application; resolve it if it's ambiguous and also validate
// it's of appropriate device type.
options.$filter = { app_name: appName };
// Check for an app of the form `user/application` and update the API query.
const match = appName.split('/');
if (match.length > 1) {
// These will match at most one app, so we'll return early.
options.$expand.user.$filter = { username: match[0] };
options.$filter.app_name = match[1];
}
// Fetch all applications with the given name that are accessible to the user
const applications = await sdk.pine.get<Application>({
resource: 'application',
options,
});
if (applications.length === 0) {
const shouldCreateApp = await form.ask({
message:
`No application found with name "${appName}".\n` +
'Would you like to create it now?',
type: 'confirm',
default: true,
});
if (shouldCreateApp) {
return createApplication(sdk, deviceType, options.$filter.app_name);
}
process.exit(1);
}
// We've found at least one app with the given name.
// Filter out apps for non-matching device types and see what we're left with.
const validApplications = applications.filter(app =>
_.includes(compatibleDeviceTypes, app.device_type),
);
if (validApplications.length === 1) {
return validApplications[0];
}
// If we got more than one application with the same name it means that the
// user has access to a collab app with the same name as a personal app. We
// present a list to the user which shows the fully qualified application
// name (user/appname) and allows them to select
return selectFromList(
'Found multiple applications with that name; please select the one to use',
_.map(validApplications, app => {
const owner = _.get(app, 'user[0].username');
return _.merge({ name: `${owner}/${app.app_name}` }, app);
}),
);
}
async function createApplication(
sdk: ResinSDK,
deviceType: string,
name?: string,
): Promise<Application> {
const form = await import('resin-cli-form');
const validation = await import('./validation');
const patterns = await import('./patterns');
const user = await sdk.auth.getUserId();
const queryOptions = {
$filter: { user },
};
const appName = await new Promise<string>(async (resolve, reject) => {
while (true) {
try {
const appName = await form.ask({
message: 'Enter a name for your new application:',
type: 'input',
default: name,
validate: validation.validateApplicationName,
});
try {
await sdk.models.application.get(appName, queryOptions);
patterns.printErrorMessage(
'You already have an application with that name; please choose another.',
);
continue;
} catch (err) {
return resolve(appName);
}
} catch (err) {
return reject(err);
}
}
});
return sdk.models.application.create({
name: appName,
deviceType,
});
}
async function generateApplicationConfig(sdk: ResinSDK, app: Application) {
const form = await import('resin-cli-form');
const { generateApplicationConfig: configGen } = await import('./config');
const manifest = await sdk.models.device.getManifestBySlug(app.device_type);
const opts =
manifest.options && manifest.options.filter(opt => opt.name !== 'network');
const values = await form.run(opts);
const config = await configGen(app, values);
if (config.connectivity === 'connman') {
delete config.connectivity;
delete config.files;
}
return config;
}

65
lib/utils/ssh.ts Normal file
View File

@ -0,0 +1,65 @@
import { spawn } from 'child_process';
import * as Bluebird from 'bluebird';
import TypedError = require('typed-error');
import { getSubShellCommand } from './helpers';
export class ExecError extends TypedError {
public cmd: string;
public exitCode: number;
constructor(cmd: string, exitCode: number) {
super(`Command '${cmd}' failed with error: ${exitCode}`);
this.cmd = cmd;
this.exitCode = exitCode;
}
}
export async function exec(
deviceIp: string,
cmd: string,
stdout?: NodeJS.WritableStream,
): Promise<void> {
const command = `ssh \
-t \
-p 22222 \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@${deviceIp} \
${cmd}`;
const stdio = ['ignore', stdout ? 'pipe' : 'inherit', 'ignore'];
const { program, args } = getSubShellCommand(command);
const exitCode = await new Bluebird<number>((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', resolve);
if (stdout) {
ps.stdout.pipe(stdout);
}
});
if (exitCode != 0) {
throw new ExecError(cmd, exitCode);
}
}
export async function execBuffered(
deviceIp: string,
cmd: string,
enc?: string,
): Promise<string> {
const through = await import('through2');
const buffer: string[] = [];
await exec(
deviceIp,
cmd,
through(function(data, _enc, cb) {
buffer.push(data.toString(enc));
cb();
}),
);
return buffer.join('');
}

26
lib/utils/sudo.ts Normal file
View File

@ -0,0 +1,26 @@
import { spawn } from 'child_process';
import * as Bluebird from 'bluebird';
import * as rindle from 'rindle';
export async function executeWithPrivileges(
command: string[],
stderr?: NodeJS.WritableStream,
): Promise<void> {
const opts = {
stdio: ['inherit', 'inherit', stderr ? 'pipe' : 'inherit'],
env: process.env,
};
const args = process.argv
.slice(0, 2)
.concat(['internal', 'sudo', command.join(' ')]);
const ps = spawn(args[0], args.slice(1), opts);
if (stderr) {
ps.stderr.pipe(stderr);
}
return Bluebird.fromCallback(resolver => rindle.wait(ps, resolver));
}

View File

@ -1,6 +1,6 @@
{
"name": "resin-cli",
"version": "7.7.4",
"version": "7.8.0",
"description": "The official resin.io CLI tool",
"main": "./build/actions/index.js",
"homepage": "https://github.com/resin-io/resin-cli",
@ -90,6 +90,7 @@
"dependencies": {
"@resin.io/valid-email": "^0.1.0",
"@types/stream-to-promise": "2.2.0",
"@types/through2": "^2.0.33",
"JSONStream": "^1.0.3",
"ansi-escapes": "^2.0.0",
"any-promise": "^1.3.0",
@ -129,7 +130,6 @@
"mz": "^2.6.0",
"node-cleanup": "^2.1.2",
"opn": "^5.1.0",
"president": "^2.0.1",
"prettyjson": "^1.1.3",
"progress-stream": "^2.0.0",
"raven": "^2.5.0",
@ -164,12 +164,14 @@
"tar-stream": "^1.5.5",
"through2": "^2.0.3",
"tmp": "0.0.31",
"typed-error": "^2.0.0",
"umount": "^1.1.6",
"unzip2": "^0.2.5",
"update-notifier": "^2.2.0",
"window-size": "^1.1.0"
},
"optionalDependencies": {
"removedrive": "^1.0.0"
"removedrive": "^1.0.0",
"windosu": "^0.2.0"
}
}

View File

@ -21,6 +21,8 @@ declare module 'capitano' {
help: string;
options?: OptionDefinition[];
permission?: 'user';
root?: boolean;
primary?: boolean;
action(params: P, options: O, done: () => void): void;
}