windows-shell.mjs 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. import { spawn } from 'node:child_process'
  2. import { resolve as resolvePath } from 'node:path'
  3. export function powershellArguments(command) {
  4. const script = `$ProgressPreference = 'SilentlyContinue'\n[Console]::InputEncoding = [Console]::OutputEncoding = $OutputEncoding = New-Object System.Text.UTF8Encoding($false)\n$global:LASTEXITCODE = 0\ntry {\n& {\n${command}\n}\n$ok = $?\nif ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\nif (-not $ok) { exit 1 }\n} catch { [Console]::Error.WriteLine($_.ToString()); exit 1 }`
  5. return ['-NoLogo', '-NoProfile', '-NonInteractive', '-OutputFormat', 'Text', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')]
  6. }
  7. /** Bound both output streams, and terminate the owned process tree before
  8. * resolving a timeout or authorization cancellation. Never spawn a shell. */
  9. export function windowsShell(shell, command, { cwd, timeout, signal, helper = resolvePath(import.meta.dirname, '../.desktop-build/computer-helper-dev.exe'), limit = 512 * 1024 }) {
  10. return new Promise(resolve => {
  11. let child, timer, timedOut = false, cancelled = false, finished = false
  12. const output = { stdout: [], stderr: [] }, sizes = { stdout: 0, stderr: 0 }
  13. const capture = (key, bytes) => {
  14. const kept = bytes.subarray(0, Math.max(0, limit - sizes[key]))
  15. if (kept.length) output[key].push(kept)
  16. sizes[key] += kept.length
  17. }
  18. const finish = async (code, error) => {
  19. if (finished) return
  20. finished = true; clearTimeout(timer); signal?.removeEventListener('abort', abort)
  21. if (error) capture('stderr', Buffer.from(error.message))
  22. resolve({ command, exitCode: timedOut || cancelled || error ? 1 : code ?? 1, timedOut,
  23. ...(cancelled ? { cancelled: true } : {}),
  24. stdout: Buffer.concat(output.stdout).toString('utf8'), stderr: Buffer.concat(output.stderr).toString('utf8') })
  25. }
  26. const stop = () => {
  27. // The native helper owns a KILL_ON_JOB_CLOSE job. Terminating the helper
  28. // closes that kernel handle and the entire owned PowerShell process tree.
  29. if (!finished && child?.pid) child.kill()
  30. }
  31. const abort = () => { cancelled = true; stop() }
  32. if (signal?.aborted) { cancelled = true; void finish(1); return }
  33. try {
  34. child = spawn(helper, ['--shell'], { cwd, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, TERM: 'dumb' } })
  35. child.stdin.on('error', () => {})
  36. child.stdin.end(JSON.stringify({ encoded: powershellArguments(command).at(-1), cwd }))
  37. child.stdout.on('data', bytes => capture('stdout', bytes))
  38. child.stderr.on('data', bytes => capture('stderr', bytes))
  39. child.once('error', error => { void finish(1, error) })
  40. child.once('close', code => { void finish(code) })
  41. signal?.addEventListener('abort', abort, { once: true })
  42. timer = setTimeout(() => { timedOut = true; stop() }, timeout)
  43. } catch (error) { void finish(1, error) }
  44. })
  45. }