text: // Copyright (c) 2017, Compiler Explorer Authors
  // 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.
  
  import child_process from 'child_process';
  import path from 'path';
  
  import fs from 'fs-extra';
  import Graceful from 'node-graceful';
  import treeKill from 'tree-kill';
  import _ from 'underscore';
  
  import {logger} from './logger';
  import {propsFor} from './properties';
  
  const execProps = propsFor('execution');
  
  function setupOnError(stream, name) {
      if (stream === undefined) return;
      stream.on('error', err => {
          logger.error(`Error with ${name} stream:`, err);
      });
  }
  
  export 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 = {...process.env, ...options.env};
  
      if (options.ldPath) {
          env.LD_LIBRARY_PATH = options.ldPath;
      }
  
      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});
      const startTime = process.hrtime.bigint();
  
      // 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 endTime = process.hrtime.bigint();
              const result = {
                  code,
                  okToCache,
                  filenameTransform,
                  stdout: streams.stdout,
                  stderr: streams.stderr,
                  execTime: ((endTime - startTime) / BigInt(1000000)).toString(),
              };
              logger.debug('Execution', {type: 'executed', command: command, args: args, result: result});
              resolve(result);
          });
          if (child.stdin) {
              if (options.input) child.stdin.write(options.input);
              child.stdin.end();
          }
      });
  }
  
  export function getNsJailCfgFilePath(configName) {
      const propKey = `nsjail.config.${configName}`;
      const configPath = execProps(propKey);
      if (configPath === undefined) {
          logger.error(`Could not find ${propKey}. Are you missing a definition?`);
          throw new Error(`Missing nsjail execution config property key ${propKey}`);
      }
      return configPath;
  }
  
  export function getFirejailProfileFilePath(profileName) {
      const propKey = `firejail.profile.${profileName}`;
      const profilePath = execProps(propKey);
      if (profilePath === undefined) {
          logger.error(`Could not find ${propKey}. Are you missing a definition?`);
          throw new Error(`Missing firejail execution profile property key ${propKey}`);
      }
      return profilePath;
  }
  
  export function getNsJailOptions(configName, command, args, options) {
      options = {...options};
      const jailingOptions = ['--config', getNsJailCfgFilePath(configName)];
  
      if (options.timeoutMs) {
          const ExtraWallClockLeewayMs = 1000;
          jailingOptions.push(`--time_limit=${Math.round((options.timeoutMs + ExtraWallClockLeewayMs) / 1000)}`);
      }
  
      const homeDir = '/app';
      let filenameTransform;
      if (options.customCwd) {
          let replacement = options.customCwd;
          if (options.appHome) {
              replacement = options.appHome;
              const relativeCwd = path.join(homeDir, path.relative(options.appHome, options.customCwd));
              jailingOptions.push('--cwd', relativeCwd, '--bindmount', `${options.appHome}:${homeDir}`);
          } else {
              jailingOptions.push('--cwd', homeDir, '--bindmount', `${options.customCwd}:${homeDir}`);
          }
  
          filenameTransform = opt => opt.replace(replacement, '/app');
          args = args.map(filenameTransform);
          delete options.customCwd;
      }
  
      const env = {...options.env, HOME: homeDir};
      if (options.ldPath) {
          jailingOptions.push(`--env=LD_LIBRARY_PATH=${options.ldPath}`);
          delete options.ldPath;
          delete env.LD_LIBRARY_PATH;
      }
  
      for (const [key, value] of Object.entries(env)) {
          if (value !== undefined) jailingOptions.push(`--env=${key}=${value}`);
      }
      delete options.env;
  
      return {
          args: jailingOptions.concat(['--', command]).concat(args),
          options,
          filenameTransform,
      };
  }
  
  export function getSandboxNsjailOptions(command, args, options) {
      // If we already had a custom cwd, use that.
      if (options.customCwd) {
          let relativeCommand = command;
          if (command.startsWith(options.customCwd)) {
              relativeCommand = path.relative(options.customCwd, command);
              if (path.dirname(relativeCommand) === '.') {
                  relativeCommand = `./${relativeCommand}`;
              }
          }
          return getNsJailOptions('sandbox', relativeCommand, args, options);
      }
  
      // Else, assume the executable should be run as `./exec` and run it from its directory.
      options = {...options, customCwd: path.dirname(command)};
      return getNsJailOptions('sandbox', `./${path.basename(command)}`, args, options);
  }
  
  function sandboxNsjail(command, args, options) {
      logger.info('Sandbox execution via nsjail', {command, args});
      const nsOpts = getSandboxNsjailOptions(command, args, options);
      return executeDirect(execProps('nsjail'), nsOpts.args, nsOpts.options, nsOpts.filenameTransform);
  }
  
  function executeNsjail(command, args, options) {
      const nsOpts = getNsJailOptions('execute', command, args, options);
      return executeDirect(execProps('nsjail'), nsOpts.args, nsOpts.options, nsOpts.filenameTransform);
  }
  
  function withFirejailTimeout(args, options) {
      if (options && options.timeoutMs) {
          // const ExtraWallClockLeewayMs = 1000;
          const ExtraCpuLeewayMs = 1500;
          return args.concat([`--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=' + getFirejailProfileFilePath('sandbox'),
          `--private=${execPath}`,
          '--private-cwd',
      ]);
  
      if (options.ldPath) {
          jailingOptions.push(`--env=LD_LIBRARY_PATH=${options.ldPath}`);
          delete options.ldPath;
      }
  
      for (const key of Object.keys(options.env || {})) {
          jailingOptions.push(`--env=${key}=${options.env[key]}`);
      }
      delete options.env;
  
      return executeDirect(execProps('firejail'), jailingOptions.concat([`./${execName}`]).concat(args), options);
  }
  
  const sandboxDispatchTable = {
      none: (command, args, options) => {
          logger.info('Sandbox execution (sandbox disabled)', {command, args});
          return executeDirect(command, args, options);
      },
      nsjail: sandboxNsjail,
      firejail: sandboxFirejail,
  };
  
  export async function sandbox(command, args, options) {
      const type = execProps('sandboxType', 'firejail');
      const dispatchEntry = sandboxDispatchTable[type];
      if (!dispatchEntry) throw new Error(`Bad sandbox type ${type}`);
      return dispatchEntry(command, args, options);
  }
  
  const wineSandboxName = 'ce-wineserver';
  // WINE takes a while to initialise and very often we don't need to run it at
  // all during startup. So, we do just the bare minimum at startup and then make
  // a promise that all subsequent WINE calls wait on.
  let wineInitPromise = null;
  
  export function startWineInit() {
      const wine = execProps('wine');
      if (!wine) {
          logger.info('WINE not configured');
          return;
      }
  
      const server = execProps('wineServer');
      const executionType = execProps('executionType', 'none');
      // We need to fire up a firejail wine server even in nsjail world (for now).
      const firejail = executionType === 'firejail' || executionType === 'nsjail' ? execProps('firejail') : null;
      const env = applyWineEnv({PATH: process.env.PATH});
      const prefix = env.WINEPREFIX;
  
      logger.info(`Initialising WINE in ${prefix}`);
  
      const asyncSetup = async () => {
          if (!(await fs.pathExists(prefix))) {
              logger.info(`Creating directory ${prefix}`);
              await fs.mkdir(prefix);
          }
  
          logger.info(`Killing any pre-existing wine-server`);
          let result = await child_process.exec(`${server} -k || true`, {env: env});
          logger.info(`Result: ${result}`);
          logger.info(`Waiting for any pre-existing server to stop...`);
          result = await child_process.exec(`${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=' + getFirejailProfileFilePath('wine'),
                      '--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();
              });
          });
      };
      wineInitPromise = asyncSetup();
  }
  
  function applyWineEnv(env) {
      return {
          ...env,
          // Force use of wine vcruntime (See change 45106c382)
          WINEDLLOVERRIDES: 'vcruntime140=b',
          WINEDEBUG: '-all',
          WINEPREFIX: execProps('winePrefix'),
      };
  }
  
  function needsWine(command) {
      return command.match(/\.exe$/i) && process.platform === 'linux' && !process.env.wsl;
  }
  
  async function executeWineDirect(command, args, options) {
      options = _.clone(options) || {};
      options.env = applyWineEnv(options.env);
      args = [command, ...args];
      await wineInitPromise;
      return executeDirect(execProps('wine'), args, options);
  }
  
  async function executeFirejail(command, args, options) {
      options = _.clone(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 = [command, ...args];
          command = execProps('wine');
          baseOptions.push('--profile=' + getFirejailProfileFilePath('wine'), `--join=${wineSandboxName}`);
          delete options.customCwd;
          baseOptions.push(command);
          await wineInitPromise;
          return executeDirect(firejail, baseOptions.concat(args), options);
      }
  
      logger.debug('Regular execution via firejail', {command, args});
      baseOptions.push('--profile=' + getFirejailProfileFilePath('execute'));
  
      if (options.ldPath) {
          baseOptions.push(`--env=LD_LIBRARY_PATH=${options.ldPath}`);
          delete options.ldPath;
      }
  
      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);
  }
  
  async function executeNone(command, args, options) {
      if (needsWine(command)) {
          return executeWineDirect(command, args, options);
      }
      return executeDirect(command, args, options);
  }
  
  const executeDispatchTable = {
      none: executeNone,
      firejail: executeFirejail,
      nsjail: (command, args, options) =>
          needsWine(command) ? executeFirejail(command, args, options) : executeNsjail(command, args, options),
  };
  
  export async function execute(command, args, options) {
      const type = execProps('executionType', 'none');
      const dispatchEntry = executeDispatchTable[type];
      if (!dispatchEntry) throw new Error(`Bad sandbox type ${type}`);
      return dispatchEntry(command, args, options);
  }
  