71 lines
4.3 KiB
JavaScript
71 lines
4.3 KiB
JavaScript
'use strict'
|
|
// Explicit preload for synchronous subprocess capture in Windows AppContainers.
|
|
// Every subprocess still uses Node's own spawn and inherits the existing token/Job.
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const cp = require('node:child_process')
|
|
const { syncBuiltinESMExports } = require('node:module')
|
|
const originals = { spawnSync: cp.spawnSync, execFileSync: cp.execFileSync, execSync: cp.execSync }
|
|
const maximum = 16 * 1024 * 1024
|
|
function captured(method, file, args, options, spawn) {
|
|
if (options.stdio !== undefined && options.stdio !== 'pipe') return method()
|
|
const limit = options.maxBuffer === undefined ? 1024 * 1024 : options.maxBuffer
|
|
if (!Number.isSafeInteger(limit) || limit < 0 || limit > maximum) throw new RangeError('Sandbox process adapter maxBuffer must be between 0 and 16 MiB')
|
|
const input = options.input === undefined ? Buffer.alloc(0) : typeof options.input === 'string'
|
|
? Buffer.from(options.input, options.encoding && options.encoding !== 'buffer' ? options.encoding : 'utf8')
|
|
: ArrayBuffer.isView(options.input) ? Buffer.from(options.input.buffer, options.input.byteOffset, options.input.byteLength) : null
|
|
if (!input) throw new TypeError('Sandbox process adapter input must be a string, Buffer or ArrayBuffer view')
|
|
if (input.length > maximum) throw new RangeError('Sandbox process adapter input exceeds 16 MiB')
|
|
const directory = fs.mkdtempSync(path.join(process.env.TEMP, 'ycode-process-'))
|
|
const files = ['stdin', 'stdout', 'stderr'].map(name => path.join(directory, name)), handles = []
|
|
const encode = bytes => options.encoding && options.encoding !== 'buffer' ? bytes.toString(options.encoding) : bytes
|
|
let answer, failure
|
|
try {
|
|
fs.writeFileSync(files[0], input, { flag: 'wx', mode: 0o600 })
|
|
handles.push(fs.openSync(files[0], 'r'))
|
|
handles.push(fs.openSync(files[1], 'wx+'))
|
|
handles.push(fs.openSync(files[2], 'wx+'))
|
|
const forwarded = { ...options, stdio: handles }; delete forwarded.input
|
|
try { answer = method(forwarded) } catch (error) { failure = error }
|
|
const read = index => {
|
|
const buffer = Buffer.alloc(Math.min(fs.fstatSync(handles[index]).size, limit + 1))
|
|
let length = 0
|
|
while (length < buffer.length) { const count = fs.readSync(handles[index], buffer, length, buffer.length - length, length); if (!count) break; length += count }
|
|
return { bytes: buffer.subarray(0, Math.min(length, limit)), overflow: length > limit }
|
|
}
|
|
const out = read(1), err = read(2), stdout = encode(out.bytes), stderr = encode(err.bytes)
|
|
if (out.overflow || err.overflow) failure = Object.assign(new Error('Sandbox subprocess output exceeds maxBuffer'), { code: 'ENOBUFS', syscall: 'spawnSync', path: file, spawnargs: args })
|
|
if (spawn) {
|
|
if (failure && !answer) throw failure
|
|
return { ...answer, stdout, stderr, output: [null, stdout, stderr], ...(failure ? { error: failure } : {}) }
|
|
}
|
|
if (failure) {
|
|
failure.stdout = stdout; failure.stderr = stderr; failure.output = [null, stdout, stderr]
|
|
throw failure
|
|
}
|
|
return stdout
|
|
} finally {
|
|
for (const handle of handles) try { fs.closeSync(handle) } catch {}
|
|
for (const file of files) try { fs.unlinkSync(file) } catch {}
|
|
try { fs.rmdirSync(directory) } catch {}
|
|
}
|
|
}
|
|
function argumentsOf(args, options) {
|
|
return Array.isArray(args) ? { args, options: options || {} } : { args: [], options: args == null ? options || {} : args }
|
|
}
|
|
function spawnSync(file, args, options) {
|
|
const normalized = argumentsOf(args, options)
|
|
return captured(forwarded => originals.spawnSync(file, normalized.args, forwarded || normalized.options), file, normalized.args, normalized.options, true)
|
|
}
|
|
function execFileSync(file, args, options) {
|
|
const normalized = argumentsOf(args, options)
|
|
return captured(forwarded => originals.execFileSync(file, normalized.args, forwarded || normalized.options), file, normalized.args, normalized.options, false)
|
|
}
|
|
function execSync(command, options = {}) {
|
|
return captured(forwarded => originals.execSync(command, forwarded || options), command, [], options, false)
|
|
}
|
|
if (process.env.YCODE_SANDBOX !== 'appcontainer') throw new Error('The subprocess adapter is for the isolated YCode Node runtime')
|
|
Object.assign(cp, { spawnSync, execFileSync, execSync })
|
|
syncBuiltinESMExports()
|
|
module.exports = { spawnSync, execFileSync, execSync }
|