mirror of
https://github.com/balena-io/etcher.git
synced 2025-11-09 02:18:31 +00:00
105 lines
3.1 KiB
TypeScript
105 lines
3.1 KiB
TypeScript
/*
|
|
* Copyright 2025 balena.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 { spawn } from 'child_process';
|
|
import { access, constants } from 'fs/promises';
|
|
|
|
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
|
|
|
/** Check for kdesudo or pkexec */
|
|
function checkLinuxBinary() {
|
|
// eslint-disable-next-line no-async-promise-executor
|
|
return new Promise(async (resolve, reject) => {
|
|
// We used to prefer gksudo over pkexec since it enabled a better prompt.
|
|
// However, gksudo cannot run multiple commands concurrently.
|
|
|
|
const paths = ['/usr/bin/kdesudo', '/usr/bin/pkexec'];
|
|
for (const path of paths) {
|
|
try {
|
|
// check if the file exist and is executable
|
|
await access(path, constants.X_OK);
|
|
resolve(path);
|
|
} catch (error: any) {
|
|
continue;
|
|
}
|
|
}
|
|
reject('Unable to find pkexec or kdesudo.');
|
|
});
|
|
}
|
|
|
|
function escapeDoubleQuotes(escapeString: string) {
|
|
return escapeString.replace(/"/g, '\\"');
|
|
}
|
|
|
|
export async function sudo(
|
|
command: string[],
|
|
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
|
const linuxBinary: string = (await checkLinuxBinary()) as string;
|
|
if (!linuxBinary) {
|
|
throw new Error('Unable to find pkexec.');
|
|
}
|
|
|
|
const parameters = [];
|
|
|
|
// Add pkexec specific parameters
|
|
if (/pkexec/i.test(linuxBinary)) {
|
|
parameters.push('--disable-internal-agent');
|
|
}
|
|
|
|
// Build the shell command string
|
|
const shellCmd = `echo ${SUCCESSFUL_AUTH_MARKER} && ${command
|
|
.map((a) => escapeDoubleQuotes(a))
|
|
.join(' ')}`;
|
|
|
|
parameters.push('/bin/bash');
|
|
parameters.push('-c');
|
|
parameters.push(shellCmd);
|
|
|
|
const elevateProcess = spawn(linuxBinary, parameters);
|
|
|
|
let elevated = '';
|
|
|
|
elevateProcess.stdout.on('data', (data) => {
|
|
// console.log(`stdout: ${data.toString()}`);
|
|
if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
|
// if the first data comming out of the sudo command is the expected marker we resolve the promise
|
|
elevated = 'granted';
|
|
} else {
|
|
// if the first data comming out of the sudo command is not the expected marker we reject the promise
|
|
elevated = 'refused';
|
|
}
|
|
});
|
|
|
|
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
|
return new Promise((resolve, reject) => {
|
|
const checkElevation = setInterval(() => {
|
|
if (elevated === 'granted') {
|
|
clearInterval(checkElevation);
|
|
resolve({ cancelled: false });
|
|
} else if (elevated === 'refused') {
|
|
clearInterval(checkElevation);
|
|
resolve({ cancelled: true });
|
|
}
|
|
}, 300);
|
|
|
|
// if the elevation didn't occured in 30 seconds we reject the promise
|
|
setTimeout(() => {
|
|
clearInterval(checkElevation);
|
|
reject(new Error('Elevation timeout'));
|
|
}, 30000);
|
|
});
|
|
}
|