|  | // Copyright (c) 2017, Matt Godbolt | 
|  | // All rights reserved. | 
|  | // | 
|  | // Redistribution and use in source and binary forms, with or without | 
|  | // modification, are permitted provided that the following conditions are met: | 
|  | // | 
|  | //     * Redistributions of source code must retain the above copyright notice, | 
|  | //       this list of conditions and the following disclaimer. | 
|  | //     * Redistributions in binary form must reproduce the above copyright | 
|  | //       notice, this list of conditions and the following disclaimer in the | 
|  | //       documentation and/or other materials provided with the distribution. | 
|  | // | 
|  | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | 
|  | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 
|  | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | 
|  | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | 
|  | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | 
|  | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | 
|  | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | 
|  | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | 
|  | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | 
|  | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | 
|  | // POSSIBILITY OF SUCH DAMAGE. | 
|  |  | 
|  | const child_process = require('child_process'), | 
|  | path = require('path'), | 
|  | fs = require('fs'), | 
|  | logger = require('./logger').logger, | 
|  | treeKill = require('tree-kill'), | 
|  | execProps = require('./properties').propsFor('execution'), | 
|  | Graceful = require('node-graceful'); | 
|  |  | 
|  | function setupOnError(stream, name) { | 
|  | if (stream === undefined) return; | 
|  | stream.on('error', err => { | 
|  | logger.error(`Error with ${name} stream:`, err); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function executeDirect(command, args, options, filenameTransform) { | 
|  | // filename transform is expected to have been pre-applied by the caller. | 
|  | // it is passed through here only so clients can see it in the result. | 
|  | filenameTransform = filenameTransform || (x => x); | 
|  | options = options || {}; | 
|  | const maxOutput = options.maxOutput || 1024 * 1024; | 
|  | const timeoutMs = options.timeoutMs || 0; | 
|  | const env = options.env; | 
|  |  | 
|  | if (options.wrapper) { | 
|  | args = args.slice(0); // prevent mutating the caller's arguments | 
|  | args.unshift(command); | 
|  | command = options.wrapper; | 
|  |  | 
|  | if (command.startsWith('./')) command = path.join(process.cwd(), command); | 
|  | } | 
|  |  | 
|  | let okToCache = true; | 
|  | const cwd = options.customCwd ? options.customCwd : ( | 
|  | (command.startsWith("/mnt") && process.env.wsl) ? process.env.winTmp : process.env.tmpDir | 
|  | ); | 
|  | logger.debug("Execution", {type: "executing", command: command, args: args, env: env, cwd: cwd}); | 
|  | // AP: Run Windows-volume executables in winTmp. Otherwise, run in tmpDir (which may be undefined). | 
|  | // https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options | 
|  | const child = child_process.spawn(command, args, { | 
|  | cwd: cwd, | 
|  | env: env, | 
|  | detached: process.platform === 'linux' | 
|  | }); | 
|  | let running = true; | 
|  |  | 
|  | const kill = options.killChild || (() => { | 
|  | if (running) treeKill(child.pid); | 
|  | }); | 
|  |  | 
|  | const streams = { | 
|  | stderr: "", | 
|  | stdout: "", | 
|  | truncated: false | 
|  | }; | 
|  | let timeout; | 
|  | if (timeoutMs) timeout = setTimeout(() => { | 
|  | logger.warn(`Timeout for ${command} ${args} after ${timeoutMs}ms`); | 
|  | okToCache = false; | 
|  | kill(); | 
|  | streams.stderr += "\nKilled - processing time exceeded"; | 
|  | }, timeoutMs); | 
|  |  | 
|  | function setupStream(stream, name) { | 
|  | if (stream === undefined) return; | 
|  | stream.on('data', data => { | 
|  | if (streams.truncated) return; | 
|  | const newLength = (streams[name].length + data.length); | 
|  | if ((maxOutput > 0) && (newLength > maxOutput)) { | 
|  | streams[name] = streams[name] + data.slice(0, maxOutput - streams[name].length); | 
|  | streams[name] += "\n[Truncated]"; | 
|  | streams.truncated = true; | 
|  | kill(); | 
|  | return; | 
|  | } | 
|  | streams[name] += data; | 
|  | }); | 
|  | setupOnError(stream, name); | 
|  | } | 
|  |  | 
|  | setupOnError(child.stdin, 'stdin'); | 
|  | setupStream(child.stdout, 'stdout'); | 
|  | setupStream(child.stderr, 'stderr'); | 
|  | child.on('exit', code => { | 
|  | logger.debug("Execution", {type: 'exited', code: code}); | 
|  | if (timeout !== undefined) clearTimeout(timeout); | 
|  | running = false; | 
|  | }); | 
|  | return new Promise((resolve, reject) => { | 
|  | child.on('error', e => { | 
|  | logger.debug(`Execution error with ${command} args: ${args}:`, e); | 
|  | reject(e); | 
|  | }); | 
|  | child.on('close', code => { | 
|  | // Being killed externally gives a NULL error code. Synthesize something different here. | 
|  | if (code === null) code = -1; | 
|  | if (timeout !== undefined) clearTimeout(timeout); | 
|  | const result = { | 
|  | code, | 
|  | okToCache, | 
|  | filenameTransform, | 
|  | stdout: streams.stdout, | 
|  | stderr: streams.stderr | 
|  | }; | 
|  | logger.debug("Execution", {type: "executed", command: command, args: args, result: result}); | 
|  | resolve(result); | 
|  | }); | 
|  | if (options.input) child.stdin.write(options.input); | 
|  | child.stdin.end(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | // function msToFjTimeout(ms) { | 
|  | //     const totalSecs = Math.round(ms / 1000); | 
|  | //     return `${Math.floor(totalSecs / (60 * 60))}:${Math.floor(totalSecs / 60)}:${totalSecs % 60}`; | 
|  | // } | 
|  |  | 
|  | function withFirejailTimeout(args, options) { | 
|  | if (options && options.timeoutMs) { | 
|  | // const ExtraWallClockLeewayMs = 1000; | 
|  | const ExtraCpuLeewayMs = 1500; | 
|  | return args.concat([ | 
|  | // TODO: reinstate once we work out why this causes a 1s+ delay on every execution! | 
|  | // `--timeout=${msToFjTimeout(options.timeoutMs + ExtraWallClockLeewayMs)}`, | 
|  | `--rlimit-cpu=${Math.round((options.timeoutMs + ExtraCpuLeewayMs) / 1000)}` | 
|  | ]); | 
|  | } | 
|  | return args; | 
|  | } | 
|  |  | 
|  | function sandboxFirejail(command, args, options) { | 
|  | logger.info("Sandbox execution via firejail", {command, args}); | 
|  | const execPath = path.dirname(command); | 
|  | const execName = path.basename(command); | 
|  | const jailingOptions = withFirejailTimeout([ | 
|  | '--quiet', | 
|  | '--deterministic-exit-code', | 
|  | '--terminate-orphans', | 
|  | '--profile=etc/firejail/sandbox.profile', | 
|  | `--private=${execPath}`, | 
|  | '--private-cwd']) | 
|  | .concat([`./${execName}`]) | 
|  | .concat(args); | 
|  | command = 'firejail'; | 
|  | args = jailingOptions; | 
|  | return executeDirect(command, args, options); | 
|  | } | 
|  |  | 
|  | const sandboxDispatchTable = { | 
|  | none: (command, args, options) => { | 
|  | logger.info("Sandbox execution (sandbox disabled)", {command, args}); | 
|  | return executeDirect(command, args, options); | 
|  | }, | 
|  | firejail: sandboxFirejail | 
|  | }; | 
|  |  | 
|  | function sandbox(command, args, options) { | 
|  | const type = execProps("sandboxType", "firejail"); | 
|  | const dispatchEntry = sandboxDispatchTable[type]; | 
|  | if (!dispatchEntry) | 
|  | return Promise.reject(`Bad sandbox type ${type}`); | 
|  | return dispatchEntry(command, args, options); | 
|  | } | 
|  |  | 
|  | const wineSandboxName = "ce-wineserver"; | 
|  |  | 
|  | function initialiseWine() { | 
|  | const wine = execProps("wine"); | 
|  | if (!wine) { | 
|  | logger.info("WINE not configured"); | 
|  | return Promise.resolve(); | 
|  | } | 
|  |  | 
|  | const server = execProps("wineServer"); | 
|  | const firejail = execProps("executionType", "none") === "firejail" ? execProps("firejail") : null; | 
|  | const env = applyWineEnv({PATH: process.env.PATH}); | 
|  | const prefix = env.WINEPREFIX; | 
|  |  | 
|  | logger.info(`Initialising WINE in ${prefix}`); | 
|  | if (!fs.existsSync(prefix) || !fs.statSync(prefix).isDirectory()) { | 
|  | logger.info(`Creating directory ${prefix}`); | 
|  | fs.mkdirSync(prefix); | 
|  | } | 
|  |  | 
|  | logger.info(`Killing any pre-existing wine-server`); | 
|  | let result = child_process.execSync(`${server} -k || true`, {env: env}); | 
|  | logger.info(`Result: ${result}`); | 
|  | logger.info(`Waiting for any pre-existing server to stop...`); | 
|  | result = child_process.execSync(`${server} -w`, {env: env}); | 
|  | logger.info(`Result: ${result}`); | 
|  |  | 
|  | // We run a long-lived cmd process, to: | 
|  | // * test that WINE works | 
|  | // * be something which holds open a working firejail sandbox | 
|  | // All future WINE compiles go through the same sandbox. | 
|  | // We wait until the process has printed out some known good text, but don't wait | 
|  | // for it to exit (it won't, on purpose). | 
|  |  | 
|  | let wineServer; | 
|  | if (firejail) { | 
|  | logger.info(`Starting a new, firejailed, long-lived wineserver complex`); | 
|  | wineServer = child_process.spawn( | 
|  | firejail, | 
|  | [ | 
|  | "--quiet", | 
|  | "--profile=etc/firejail/wine.profile", | 
|  | "--private", | 
|  | `--name=${wineSandboxName}`, | 
|  | wine, | 
|  | "cmd" | 
|  | ], | 
|  | {env: env, detached: true}); | 
|  | logger.info(`firejailed pid=${wineServer.pid}`); | 
|  | } else { | 
|  | logger.info(`Starting a new, long-lived wineserver complex ${server}`); | 
|  | wineServer = child_process.spawn( | 
|  | wine, | 
|  | ["cmd"], | 
|  | {env: env, detached: true}); | 
|  | logger.info(`wineserver pid=${wineServer.pid}`); | 
|  | } | 
|  |  | 
|  | wineServer.on('close', code => { | 
|  | logger.info(`WINE server complex exited with code ${code}`); | 
|  | }); | 
|  |  | 
|  | Graceful.on('exit', () => { | 
|  | const waitingPromises = []; | 
|  |  | 
|  | function waitForExit(process, name) { | 
|  | return new Promise((resolve) => { | 
|  | process.on('close', () => { | 
|  | logger.info(`Process '${name}' closed`); | 
|  | resolve(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | if (wineServer && !wineServer.killed) { | 
|  | logger.info('Shutting down WINE server complex'); | 
|  | wineServer.kill(); | 
|  | if (wineServer.killed) { | 
|  | waitingPromises.push(waitForExit(wineServer, "WINE server")); | 
|  | } | 
|  | wineServer = null; | 
|  | } | 
|  | return Promise.all(waitingPromises); | 
|  | }); | 
|  |  | 
|  | return new Promise((resolve, reject) => { | 
|  | setupOnError(wineServer.stdin, "stdin"); | 
|  | setupOnError(wineServer.stdout, "stdout"); | 
|  | setupOnError(wineServer.stderr, "stderr"); | 
|  | const magicString = "!!EVERYTHING IS WORKING!!"; | 
|  | wineServer.stdin.write(`echo ${magicString}`); | 
|  |  | 
|  | let output = ""; | 
|  | wineServer.stdout.on('data', data => { | 
|  | logger.info(`Output from wine server complex: ${data.toString().trim()}`); | 
|  | output += data; | 
|  | if (output.includes(magicString)) { | 
|  | resolve(); | 
|  | } | 
|  | }); | 
|  | wineServer.stderr.on('data', | 
|  | data => logger.info(`stderr output from wine server complex: ${data.toString().trim()}`)); | 
|  | wineServer.on('error', e => { | 
|  | logger.error(`WINE server complex exited with error ${e}`); | 
|  | reject(e); | 
|  | }); | 
|  | wineServer.on('close', code => { | 
|  | logger.info(`WINE server complex exited with code ${code}`); | 
|  | reject(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function applyWineEnv(env) { | 
|  | const prefix = execProps("winePrefix"); | 
|  | env = env || {}; | 
|  | // Force use of wine vcruntime (See change 45106c382) | 
|  | env.WINEDLLOVERRIDES = "vcruntime140=b"; | 
|  | env.WINEDEBUG = "-all"; | 
|  | env.WINEPREFIX = prefix; | 
|  | return env; | 
|  | } | 
|  |  | 
|  | function needsWine(command) { | 
|  | return command.match(/\.exe$/i) && process.platform === 'linux'; | 
|  | } | 
|  |  | 
|  | function executeWineDirect(command, args, options) { | 
|  | options = options || {}; | 
|  | options.env = applyWineEnv(options.env); | 
|  | args.unshift(command); | 
|  | return executeDirect(execProps("wine"), args, options); | 
|  | } | 
|  |  | 
|  | function executeFirejail(command, args, options) { | 
|  | options = options || {}; | 
|  | const firejail = execProps("firejail"); | 
|  | const baseOptions = withFirejailTimeout(['--quiet', '--deterministic-exit-code', '--terminate-orphans'], options); | 
|  | if (needsWine(command)) { | 
|  | logger.debug("WINE execution via firejail", {command, args}); | 
|  | options.env = applyWineEnv(options.env); | 
|  | args.unshift(command); | 
|  | command = execProps("wine"); | 
|  | baseOptions.push('--profile=etc/firejail/wine.profile'); | 
|  | baseOptions.push(`--join=${wineSandboxName}`); | 
|  | delete options.customCwd; | 
|  | baseOptions.push(command); | 
|  | return executeDirect(firejail, baseOptions.concat(args), options); | 
|  | } | 
|  |  | 
|  | logger.debug("Regular execution via firejail", {command, args}); | 
|  | baseOptions.push('--profile=etc/firejail/execute.profile'); | 
|  | let filenameTransform; | 
|  | if (options.customCwd) { | 
|  | baseOptions.push(`--private=${options.customCwd}`); | 
|  | const replacement = options.customCwd; | 
|  | filenameTransform = opt => opt.replace(replacement, "."); | 
|  | args = args.map(filenameTransform); | 
|  | delete options.customCwd; | 
|  | // TODO: once it's supported properly in our patched firejail, make this option common to both customCwd and | 
|  | // non-customCwd code paths. | 
|  | baseOptions.push('--private-cwd'); | 
|  | } else { | 
|  | baseOptions.push('--private'); | 
|  | } | 
|  | baseOptions.push(command); | 
|  | return executeDirect(firejail, baseOptions.concat(args), options, filenameTransform); | 
|  | } | 
|  |  | 
|  | function executeNone(command, args, options) { | 
|  | if (needsWine(command)) { | 
|  | return executeWineDirect(command, args, options); | 
|  | } | 
|  | return executeDirect(command, args, options); | 
|  | } | 
|  |  | 
|  | const executeDispatchTable = { | 
|  | none: executeNone, | 
|  | firejail: executeFirejail | 
|  | }; | 
|  |  | 
|  | function execute(command, args, options) { | 
|  | const type = execProps("executionType", "none"); | 
|  | const dispatchEntry = executeDispatchTable[type]; | 
|  | if (!dispatchEntry) | 
|  | return Promise.reject(`Bad sandbox type ${type}`); | 
|  | return dispatchEntry(command, args, options); | 
|  | } | 
|  |  | 
|  | module.exports = { | 
|  | execute, | 
|  | sandbox, | 
|  | initialiseWine | 
|  | }; |