2019-04-25 17:39:16 +00:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2019 Balena Ltd.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
2018-05-22 15:12:51 +00:00
|
|
|
|
2019-10-13 23:52:43 +00:00
|
|
|
import { ChildProcess, spawn, SpawnOptions } from 'child_process';
|
2020-05-04 14:57:23 +00:00
|
|
|
import { stripIndent } from './lazy';
|
2018-05-22 15:12:51 +00:00
|
|
|
|
2019-10-13 23:52:43 +00:00
|
|
|
/**
|
|
|
|
* Execute a child process with admin / superuser privileges, prompting the user for
|
|
|
|
* elevation as needed, and taking care of shell-escaping arguments in a suitable way
|
|
|
|
* for Windows and Linux/Mac.
|
|
|
|
*
|
|
|
|
* @param command Unescaped array of command and args to be executed. If isCLIcmd is
|
|
|
|
* true, the command should not include the 'node' or 'balena' components, for
|
|
|
|
* example: ['internal', 'osinit', ...]. This function will add argv[0] and argv[1]
|
|
|
|
* as needed (taking process.pkg into account -- CLI standalone zip package), and
|
|
|
|
* will also shell-escape the arguments as needed, taking into account the
|
|
|
|
* differences between bash/sh and the Windows cmd.exe in relation to escape
|
|
|
|
* characters.
|
|
|
|
* @param stderr Optional stream to which stderr should be piped
|
2020-10-20 21:11:17 +00:00
|
|
|
* @param isCLIcmd (default: true) Whether the command array is a balena CLI command
|
2019-10-13 23:52:43 +00:00
|
|
|
* (e.g. ['internal', 'osinit', ...]), in which case process.argv[0] and argv[1] are
|
|
|
|
* added as necessary, depending on whether the CLI is running as a standalone zip
|
|
|
|
* package (with Node built in).
|
|
|
|
*/
|
2018-05-22 15:12:51 +00:00
|
|
|
export async function executeWithPrivileges(
|
|
|
|
command: string[],
|
|
|
|
stderr?: NodeJS.WritableStream,
|
2019-10-13 23:52:43 +00:00
|
|
|
isCLIcmd = true,
|
|
|
|
): Promise<void> {
|
|
|
|
// whether the CLI is already running with admin / super user privileges
|
|
|
|
const isElevated = await (await import('is-elevated'))();
|
|
|
|
const { shellEscape } = await import('./helpers');
|
|
|
|
const opts: SpawnOptions = {
|
2018-05-22 15:12:51 +00:00
|
|
|
env: process.env,
|
2019-10-13 23:52:43 +00:00
|
|
|
stdio: ['inherit', 'inherit', stderr ? 'pipe' : 'inherit'],
|
2018-05-22 15:12:51 +00:00
|
|
|
};
|
2019-10-13 23:52:43 +00:00
|
|
|
if (isElevated) {
|
|
|
|
if (isCLIcmd) {
|
|
|
|
// opts.shell is false, so preserve pkg's '/snapshot' at argv[1]
|
|
|
|
command = [process.argv[0], process.argv[1], ...command];
|
|
|
|
}
|
|
|
|
// already running with privileges: simply spawn the command
|
|
|
|
await spawnAndPipe(command[0], command.slice(1), opts, stderr);
|
|
|
|
} else {
|
|
|
|
if (isCLIcmd) {
|
|
|
|
// In the case of a CLI standalone zip package (process.pkg is truthy),
|
|
|
|
// the Node executable is bundled with the source code and node_modules
|
|
|
|
// folder in a single file named in argv[0]. In this case, argv[1]
|
|
|
|
// contains a "/snapshot" path that should be discarded when opts.shell
|
|
|
|
// is true.
|
|
|
|
command = (process as any).pkg
|
|
|
|
? [process.argv[0], ...command]
|
|
|
|
: [process.argv[0], process.argv[1], ...command];
|
|
|
|
}
|
|
|
|
opts.shell = true;
|
|
|
|
const escapedCmd = shellEscape(command);
|
|
|
|
// running as ordinary user: elevate privileges
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
await windosuExec(escapedCmd, stderr);
|
|
|
|
} else {
|
|
|
|
await spawnAndPipe('sudo', escapedCmd, opts, stderr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-22 15:12:51 +00:00
|
|
|
|
2019-10-13 23:52:43 +00:00
|
|
|
async function spawnAndPipe(
|
|
|
|
spawnCmd: string,
|
|
|
|
spawnArgs: string[],
|
|
|
|
spawnOpts: SpawnOptions,
|
|
|
|
stderr?: NodeJS.WritableStream,
|
|
|
|
) {
|
2021-07-09 13:44:38 +00:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
2019-10-13 23:52:43 +00:00
|
|
|
const ps: ChildProcess = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
|
|
ps.on('error', reject);
|
2020-06-15 22:53:07 +00:00
|
|
|
ps.on('exit', (codeOrSignal) => {
|
2019-10-13 23:52:43 +00:00
|
|
|
if (codeOrSignal !== 0) {
|
|
|
|
const errMsgCmd = `[${[spawnCmd, ...spawnArgs].join()}]`;
|
|
|
|
reject(
|
|
|
|
new Error(
|
|
|
|
`Child process exited with error code "${codeOrSignal}" for command:\n${errMsgCmd}`,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (stderr) {
|
|
|
|
ps.stderr.pipe(stderr);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2018-05-22 15:12:51 +00:00
|
|
|
|
2019-10-13 23:52:43 +00:00
|
|
|
async function windosuExec(
|
|
|
|
escapedArgs: string[],
|
|
|
|
stderr?: NodeJS.WritableStream,
|
|
|
|
): Promise<void> {
|
2018-05-22 15:12:51 +00:00
|
|
|
if (stderr) {
|
2019-10-13 23:52:43 +00:00
|
|
|
const msg = stripIndent`
|
|
|
|
Error: unable to elevate privileges. Please run the command prompt as an Administrator:
|
|
|
|
https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/
|
|
|
|
`;
|
|
|
|
throw new Error(msg);
|
2018-05-22 15:12:51 +00:00
|
|
|
}
|
2019-10-13 23:52:43 +00:00
|
|
|
return require('windosu').exec(escapedArgs.join(' '));
|
2018-05-22 15:12:51 +00:00
|
|
|
}
|