From fdf8820b5af975a8092f0ce8c85942b907a040a5 Mon Sep 17 00:00:00 2001 From: Edwin Joassart Date: Thu, 17 Jul 2025 17:50:15 +0200 Subject: [PATCH] patch: refactor permission code for windows --- lib/shared/permissions.ts | 26 +----------------- lib/shared/sudo/windows.ts | 55 ++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/lib/shared/permissions.ts b/lib/shared/permissions.ts index 1c11a733..40c6fff5 100755 --- a/lib/shared/permissions.ts +++ b/lib/shared/permissions.ts @@ -102,31 +102,7 @@ export async function elevateCommand( return { cancelled: false }; } - const isWindows = os.platform() === 'win32'; - - // Augment the command to pass the environment variables as args - // Powershell (required to ask for elevated privileges) as of win10 cannot pass environment variables as a map, so we pass them as args - // For the sake of consistency, we do the same for Linux and macOS, but it's likely not required - // Once we deprecate win10 we can move to a more elegant solution (passing the env to powershell) - // const envFilter: string[] = [ - // 'ETCHER_SERVER_ADDRESS', - // 'ETCHER_SERVER_PORT', - // 'ETCHER_SERVER_ID', - // 'ETCHER_NO_SPAWN_UTIL', - // 'ETCHER_TERMINATE_TIMEOUT', - // 'UV_THREADPOOL_SIZE', - // ]; - - // const preparedCmd = [ - // command[0], - // ...command.slice(1), - // ...Object.keys(options.env) - // .filter((key) => Object.prototype.hasOwnProperty.call(options.env, key)) - // .filter((key) => envFilter.includes(key)) - // .map((key) => `--${key}=${options.env[key]}`), - // ]; - - if (isWindows) { + if (os.platform() === 'win32') { return elevateScriptWindows(command, options.env); } if ( diff --git a/lib/shared/sudo/windows.ts b/lib/shared/sudo/windows.ts index 13fc2a73..122c9c8e 100644 --- a/lib/shared/sudo/windows.ts +++ b/lib/shared/sudo/windows.ts @@ -1,11 +1,6 @@ -/* - * This is heavily inspired (read: a ripof) https://github.com/balena-io-modules/sudo-prompt - * Which was a fork of https://github.com/jorangreef/sudo-prompt +/* * * - * This and the original code was released under The MIT License (MIT) - * - * Copyright (c) 2015 Joran Dirk Greef - * Copyright (c) 2024 Balena + * Copyright (c) 2025 Balena * The MIT License (MIT) @@ -35,7 +30,29 @@ export async function sudo( env: any, ): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { try { - // WindowsWriteCommandScript(instance, end) + // Augment the command to pass the environment variables as args + + // Powershell (required to ask for elevated privileges) as of win10 + // cannot pass environment variables as a map, so we pass them as args + // this is a workaround as we can't use an equivalent of `sudo -E` on Windows + + const envFilter: string[] = [ + 'ETCHER_SERVER_ADDRESS', + 'ETCHER_SERVER_PORT', + 'ETCHER_SERVER_ID', + 'ETCHER_NO_SPAWN_UTIL', + 'ETCHER_TERMINATE_TIMEOUT', + 'UV_THREADPOOL_SIZE', + ]; + + const preparedCmd = [ + command[0], + ...command.slice(1), + ...Object.keys(env) + .filter((key) => Object.prototype.hasOwnProperty.call(env, key)) + .filter((key) => envFilter.includes(key)) + .map((key) => `--${key}=${env[key]}`), + ]; const spawnCommand = []; spawnCommand.push('Start-Process'); @@ -45,12 +62,13 @@ export async function sudo( // Escape characters for PowerShell using single quotes: // Escape single quotes for PowerShell using backtick: // See: https://ss64.com/ps/syntax-esc.html - spawnCommand.push(`'${command[0].replace(/'/g, "`'")}'`); + spawnCommand.push(`'${preparedCmd[0].replace(/'/g, "`'")}'`); + spawnCommand.push('-ArgumentList'); // Join and escape arguments for PowerShell spawnCommand.push( - `'${command + `'${preparedCmd .slice(1) .map((a) => a.replace(/'/g, "`'")) .join(' ')}'`, @@ -58,18 +76,27 @@ export async function sudo( spawnCommand.push('-WindowStyle hidden'); spawnCommand.push('-Verb runAs'); - const child = spawn('powershell.exe', spawnCommand, { - env, - }); + const child = spawn('powershell.exe', spawnCommand); let result = { status: 'waiting' }; + // child.stdout.on('data', (data) => { + // console.log(`stdout: ${data}`); + // }); + + // child.stderr.on('data', (data) => { + // console.error(`stderr: ${data}`); + // result = { status: data.toString() }; + // }); + child.on('close', (code) => { if (code === 0) { // User accepted UAC, process started + console.log('UAC accepted, process started'); result = { status: 'granted' }; } else { // User cancelled or error occurred + console.log('UAC cancelled or error occurred'); result = { status: 'cancelled' }; } }); @@ -80,7 +107,7 @@ export async function sudo( // we don't spawn directly in the promise otherwise resolving stop the process return new Promise((resolve, reject) => { - setTimeout(() => { + setInterval(() => { if (result.status === 'waiting') { // Still waiting for user input, don't resolve or reject yet return;