Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ describe('AbstractExecutionService', () => {
`,
endowments: ['console'],
}),
).rejects.toThrow('The Snaps execution environment failed to start.');
).rejects.toThrow(
'The executor for npm:@metamask/example-snap was unreachable.',
);
});

it('throws an error if execution environment fails to init', async () => {
it('throws an error if execution environment fails to start initialization', async () => {
const { service } = createService(MockExecutionService);

// @ts-expect-error Accessing private property and returning unusable worker.
Expand All @@ -78,7 +80,33 @@ describe('AbstractExecutionService', () => {
`,
endowments: ['console'],
}),
).rejects.toThrow('The Snaps execution environment failed to start.');
).rejects.toThrow(
`The executor for npm:@metamask/example-snap couldn't start initialization. The offscreen document may not exist.`,
);
});

it('throws an error if execution environment fails to init', async () => {
const { service } = createService(MockExecutionService);

// @ts-expect-error Accessing private property and returning unusable worker.
service.initEnvStream = async (snapId: string) =>
new Promise((_resolve) => {
// @ts-expect-error Accessing private property to mirror updating state
service.setSnapStatus(snapId, 'initializing');
// no-op
});

await expect(
service.executeSnap({
snapId: MOCK_SNAP_ID,
sourceCode: `
console.log('foo');
`,
endowments: ['console'],
}),
).rejects.toThrow(
'The executor for npm:@metamask/example-snap failed to initialize. The iframe/webview/worker failed to load.',
);
});

it('throws an error if Snap fails to init', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ export type Job<WorkerType> = {
export type TerminateJobArgs<WorkerType> = Partial<Job<WorkerType>> &
Pick<Job<WorkerType>, 'id'>;

/**
Statuses used for diagnostic purposes
- created: The initial state, no initialization has started
- initializing: Snap execution environment is initializing
- initialized: Snap execution environment has initialized
- executing: Snap source code is being executed
- running: Snap executed and ready for RPC requests
*/
type ExecutionStatus =
| 'created'
| 'initializing'
| 'initialized'
| 'executing'
| 'running';

export abstract class AbstractExecutionService<WorkerType>
implements ExecutionService
{
Expand All @@ -69,6 +84,8 @@ export abstract class AbstractExecutionService<WorkerType>

readonly #jobs: Map<string, Job<WorkerType>>;

readonly #status: Map<string, ExecutionStatus>;

readonly #setupSnapProvider: SetupSnapProvider;

readonly #messenger: ExecutionServiceMessenger;
Expand All @@ -90,6 +107,7 @@ export abstract class AbstractExecutionService<WorkerType>
usePing = true,
}: ExecutionServiceArgs) {
this.#jobs = new Map();
this.#status = new Map();
this.#setupSnapProvider = setupSnapProvider;
this.#messenger = messenger;
this.#initTimeout = initTimeout;
Expand Down Expand Up @@ -186,6 +204,7 @@ export abstract class AbstractExecutionService<WorkerType>
this.terminateJob(job);

this.#jobs.delete(snapId);
this.#status.delete(snapId);
log(`Snap "${snapId}" terminated.`);
}

Expand Down Expand Up @@ -244,7 +263,17 @@ export abstract class AbstractExecutionService<WorkerType>
if (result === hasTimedOut) {
// For certain environments, such as the iframe we may have already created the worker and wish to terminate it.
this.terminateJob({ id: snapId });
throw new Error('The Snaps execution environment failed to start.');

const status = this.#status.get(snapId);
if (status === 'created') {
// Currently this error can only be thrown by OffscreenExecutionService.
throw new Error(
`The executor for ${snapId} couldn't start initialization. The offscreen document may not exist.`,
);
}
throw new Error(
`The executor for ${snapId} failed to initialize. The iframe/webview/worker failed to load.`,
);
}

const { worker, stream: envStream } = result;
Expand Down Expand Up @@ -307,6 +336,16 @@ export abstract class AbstractExecutionService<WorkerType>
stream: BasePostMessageStream;
}>;

/**
* Set the execution status of the Snap.
*
* @param snapId - The Snap ID.
* @param status - The current execution status.
*/
protected setSnapStatus(snapId: string, status: ExecutionStatus) {
this.#status.set(snapId, status);
}

async terminateAllSnaps() {
await Promise.all(
[...this.#jobs.keys()].map(async (snapId) => this.terminateSnap(snapId)),
Expand All @@ -332,6 +371,8 @@ export abstract class AbstractExecutionService<WorkerType>
throw new Error(`"${snapId}" is already running.`);
}

this.setSnapStatus(snapId, 'created');

const timer = new Timer(this.#initTimeout);

// This may resolve even if the environment has failed to start up fully
Expand All @@ -350,7 +391,9 @@ export abstract class AbstractExecutionService<WorkerType>
);

if (pingResult === hasTimedOut) {
throw new Error('The Snaps execution environment failed to start.');
throw new Error(
`The executor for ${snapId} was unreachable. The executor did not respond in time.`,
);
}
}

Expand All @@ -360,6 +403,8 @@ export abstract class AbstractExecutionService<WorkerType>

const remainingTime = timer.remaining;

this.setSnapStatus(snapId, 'initialized');

const request = {
jsonrpc: '2.0',
method: 'executeSnap',
Expand All @@ -369,6 +414,8 @@ export abstract class AbstractExecutionService<WorkerType>

assertIsJsonRpcRequest(request);

this.setSnapStatus(snapId, 'executing');

const result = await withTimeout(
this.#command(job.id, request),
remainingTime,
Expand All @@ -378,6 +425,10 @@ export abstract class AbstractExecutionService<WorkerType>
throw new Error(`${snapId} failed to start.`);
}

if (result === 'OK') {
this.setSnapStatus(snapId, 'running');
}

return result as string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export class IframeExecutionService extends AbstractExecutionService<Window> {
worker: Window;
stream: BasePostMessageStream;
}> {
this.setSnapStatus(snapId, 'initializing');

const iframeWindow = await createWindow({
uri: this.iframeUrl.toString(),
id: snapId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import type { TerminateJobArgs } from '..';
import { AbstractExecutionService } from '..';

export class NodeProcessExecutionService extends AbstractExecutionService<ChildProcess> {
protected async initEnvStream(): Promise<{
protected async initEnvStream(snapId: string): Promise<{
worker: ChildProcess;
stream: BasePostMessageStream;
}> {
this.setSnapStatus(snapId, 'initializing');

const worker = fork(
require.resolve('@metamask/snaps-execution-environments/node-process'),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import type { TerminateJobArgs } from '..';
import { AbstractExecutionService } from '..';

export class NodeThreadExecutionService extends AbstractExecutionService<Worker> {
protected async initEnvStream(): Promise<{
protected async initEnvStream(snapId: string): Promise<{
worker: Worker;
stream: BasePostMessageStream;
}> {
this.setSnapStatus(snapId, 'initializing');

const worker = new Worker(
require.resolve('@metamask/snaps-execution-environments/node-thread'),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export class ProxyExecutionService extends AbstractExecutionService<string> {
* @returns An object with the worker ID and stream.
*/
protected async initEnvStream(snapId: string) {
this.setSnapStatus(snapId, 'initializing');

const stream = new ProxyPostMessageStream({
stream: this.#stream,
jobId: snapId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export class WebViewExecutionService extends AbstractExecutionService<WebViewInt
* @returns An object with the webview and stream.
*/
protected async initEnvStream(snapId: string) {
this.setSnapStatus(snapId, 'initializing');

const webView = await this.#createWebView(snapId);

const stream = new WebViewMessageStream({
Expand Down
Loading