@@ -3,60 +3,76 @@ import { net } from 'appium/support';
33import { promisify } from 'node:util' ;
44import { exec } from 'node:child_process' ;
55import B from 'bluebird' ;
6+ import { log } from './logger' ;
67
78const execAsync = promisify ( exec ) ;
89
910/**
1011 * This API triggers UAC when necessary
11- * unlike the 'spawn' call used by teen_process's exec.
12- * See https://github.com/nodejs/node-v0.x-archive/issues/6797
1312 *
1413 * @param {string } cmd
1514 * @param {string[] } args
1615 * @param {import('node:child_process').ExecOptions & {timeoutMs?: number} } opts
1716 * @returns {Promise<{stdout: string; stderr: string;}> }
1817 * @throws {import('node:child_process').ExecException }
18+ *
19+ * Notes:
20+ * - If the UAC prompt is cancelled by the user, Start-Process returns a non-zero exit code.
1921 */
20- export async function shellExec ( cmd , args = [ ] , opts = { } ) {
22+ export async function runElevated ( cmd , args = [ ] , opts = { } ) {
2123 const {
2224 timeoutMs = 60 * 1000 * 5
2325 } = opts ;
24- const fullCmd = [ cmd , ...args ] . map ( escapeWindowsArg ) . join ( ' ' ) ;
25- const { stdout, stderr } = await B . resolve ( execAsync ( fullCmd , opts ) )
26- . timeout ( timeoutMs , `The command '${ fullCmd } ' timed out after ${ timeoutMs } ms` ) ;
26+
27+ const psFilePath = escapePSSingleQuoted ( cmd ) ;
28+ const psArgList = buildPSArgumentList ( args ) ;
29+ // Build the PowerShell Start-Process command (safe quoting for inner tokens)
30+ const psCommand = `Start-Process -FilePath ${ psFilePath } -ArgumentList ${ psArgList } -Verb RunAs` ;
31+ // Wrap the PowerShell command in double-quotes for the outer shell call.
32+ // We avoid additional interpolation here by using only single-quoted literals inside the PS command.
33+ const fullCmd = `powershell -NoProfile -Command "${ psCommand } "` ;
34+ log . debug ( `Executing command: ${ fullCmd } ` ) ;
35+ const { stdout, stderr } = /** @type {any } */ (
36+ await B . resolve ( execAsync ( fullCmd , opts ) )
37+ . timeout ( timeoutMs , `The command '${ fullCmd } ' timed out after ${ timeoutMs } ms` )
38+ ) ;
2739 return {
2840 stdout : _ . isString ( stdout ) ? stdout : stdout . toString ( ) ,
2941 stderr : _ . isString ( stderr ) ? stderr : stderr . toString ( ) ,
3042 } ;
3143}
3244
3345/**
34- * Escapes a string to be used as a Windows command line argument
3546 *
36- * @param {string } arg
37- * @returns {string }
47+ * @param {string } srcUrl
48+ * @param {string } dstPath
49+ * @returns {Promise<void> }
3850 */
39- function escapeWindowsArg ( arg ) {
40- if ( ! arg ) {
41- return '""' ;
42- }
43-
44- const needsQuotes = / [ \s " ] / g. test ( arg ) ;
45- if ( ! needsQuotes ) {
46- return arg ;
47- }
51+ export async function downloadToFile ( srcUrl , dstPath ) {
52+ await net . downloadFile ( srcUrl , dstPath ) ;
53+ }
4854
49- // Escape double quotes and backslashes before quotes
50- const escaped = arg . replace ( / ( \\ * ) " / g, '$1$1\\"' ) . replace ( / ( \\ + ) $ / , '$1$1' ) ;
51- return `"${ escaped } "` ;
55+ /**
56+ * Escapes a string to be used as a PowerShell single-quoted string
57+ *
58+ * @param {string } str
59+ * @returns {string }
60+ */
61+ function escapePSSingleQuoted ( str ) {
62+ return `'${ String ( str ) . replace ( / ' / g, "''" ) } '` ;
5263}
5364
5465/**
66+ * Build the PowerShell -ArgumentList value from an array of args.
67+ * Produces a comma-separated list of single-quoted entries:
68+ * 'arg1','arg2','arg with spaces'
5569 *
56- * @param {string } srcUrl
57- * @param {string } dstPath
58- * @returns {Promise<void> }
70+ * @param {string[] } args
71+ * @returns {string }
5972 */
60- export async function downloadToFile ( srcUrl , dstPath ) {
61- await net . downloadFile ( srcUrl , dstPath ) ;
73+ function buildPSArgumentList ( args ) {
74+ if ( ! Array . isArray ( args ) || args . length === 0 ) {
75+ return "''" ; // Pass empty string argument list
76+ }
77+ return args . map ( escapePSSingleQuoted ) . join ( ',' ) ;
6278}
0 commit comments