Skip to content

Commit

Permalink
Async WINE setup (#3558)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgodbolt committed Apr 26, 2022
1 parent fb35d64 commit f77dd84
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 93 deletions.
4 changes: 2 additions & 2 deletions app.js
Expand Up @@ -50,7 +50,7 @@ import {CompilationEnvironment} from './lib/compilation-env';
import {CompilationQueue} from './lib/compilation-queue';
import {CompilerFinder} from './lib/compiler-finder';
// import { policy as csp } from './lib/csp';
import {initialiseWine} from './lib/exec';
import {startWineInit} from './lib/exec';
import {CompileHandler} from './lib/handlers/compile';
import * as healthCheck from './lib/handlers/health-check';
import {NoScriptHandler} from './lib/handlers/noscript';
Expand Down Expand Up @@ -459,7 +459,7 @@ const awsProps = props.propsFor('aws');
// eslint-disable-next-line max-statements
async function main() {
await aws.initConfig(awsProps);
await initialiseWine();
startWineInit();

const clientOptionsHandler = new ClientOptionsHandler(sources, compilerProps, defArgs);
const compilationQueue = CompilationQueue.fromProps(compilerProps.ceProps);
Expand Down
192 changes: 101 additions & 91 deletions lib/exec.js
Expand Up @@ -23,7 +23,7 @@
// POSSIBILITY OF SUCH DAMAGE.

import child_process from 'child_process';
import fs from 'fs';
import fs from 'fs-extra';
import path from 'path';

import Graceful from 'node-graceful';
Expand Down Expand Up @@ -299,12 +299,16 @@ export async function sandbox(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 initialiseWine() {
export function startWineInit() {
const wine = execProps('wine');
if (!wine) {
logger.info('WINE not configured');
return Promise.resolve();
return;
}

const server = execProps('wineServer');
Expand All @@ -315,101 +319,105 @@ export function initialiseWine() {
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=' + 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}`);
}
const asyncSetup = async () => {
if (!(await fs.pathExists(prefix))) {
logger.info(`Creating directory ${prefix}`);
await fs.mkdir(prefix);
}

wineServer.on('close', code => {
logger.info(`WINE server complex exited with code ${code}`);
});
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}`);
}

Graceful.on('exit', () => {
const waitingPromises = [];
wineServer.on('close', code => {
logger.info(`WINE server complex exited with code ${code}`);
});

function waitForExit(process, name) {
return new Promise(resolve => {
process.on('close', () => {
logger.info(`Process '${name}' closed`);
resolve();
});
});
}
Graceful.on('exit', () => {
const waitingPromises = [];

if (wineServer && !wineServer.killed) {
logger.info('Shutting down WINE server complex');
wineServer.kill();
if (wineServer.killed) {
waitingPromises.push(waitForExit(wineServer, 'WINE server'));
function waitForExit(process, name) {
return new Promise(resolve => {
process.on('close', () => {
logger.info(`Process '${name}' closed`);
resolve();
});
});
}
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();
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);
});
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();

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) {
Expand All @@ -426,14 +434,15 @@ function needsWine(command) {
return command.match(/\.exe$/i) && process.platform === 'linux' && !process.env.wsl;
}

function executeWineDirect(command, args, options) {
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);
}

function executeFirejail(command, 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);
Expand All @@ -445,6 +454,7 @@ function executeFirejail(command, args, options) {
baseOptions.push('--profile=' + getFirejailProfileFilePath('wine'), `--join=${wineSandboxName}`);
delete options.customCwd;
baseOptions.push(command);
await wineInitPromise;
return executeDirect(firejail, baseOptions.concat(args), options);
}

Expand Down Expand Up @@ -473,7 +483,7 @@ function executeFirejail(command, args, options) {
return executeDirect(firejail, baseOptions.concat(args), options, filenameTransform);
}

function executeNone(command, args, options) {
async function executeNone(command, args, options) {
if (needsWine(command)) {
return executeWineDirect(command, args, options);
}
Expand Down

0 comments on commit f77dd84

Please sign in to comment.