Skip to content

Commit

Permalink
test(xsnap): Test xsnap close and terminate
Browse files Browse the repository at this point in the history
  • Loading branch information
kriskowal authored and dckc committed Jan 21, 2021
1 parent 1e2e10d commit 0b4c018
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 22 deletions.
2 changes: 1 addition & 1 deletion packages/xsnap/src/netstring.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const encoder = new TextEncoder();
* @param {AsyncIterable<Uint8Array>} input
* @param {string=} name
* @param {number=} capacity
* @returns {AsyncIterableIterator<Uint8Array>} input
* @returns {AsyncGenerator<Uint8Array>} input
*/
export async function* reader(input, name = '<unknown>', capacity = 1024) {
let length = 0;
Expand Down
8 changes: 4 additions & 4 deletions packages/xsnap/src/node-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ const continues = { value: undefined };
* Back pressure emerges from awaiting on the promise
* returned by `next` before calling `next` again.
*
* @param {NodeJS.WritableStream} output
* @param {NodeJS.WritableStream} output the destination Node.js writer
* @param {string} [name] a debug name for stream errors
* @returns {Stream<void, Uint8Array, void>}
*/
export function writer(output) {
export function writer(output, name = '<unnamed stream>') {
/**
* @type {Deferred<IteratorResult<void>>}
*/
let drained = defer();
drained.resolve(continues);

output.on('error', err => {
console.log('err', err);
drained.reject(err);
drained.reject(new Error(`Cannot write ${name}: ${err.message}`));
});

output.on('drain', () => {
Expand Down
35 changes: 18 additions & 17 deletions packages/xsnap/src/xsnap.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export function xsnap(options) {
handleCommand = echoCommand,
debug = false,
snapshot = undefined,
stdout = 'inherit',
stderr = 'inherit',
stdout = 'ignore',
stderr = 'ignore',
} = options;

const platform = {
Expand All @@ -64,12 +64,8 @@ export function xsnap(options) {
throw new Error(`xsnap does not support platform ${os}`);
}

const xsnapBin = new URL(
`../build/bin/${platform}/release/xsnap`,
importMetaUrl,
).pathname;
const xsnapDebugBin = new URL(
`../build/bin/${platform}/debug/xsnap`,
const bin = new URL(
`../build/bin/${platform}/${debug ? 'debug' : 'release'}/xsnap`,
importMetaUrl,
).pathname;

Expand All @@ -78,15 +74,15 @@ export function xsnap(options) {

const args = snapshot ? ['-r', snapshot] : [];

const bin = debug ? xsnapDebugBin : xsnapBin;

const xsnapProcess = spawn(bin, args, {
stdio: ['ignore', stdout, stderr, 'pipe', 'pipe'],
});

xsnapProcess.on('exit', code => {
if (code === 0 || code === null) {
xsnapProcess.on('exit', (code, signal) => {
if (code === 0) {
vatExit.resolve();
} else if (signal !== null) {
vatExit.reject(new Error(`${name} exited due to signal ${signal}`));
} else {
vatExit.reject(new Error(`${name} exited with code ${code}`));
}
Expand All @@ -97,7 +93,10 @@ export function xsnap(options) {
);

const messagesToXsnap = netstring.writer(
node.writer(/** @type {NodeJS.WritableStream} */ (xsnapProcess.stdio[3])),
node.writer(
/** @type {NodeJS.WritableStream} */ (xsnapProcess.stdio[3]),
`messages to ${name}`,
),
);
const messagesFromXsnap = netstring.reader(
/** @type {AsyncIterable<Uint8Array>} */ (xsnapProcess.stdio[4]),
Expand All @@ -114,7 +113,7 @@ export function xsnap(options) {
const { done, value: message } = await messagesFromXsnap.next();
if (done) {
xsnapProcess.kill();
throw new Error('xsnap protocol error: unexpected end of output');
return vatCancelled;
}
if (message.byteLength === 0) {
// A protocol error kills the xsnap child process and breaks the baton
Expand Down Expand Up @@ -214,8 +213,10 @@ export function xsnap(options) {
* @returns {Promise<void>}
*/
async function close() {
await messagesToXsnap.return();
baton = Promise.reject(new Error(`xsnap closed`));
baton = baton.then(async () => {
await messagesToXsnap.return();
throw new Error(`${name} closed`);
});
baton.catch(() => {}); // Suppress Node.js unhandled exception warning.
return vatExit.promise;
}
Expand All @@ -225,7 +226,7 @@ export function xsnap(options) {
*/
async function terminate() {
xsnapProcess.kill();
baton = Promise.reject(new Error(`xsnap closed`));
baton = Promise.reject(new Error(`${name} terminated`));
baton.catch(() => {}); // Suppress Node.js unhandled exception warning.
// Mute the vatExit exception: it is expected.
return vatExit.promise.catch(() => {});
Expand Down
73 changes: 73 additions & 0 deletions packages/xsnap/test/test-xsnap.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ const decoder = new TextDecoder();
const encoder = new TextEncoder();

const xsnapOptions = {
name: 'xsnap test worker',
spawn: childProcess.spawn,
os: os.type(),
stderr: 'inherit',
stdout: 'inherit',
debug: true
};

test('evaluate and issueCommand', async t => {
Expand Down Expand Up @@ -159,3 +163,72 @@ test('write and read snapshot', async t => {

t.deepEqual(['Hello, World!'], messages);
});

function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

test('fail to send command to already-closed xnsap worker', async t => {
const vat = xsnap({ ...xsnapOptions });
await vat.close();
await vat.evaluate(``).catch(err => {
t.is(err.message, 'xsnap test worker exited')
});
});

test('fail to send command to already-terminated xnsap worker', async t => {
const vat = xsnap({ ...xsnapOptions });
await vat.terminate();
await vat.evaluate(``).catch(err => {
t.is(err.message, 'xsnap test worker exited due to signal SIGTERM')
});
});

test('fail to send command to terminated xnsap worker', async t => {
const vat = xsnap({ ...xsnapOptions });
const hang = vat.evaluate(`for (;;) {}`).then(
() => t.fail('command should not complete'),
err => {
t.is(err.message, 'Cannot write messages to xsnap test worker: write EPIPE')
},
);

await vat.terminate();
await hang;
});

test('abnormal termination', async t => {
const vat = xsnap({ ...xsnapOptions });
const hang = vat.evaluate(`for (;;) {}`).then(
() => t.fail('command should not complete'),
err => {
t.is(err.message, 'xsnap test worker exited due to signal SIGTERM')
},
);

// Allow the evaluate command to flush.
await delay(10);
await vat.terminate();
await hang;
});

test('normal close of pathological script', async t => {
const vat = xsnap({ ...xsnapOptions });
const hang = vat.evaluate(`for (;;) {}`).then(
() => t.fail('command should not complete'),
err => {
t.is(err.message, 'xsnap test worker exited due to signal SIGTERM')
},
);
// Allow the evaluate command to flush.
await delay(10);
// Close must timeout and the evaluation command
// must hang.
await Promise.race([
vat.close().then(() => t.fail()),
hang,
delay(10),
]);
await vat.terminate();
await hang;
});

0 comments on commit 0b4c018

Please sign in to comment.