Skip to content

Commit c64c03b

Browse files
authored
revert: "chore!: Remove web worker execution service and environment (#3357)" (#3359)
This reverts commit 042d2fa. To avoid multiple major releases shortly after each other, we should group this closer with the merge of #3322.
1 parent 042d2fa commit c64c03b

File tree

20 files changed

+1368
-8
lines changed

20 files changed

+1368
-8
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"branches": 93.51,
3-
"functions": 97.36,
4-
"lines": 98.33,
5-
"statements": 98.17
2+
"branches": 93.54,
3+
"functions": 97.38,
4+
"lines": 98.34,
5+
"statements": 98.08
66
}

packages/snaps-controllers/src/services/browser.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ describe('browser entrypoint', () => {
66
'setupMultiplex',
77
'IframeExecutionService',
88
'OffscreenExecutionService',
9+
'WebWorkerExecutionService',
910
'ProxyPostMessageStream',
1011
'WebViewExecutionService',
1112
'WebViewMessageStream',

packages/snaps-controllers/src/services/browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './ProxyPostMessageStream';
55
export * from './iframe';
66
export * from './offscreen';
77
export * from './webview';
8+
export { WebWorkerExecutionService } from './webworker';

packages/snaps-controllers/src/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export type * from './ExecutionService';
33
export * from './ProxyPostMessageStream';
44
export * from './iframe';
55
export * from './offscreen';
6+
export { WebWorkerExecutionService } from './webworker';
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { HandlerType } from '@metamask/snaps-utils';
2+
import {
3+
DEFAULT_SNAP_BUNDLE,
4+
MOCK_LOCAL_SNAP_ID,
5+
MOCK_ORIGIN,
6+
MOCK_SNAP_ID,
7+
spy,
8+
} from '@metamask/snaps-utils/test-utils';
9+
import { describe, it, expect, afterEach } from 'vitest';
10+
11+
import {
12+
WebWorkerExecutionService,
13+
WORKER_POOL_ID,
14+
} from './WebWorkerExecutionService';
15+
import { MOCK_BLOCK_NUMBER } from '../../test-utils/constants';
16+
import { createService } from '../../test-utils/service';
17+
18+
const WORKER_POOL_URL = 'http://localhost:63315/worker/pool/index.html';
19+
20+
describe('WebWorkerExecutionService', () => {
21+
afterEach(() => {
22+
document.getElementById(WORKER_POOL_ID)?.remove();
23+
});
24+
25+
it('can boot', async () => {
26+
const { service } = createService(WebWorkerExecutionService, {
27+
documentUrl: new URL(WORKER_POOL_URL),
28+
});
29+
30+
expect(service).toBeDefined();
31+
await service.terminateAllSnaps();
32+
});
33+
34+
it('only creates a single iframe', async () => {
35+
const { service } = createService(WebWorkerExecutionService, {
36+
documentUrl: new URL(WORKER_POOL_URL),
37+
});
38+
39+
await service.executeSnap({
40+
snapId: MOCK_SNAP_ID,
41+
sourceCode: `
42+
module.exports.onRpcRequest = () => null;
43+
`,
44+
endowments: [],
45+
});
46+
47+
await service.executeSnap({
48+
snapId: MOCK_LOCAL_SNAP_ID,
49+
sourceCode: `
50+
module.exports.onRpcRequest = () => null;
51+
`,
52+
endowments: [],
53+
});
54+
55+
expect(document.getElementById(WORKER_POOL_ID)).not.toBeNull();
56+
expect(document.getElementsByTagName('iframe')).toHaveLength(1);
57+
58+
await service.terminateAllSnaps();
59+
});
60+
61+
it('can create a snap worker and start the snap', async () => {
62+
const { service } = createService(WebWorkerExecutionService, {
63+
documentUrl: new URL(WORKER_POOL_URL),
64+
});
65+
66+
const response = await service.executeSnap({
67+
snapId: 'TestSnap',
68+
sourceCode: `
69+
module.exports.onRpcRequest = () => null;
70+
`,
71+
endowments: [],
72+
});
73+
74+
expect(response).toBe('OK');
75+
await service.terminateAllSnaps();
76+
});
77+
78+
it('executes a snap', async () => {
79+
const { service } = createService(WebWorkerExecutionService, {
80+
documentUrl: new URL(WORKER_POOL_URL),
81+
});
82+
83+
await service.executeSnap({
84+
snapId: MOCK_SNAP_ID,
85+
sourceCode: DEFAULT_SNAP_BUNDLE,
86+
endowments: ['console'],
87+
});
88+
89+
const result = await service.handleRpcRequest(MOCK_SNAP_ID, {
90+
origin: MOCK_ORIGIN,
91+
handler: HandlerType.OnRpcRequest,
92+
request: {
93+
jsonrpc: '2.0',
94+
id: 1,
95+
method: 'foo',
96+
},
97+
});
98+
99+
expect(result).toBe('foo1');
100+
101+
await service.terminateAllSnaps();
102+
});
103+
104+
it('can handle a crashed snap', async () => {
105+
expect.assertions(1);
106+
const { service } = createService(WebWorkerExecutionService, {
107+
documentUrl: new URL(WORKER_POOL_URL),
108+
});
109+
110+
const action = async () => {
111+
await service.executeSnap({
112+
snapId: MOCK_SNAP_ID,
113+
sourceCode: `
114+
throw new Error("Crashed.");
115+
`,
116+
endowments: [],
117+
});
118+
};
119+
120+
await expect(action()).rejects.toThrow(
121+
`Error while running snap '${MOCK_SNAP_ID}': Crashed.`,
122+
);
123+
await service.terminateAllSnaps();
124+
});
125+
126+
it('can detect outbound requests', async () => {
127+
expect.assertions(5);
128+
129+
const { service, messenger } = createService(WebWorkerExecutionService, {
130+
documentUrl: new URL(WORKER_POOL_URL),
131+
});
132+
133+
const publishSpy = spy(messenger, 'publish');
134+
135+
const executeResult = await service.executeSnap({
136+
snapId: MOCK_SNAP_ID,
137+
sourceCode: `
138+
module.exports.onRpcRequest = () => ethereum.request({ method: 'eth_blockNumber', params: [] });
139+
`,
140+
endowments: ['ethereum'],
141+
});
142+
143+
expect(executeResult).toBe('OK');
144+
145+
const result = await service.handleRpcRequest(MOCK_SNAP_ID, {
146+
origin: 'foo',
147+
handler: HandlerType.OnRpcRequest,
148+
request: {
149+
jsonrpc: '2.0',
150+
id: 1,
151+
method: 'foobar',
152+
params: [],
153+
},
154+
});
155+
156+
expect(result).toBe(MOCK_BLOCK_NUMBER);
157+
158+
expect(publishSpy.calls).toHaveLength(2);
159+
expect(publishSpy.calls[0]).toStrictEqual({
160+
args: ['ExecutionService:outboundRequest', MOCK_SNAP_ID],
161+
result: undefined,
162+
});
163+
164+
expect(publishSpy.calls[1]).toStrictEqual({
165+
args: ['ExecutionService:outboundResponse', MOCK_SNAP_ID],
166+
result: undefined,
167+
});
168+
169+
await service.terminateAllSnaps();
170+
publishSpy.reset();
171+
});
172+
173+
it('confirms that events are secured', async () => {
174+
// Check if the security critical properties of the Event object
175+
// are unavailable. This will confirm that executeLockdownEvents works
176+
// inside snaps-execution-environments
177+
const { service } = createService(WebWorkerExecutionService, {
178+
documentUrl: new URL(WORKER_POOL_URL),
179+
});
180+
181+
await service.executeSnap({
182+
snapId: MOCK_SNAP_ID,
183+
sourceCode: `
184+
module.exports.onRpcRequest = async ({ request }) => {
185+
let result;
186+
const promise = new Promise((resolve) => {
187+
const xhr = new XMLHttpRequest();
188+
xhr.open('GET', 'https://metamask.io/');
189+
xhr.send();
190+
xhr.onreadystatechange = (ev) => {
191+
result = ev;
192+
resolve();
193+
};
194+
});
195+
await promise;
196+
197+
return {
198+
targetIsUndefined: result.target === undefined,
199+
currentTargetIsUndefined: result.target === undefined,
200+
srcElementIsUndefined: result.target === undefined,
201+
composedPathIsUndefined: result.target === undefined
202+
};
203+
};
204+
`,
205+
endowments: ['console', 'XMLHttpRequest'],
206+
});
207+
208+
const result = await service.handleRpcRequest(MOCK_SNAP_ID, {
209+
origin: 'foo',
210+
handler: HandlerType.OnRpcRequest,
211+
request: {
212+
jsonrpc: '2.0',
213+
id: 1,
214+
method: 'foobar',
215+
params: [],
216+
},
217+
});
218+
219+
expect(result).toStrictEqual({
220+
targetIsUndefined: true,
221+
currentTargetIsUndefined: true,
222+
srcElementIsUndefined: true,
223+
composedPathIsUndefined: true,
224+
});
225+
});
226+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import type { BasePostMessageStream } from '@metamask/post-message-stream';
2+
import { WindowPostMessageStream } from '@metamask/post-message-stream';
3+
import { createWindow } from '@metamask/snaps-utils';
4+
import { assert } from '@metamask/utils';
5+
import { nanoid } from 'nanoid';
6+
7+
import type {
8+
ExecutionServiceArgs,
9+
TerminateJobArgs,
10+
} from '../AbstractExecutionService';
11+
import { AbstractExecutionService } from '../AbstractExecutionService';
12+
import { ProxyPostMessageStream } from '../ProxyPostMessageStream';
13+
14+
type WebWorkerExecutionEnvironmentServiceArgs = {
15+
documentUrl: URL;
16+
} & ExecutionServiceArgs;
17+
18+
export const WORKER_POOL_ID = 'snaps-worker-pool';
19+
20+
export class WebWorkerExecutionService extends AbstractExecutionService<string> {
21+
readonly #documentUrl: URL;
22+
23+
#runtimeStream?: BasePostMessageStream;
24+
25+
/**
26+
* Create a new webworker execution service.
27+
*
28+
* @param args - The constructor arguments.
29+
* @param args.documentUrl - The URL of the worker pool document to use as the
30+
* execution environment.
31+
* @param args.messenger - The messenger to use for communication with the
32+
* `SnapController`.
33+
* @param args.setupSnapProvider - The function to use to set up the snap
34+
* provider.
35+
*/
36+
constructor({
37+
documentUrl,
38+
messenger,
39+
setupSnapProvider,
40+
...args
41+
}: WebWorkerExecutionEnvironmentServiceArgs) {
42+
super({
43+
...args,
44+
messenger,
45+
setupSnapProvider,
46+
});
47+
48+
this.#documentUrl = documentUrl;
49+
}
50+
51+
/**
52+
* Send a termination command to the worker pool document.
53+
*
54+
* @param job - The job to terminate.
55+
*/
56+
// TODO: Either fix this lint violation or explain why it's necessary to
57+
// ignore.
58+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
59+
protected async terminateJob(job: TerminateJobArgs<string>) {
60+
// The `AbstractExecutionService` will have already closed the job stream,
61+
// so we write to the runtime stream directly.
62+
assert(this.#runtimeStream, 'Runtime stream not initialized.');
63+
this.#runtimeStream.write({
64+
jobId: job.id,
65+
data: {
66+
jsonrpc: '2.0',
67+
method: 'terminateJob',
68+
id: nanoid(),
69+
},
70+
});
71+
}
72+
73+
/**
74+
* Create a new stream for the specified job. This wraps the runtime stream
75+
* in a stream specific to the job.
76+
*
77+
* @param jobId - The job ID.
78+
* @returns An object with the worker ID and stream.
79+
*/
80+
protected async initEnvStream(jobId: string) {
81+
// Lazily create the worker pool document.
82+
await this.createDocument();
83+
84+
// `createDocument` should have initialized the runtime stream.
85+
assert(this.#runtimeStream, 'Runtime stream not initialized.');
86+
87+
const stream = new ProxyPostMessageStream({
88+
stream: this.#runtimeStream,
89+
jobId,
90+
});
91+
92+
return { worker: jobId, stream };
93+
}
94+
95+
/**
96+
* Creates the worker pool document to be used as the execution environment.
97+
*
98+
* If the document already exists, this does nothing.
99+
*/
100+
// TODO: Either fix this lint violation or explain why it's necessary to
101+
// ignore.
102+
// eslint-disable-next-line no-restricted-syntax
103+
private async createDocument() {
104+
// We only want to create a single pool.
105+
if (document.getElementById(WORKER_POOL_ID)) {
106+
return;
107+
}
108+
109+
const window = await createWindow({
110+
uri: this.#documentUrl.href,
111+
id: WORKER_POOL_ID,
112+
sandbox: false,
113+
});
114+
115+
this.#runtimeStream = new WindowPostMessageStream({
116+
name: 'parent',
117+
target: 'child',
118+
targetWindow: window,
119+
targetOrigin: '*',
120+
});
121+
}
122+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './WebWorkerExecutionService';

0 commit comments

Comments
 (0)