diff --git a/lib/exec.ts b/lib/exec.ts index 7626b0ac1..dce5f8589 100644 --- a/lib/exec.ts +++ b/lib/exec.ts @@ -36,18 +36,19 @@ import type {FilenameTransformFunc, UnprocessedExecResult} from '../types/execut import {logger} from './logger.js'; import {propsFor} from './properties.js'; import {Graceful} from './node-graceful.js'; -import {unwrapString} from './assert.js'; +import {unwrapString, unwrap, assert} from './assert.js'; import os from 'os'; +import {Stream} from 'stream'; type NsJailOptions = { args: string[]; options: ExecutionOptions; - filenameTransform: FilenameTransformFunc; + filenameTransform: FilenameTransformFunc | undefined; }; const execProps = propsFor('execution'); -function setupOnError(stream, name) { +function setupOnError(stream: Stream, name: string) { if (stream === undefined) return; stream.on('error', err => { logger.error(`Error with ${name} stream:`, err); @@ -116,7 +117,7 @@ export function executeDirect( streams.stderr += '\nKilled - processing time exceeded\n'; }, timeoutMs); - function setupStream(stream, name) { + function setupStream(stream: Stream, name: string) { if (stream === undefined) return; stream.on('data', data => { if (streams.truncated) return; @@ -222,7 +223,7 @@ export function getNsJailOptions( } const homeDir = '/app'; - let filenameTransform; + let filenameTransform: FilenameTransformFunc | undefined; if (options.customCwd) { let replacement = options.customCwd; if (options.appHome) { @@ -314,28 +315,28 @@ export function getExecuteCEWrapperOptions(command: string, args: string[], opti return getCeWrapperOptions('execute', command, args, options); } -function sandboxNsjail(command, args, options) { +function sandboxNsjail(command: string, args: string[], options: ExecutionOptions) { 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) { +function executeNsjail(command: string, args: string[], options: ExecutionOptions) { const nsOpts = getNsJailOptions('execute', command, args, options); return executeDirect(execProps('nsjail'), nsOpts.args, nsOpts.options, nsOpts.filenameTransform); } -function sandboxCEWrapper(command, args, options) { +function sandboxCEWrapper(command: string, args: string[], options: ExecutionOptions) { const nsOpts = getSandboxCEWrapperOptions(command, args, options); return executeDirect(execProps('cewrapper'), nsOpts.args, nsOpts.options, nsOpts.filenameTransform); } -function executeCEWrapper(command, args, options) { +function executeCEWrapper(command: string, args: string[], options: ExecutionOptions) { const nsOpts = getExecuteCEWrapperOptions(command, args, options); return executeDirect(execProps('cewrapper'), nsOpts.args, nsOpts.options, nsOpts.filenameTransform); } -function withFirejailTimeout(args: string[], options?) { +function withFirejailTimeout(args: string[], options?: ExecutionOptions) { if (options && options.timeoutMs) { // const ExtraWallClockLeewayMs = 1000; const ExtraCpuLeewayMs = 1500; @@ -344,7 +345,7 @@ function withFirejailTimeout(args: string[], options?) { return args; } -function sandboxFirejail(command: string, args: string[], options) { +function sandboxFirejail(command: string, args: string[], options: ExecutionOptions) { logger.info('Sandbox execution via firejail', {command, args}); const execPath = path.dirname(command); const execName = path.basename(command); @@ -362,8 +363,9 @@ function sandboxFirejail(command: string, args: string[], options) { delete options.ldPath; } - for (const key of Object.keys(options.env || {})) { - jailingOptions.push(`--env=${key}=${options.env[key]}`); + const env = options.env || {}; + for (const key of Object.keys(env)) { + jailingOptions.push(`--env=${key}=${env[key]}`); } delete options.env; @@ -371,7 +373,7 @@ function sandboxFirejail(command: string, args: string[], options) { } const sandboxDispatchTable = { - none: (command, args, options) => { + none: (command: string, args: string[], options: ExecutionOptions) => { logger.info('Sandbox execution (sandbox disabled)', {command, args}); if (needsWine(command)) { return executeWineDirect(command, args, options); @@ -412,7 +414,7 @@ export function startWineInit() { 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 env = applyWineEnv({PATH: unwrapString(process.env.PATH)}); const prefix = env.WINEPREFIX; logger.info(`Initialising WINE in ${prefix}`); @@ -433,7 +435,7 @@ export function startWineInit() { // 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; + let wineServer: child_process.ChildProcess | undefined; if (firejail) { logger.info(`Starting a new, firejailed, long-lived wineserver complex`); wineServer = child_process.spawn( @@ -462,7 +464,7 @@ export function startWineInit() { Graceful.on('exit', () => { const waitingPromises: Promise[] = []; - function waitForExit(process, name): Promise { + function waitForExit(process: child_process.ChildProcess, name: string): Promise { return new Promise(resolve => { process.on('close', () => { logger.info(`Process '${name}' closed`); @@ -477,29 +479,33 @@ export function startWineInit() { if (wineServer.killed) { waitingPromises.push(waitForExit(wineServer, 'WINE server')); } - wineServer = null; + wineServer = undefined; } return Promise.all(waitingPromises); }); return new Promise((resolve, reject) => { - setupOnError(wineServer.stdin, 'stdin'); - setupOnError(wineServer.stdout, 'stdout'); - setupOnError(wineServer.stderr, 'stderr'); + assert(wineServer); + const [stdin, stdout, stderr] = [ + unwrap(wineServer.stdin), + unwrap(wineServer.stdout), + unwrap(wineServer.stderr), + ]; + setupOnError(stdin, 'stdin'); + setupOnError(stdout, 'stdout'); + setupOnError(stderr, 'stderr'); const magicString = '!!EVERYTHING IS WORKING!!'; - wineServer.stdin.write(`echo ${magicString}`); + stdin.write(`echo ${magicString}`); let output = ''; - wineServer.stdout.on('data', data => { + 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()}`), - ); + 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); @@ -513,29 +519,29 @@ export function startWineInit() { wineInitPromise = asyncSetup(); } -function applyWineEnv(env) { +function applyWineEnv(env: Record): Record { return { ...env, // Force use of wine vcruntime (See change 45106c382) WINEDLLOVERRIDES: 'vcruntime140=b', WINEDEBUG: '-all', - WINEPREFIX: execProps('winePrefix'), + WINEPREFIX: execProps('winePrefix'), }; } -function needsWine(command) { +function needsWine(command: string) { return command.match(/\.exe$/i) && process.platform === 'linux' && !process.env.wsl; } -async function executeWineDirect(command, args, options) { +async function executeWineDirect(command: string, args: string[], options: ExecutionOptions) { options = _.clone(options) || {}; - options.env = applyWineEnv(options.env); + options.env = applyWineEnv(options.env || {}); args = [command, ...args]; await wineInitPromise; return await executeDirect(unwrapString(execProps('wine')), args, options); } -async function executeFirejail(command, args, options) { +async function executeFirejail(command: string, args: string[], options: ExecutionOptions) { options = _.clone(options) || {}; const firejail = execProps('firejail'); const baseOptions = withFirejailTimeout( @@ -544,9 +550,9 @@ async function executeFirejail(command, args, options) { ); if (needsWine(command)) { logger.debug('WINE execution via firejail', {command, args}); - options.env = applyWineEnv(options.env); + options.env = applyWineEnv(options.env || {}); args = [command, ...args]; - command = execProps('wine'); + command = execProps('wine'); baseOptions.push('--profile=' + getFirejailProfileFilePath('wine'), `--join=${wineSandboxName}`); delete options.customCwd; baseOptions.push(command); @@ -562,7 +568,7 @@ async function executeFirejail(command, args, options) { delete options.ldPath; } - let filenameTransform; + let filenameTransform: FilenameTransformFunc | undefined; if (options.customCwd) { baseOptions.push(`--private=${options.customCwd}`); const replacement = options.customCwd; @@ -579,7 +585,7 @@ async function executeFirejail(command, args, options) { return await executeDirect(firejail, baseOptions.concat(args), options, filenameTransform); } -async function executeNone(command, args, options) { +async function executeNone(command: string, args: string[], options: ExecutionOptions) { if (needsWine(command)) { return await executeWineDirect(command, args, options); } @@ -589,7 +595,7 @@ async function executeNone(command, args, options) { const executeDispatchTable = { none: executeNone, firejail: executeFirejail, - nsjail: (command, args, options) => + nsjail: (command: string, args: string[], options: ExecutionOptions) => needsWine(command) ? executeFirejail(command, args, options) : executeNsjail(command, args, options), cewrapper: executeCEWrapper, }; diff --git a/package.json b/package.json index 59466c14c..3f41d3423 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "format": "prettier --write .", "format-files": "prettier --write --ignore-unknown", "ts-compile": "tsc", - "ts-check": "tsc -p ./tsconfig.backend.json --noEmit && tsc -p ./tsconfig.frontend.json --noEmit", + "ts-check": "tsc -p ./tsconfig.backend.json --noEmit && tsc -p ./tsconfig.frontend.json --noEmit && tsc -p ./tsconfig.tests.json --noEmit", "webpack": "node --no-warnings=ExperimentalWarning --loader ts-node/esm ./node_modules/webpack-cli/bin/cli.js --node-env=production --config webpack.config.esm.ts" }, "license": "BSD-2-Clause" diff --git a/test/exec-tests.ts b/test/exec-tests.ts index 5dc4b4f28..21821736b 100644 --- a/test/exec-tests.ts +++ b/test/exec-tests.ts @@ -24,6 +24,7 @@ import path from 'path'; +import {assert} from '../lib/assert.js'; import * as exec from '../lib/exec.js'; import * as props from '../lib/properties.js'; import {UnprocessedExecResult} from '../types/execution/execution.interfaces.js'; @@ -230,6 +231,7 @@ describe('Execution tests', () => { ]); options.should.deep.equals({}); expect(filenameTransform).to.not.be.undefined; + assert(filenameTransform); filenameTransform('moo').should.equal('moo'); filenameTransform('/some/custom/cwd/file').should.equal('/app/file'); }); diff --git a/tsconfig.tests.json b/tsconfig.tests.json new file mode 100644 index 000000000..b9f8118fe --- /dev/null +++ b/tsconfig.tests.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.backend.json", + "files": [], + "include": ["test/**/*.js", "test/**/*.ts"], + "exclude": [] +}