diff --git a/build/bin/runBrowserTests.js b/build/bin/runBrowserTests.js index f20b7a37..6aed1643 100644 --- a/build/bin/runBrowserTests.js +++ b/build/bin/runBrowserTests.js @@ -77,9 +77,10 @@ async function runTests(location) { }); server.listen(8080, '127.0.0.1', async () => { let failCount = 0; - const browser = await playwright['chromium'].launch({ headless: true, devtools: false }); + const browser = await playwright['chromium'].launch({ headless: true, devtools: true }); const context = await browser.newContext(); const page = await context.newPage(); + page.on('console', msg => console.log('LOG FROM INSIDE PAGE: ', msg)) const emitter = new events.EventEmitter(); emitter.on('fail', () => { failCount++; diff --git a/sync-api-common/src/browser/connection.ts b/sync-api-common/src/browser/connection.ts index c4fb64ad..08ddb7a4 100644 --- a/sync-api-common/src/browser/connection.ts +++ b/sync-api-common/src/browser/connection.ts @@ -4,42 +4,90 @@ * ------------------------------------------------------------------------------------------ */ import RAL from '../common/ral'; -import { BaseServiceConnection, BaseClientConnection, Message, RequestType } from '../common/connection'; +import { BaseServiceConnection, BaseClientConnection, Message, RequestType, KnownConnectionIds, BroadcastChannelName } from '../common/connection'; export class ClientConnection extends BaseClientConnection { - private readonly port: MessagePort | Worker | DedicatedWorkerGlobalScope; + private readonly broadcastChannel: BroadcastChannel; + private readonly messageChannel: MessageChannel; + private readonly sendPort: MessagePort; - constructor(port: MessagePort | Worker | DedicatedWorkerGlobalScope) { - super(); - this.port = port; - this.port.onmessage = ((event: MessageEvent) => { - this.handleMessage(event.data); - }); + constructor(channelName: string = BroadcastChannelName) { + super(self.location.pathname); + console.log(`Creating client with name ${channelName} with origin ${origin}`); + this.broadcastChannel = new BroadcastChannel(channelName); + this.messageChannel = new MessageChannel(); + this.sendPort = this.messageChannel.port1; + this.sendPort.addEventListener('message', this._handleMessage.bind(this)); + + // Need to send the port as transfer item, but official api doesn't support that. + const postMessageFunc = this.broadcastChannel.postMessage.bind(this.broadcastChannel) as any; + postMessageFunc(this.createPortBroadcastMessage(this.messageChannel.port2), [this.messageChannel.port2]); + } + + dispose() { + this.sendPort.removeEventListener('message', this._handleMessage.bind(this)); + this.messageChannel.port1.close(); + this.messageChannel.port2.close(); + this.broadcastChannel.close(); } - protected postMessage(sharedArrayBuffer: SharedArrayBuffer) { - this.port.postMessage(sharedArrayBuffer); + protected override postMessage(sharedArrayBuffer: SharedArrayBuffer): void { + this.sendPort.postMessage(sharedArrayBuffer); + } + + private _handleMessage(message: any) { + try { + if (message.data?.dest === this.connectionId || message.data?.dest === KnownConnectionIds.All) { + this.handleMessage(message.data); + } + } catch (error) { + RAL().console.error(error); + } + } } export class ServiceConnection extends BaseServiceConnection { - private readonly port: MessagePort | Worker | DedicatedWorkerGlobalScope; + private readonly broadcastChannel: BroadcastChannel; + private readonly clientPorts: Map; - constructor(port: MessagePort | Worker | DedicatedWorkerGlobalScope) { - super(); - this.port = port; - this.port.onmessage = (async (event: MessageEvent) => { - try { - await this.handleMessage(event.data); - } catch (error) { - RAL().console.error(error); - } - }); + constructor(channelName: string = BroadcastChannelName) { + super(KnownConnectionIds.Main); + console.log(`Creating server with name ${channelName} with origin ${origin}`); + this.broadcastChannel = new BroadcastChannel(channelName); + this.clientPorts = new Map(); + this.broadcastChannel.addEventListener('message', this.handleBroadcastMessage.bind(this)); + } + + dispose() { + this.clientPorts.clear(); + this.broadcastChannel.removeEventListener('message', this.handleBroadcastMessage.bind(this)); + this.broadcastChannel.close(); + } + + protected postMessage(message: Message) { + if (message.dest === KnownConnectionIds.All) { + const clientPorts = [...this.clientPorts.values()]; + clientPorts.forEach(c => c.postMessage(message)); + } else { + const clientPort = this.clientPorts.get(message.dest); + clientPort?.postMessage(message); + } + } + + protected onBroadcastPort(message: Message): void { + if (message.params && message.src && message.params.port) { + const messagePort = message.params.port as MessagePort; + messagePort.addEventListener('message', this._handleClientMessage.bind(this)); + this.clientPorts.set(message.src, message.params.port as MessagePort); + } } - protected postMessage(message: Message): void { - this.port.postMessage(message); + private _handleClientMessage(ev: MessageEvent) { + if (ev.data?.byteLength) { + this.handleMessage(ev.data); + } } } \ No newline at end of file diff --git a/sync-api-common/src/browser/ril.ts b/sync-api-common/src/browser/ril.ts index d8b92c4d..e13bcb70 100644 --- a/sync-api-common/src/browser/ril.ts +++ b/sync-api-common/src/browser/ril.ts @@ -21,11 +21,13 @@ class TestServiceConnection { + console.log('terminating service connection'); this.worker.terminate(); + this.dispose(); return Promise.resolve(0); } } @@ -60,7 +62,7 @@ const _ril: RIL = Object.freeze({ $testing: Object.freeze({ ClientConnection: Object.freeze({ create() { - return new ClientConnection(self); + return new ClientConnection(); } }), ServiceConnection: Object.freeze({ diff --git a/sync-api-common/src/common/connection.ts b/sync-api-common/src/common/connection.ts index f7e2dc10..0c1b200c 100644 --- a/sync-api-common/src/common/connection.ts +++ b/sync-api-common/src/common/connection.ts @@ -13,6 +13,8 @@ export type u64 = number; export type size = u32; export type Message = { + dest: string; + src: string; method: string; params?: Params; }; @@ -53,6 +55,8 @@ export type Params = { export type Request = { id: number; + src: string; + dest: string; } & Message; export namespace Request { @@ -85,6 +89,13 @@ export type RequestType = MessageType & ({ result?: TypedArray | object | null; }); +export const BroadcastChannelName = `@vscode/sync-api/default`; + +export enum KnownConnectionIds { + Main = 'main', + All = 'all' +} + class NoResult { public static readonly kind = 0 as const; constructor() { @@ -572,6 +583,7 @@ export class RPCError extends Error { export interface ClientConnection { readonly sendRequest: SendRequestSignatures; serviceReady(): Promise; + dispose(): void; } export abstract class BaseClientConnection implements ClientConnection { @@ -581,8 +593,7 @@ export abstract class BaseClientConnection; private readyCallbacks: PromiseCallbacks | undefined; - - constructor() { + constructor(protected connectionId: string) { this.id = 1; this.textEncoder = RAL().TextEncoder.create(); this.textDecoder = RAL().TextDecoder.create(); @@ -592,14 +603,19 @@ export abstract class BaseClientConnection { + this._sendRequest('$/checkready'); return this.readyPromise; } + protected createPortBroadcastMessage(receivePort: object) { + return { method: '$/broadcastport', src: this.connectionId, dest: KnownConnectionIds.Main, params: {port: receivePort}}; + } + public readonly sendRequest: SendRequestSignatures = this._sendRequest as SendRequestSignatures; private _sendRequest(method: string, arg1?: Params | ResultType | number, arg2?: ResultType | number, arg3?: number): { errno: 0; data: any } | { errno: RPCErrno } { const id = this.id++; - const request: Request = { id: id, method }; + const request: Request = { id: id, dest: 'main', src: this.connectionId, method }; let params: Params | undefined = undefined; let resultType: ResultType = new NoResult(); let timeout: number | undefined = undefined; @@ -658,10 +674,11 @@ export abstract class BaseClientConnection { readonly onRequest: HandleRequestSignatures; signalReady(): void; + dispose(): void; } export abstract class BaseServiceConnection implements ServiceConnection { @@ -771,8 +791,9 @@ export abstract class BaseServiceConnection; private readonly requestResults: Map; + private sentReady = false; - constructor() { + constructor(protected readonly connectionId: string) { this.textDecoder = RAL().TextDecoder.create(); this.textEncoder = RAL().TextEncoder.create(); this.requestHandlers = new Map(); @@ -788,6 +809,13 @@ export abstract class BaseServiceConnection { const header = new Uint32Array(sharedArrayBuffer, SyncSize.total, HeaderSize.total / 4); const requestOffset = header[HeaderIndex.messageOffset]; @@ -795,7 +823,9 @@ export abstract class BaseServiceConnection) => void): Promise { const connection = RAL().$testing.ClientConnection.create()!; - await connection.serviceReady(); try { + await connection.serviceReady(); test(connection); } catch (error) { if (error instanceof assert.AssertionError) { @@ -38,7 +38,8 @@ export async function runSingle(test: (connection: ClientConnection extends BaseClientConnection { - private readonly port: MessagePort | Worker; + private readonly broadcastChannel: BroadcastChannel; + private readonly messageChannel: MessageChannel; + private readonly sendPort: MessagePort; - constructor(port: MessagePort | Worker) { - super(); - this.port = port; - this.port.on('message', (message: Message) => { - try { + constructor(channelName: string = BroadcastChannelName) { + super(isMainThread ? KnownConnectionIds.Main : threadId.toString()); + this.broadcastChannel = new BroadcastChannel(channelName); + this.messageChannel = new MessageChannel(); + this.sendPort = this.messageChannel.port1; + this.sendPort.on('message', this._handleMessage.bind(this)); + + // Need to send the port as transfer item, but official api doesn't support that. Do a big hack + // to pull the MessagePort out of the broadcastchannel. Note this is very specific to Node's code: + // https://github.dev/nodejs/node/blob/214354fc9f3eed8cbd1a1916e1fde100f13f3254/lib/internal/worker/io.js#L432 + const symbols = Object.getOwnPropertySymbols(this.broadcastChannel); + const port = (this.broadcastChannel as any)[symbols[5]] as MessagePort; + port.postMessage(this.createPortBroadcastMessage(this.messageChannel.port2), [this.messageChannel.port2]); + } + + dispose() { + this.messageChannel.port1.close(); + this.messageChannel.port2.close(); + this.broadcastChannel.close(); + } + + protected override postMessage(sharedArrayBuffer: SharedArrayBuffer): void { + this.sendPort.postMessage(sharedArrayBuffer); + } + + private _handleMessage(message: Message) { + try { + if (message.dest === this.connectionId || message.dest === KnownConnectionIds.All) { this.handleMessage(message); - } catch (error) { - RAL().console.error(error); } - }); - } + } catch (error) { + RAL().console.error(error); + } - protected postMessage(sharedArrayBuffer: SharedArrayBuffer) { - this.port.postMessage(sharedArrayBuffer); } } export class ServiceConnection extends BaseServiceConnection { - private readonly port: MessagePort | Worker; + private readonly broadcastChannel: BroadcastChannel; + private readonly clientPorts: Map; - constructor(port: MessagePort | Worker) { - super(); - this.port = port; - this.port.on('message', async (sharedArrayBuffer: SharedArrayBuffer) => { - try { - await this.handleMessage(sharedArrayBuffer); - } catch (error) { - RAL().console.error(error); - } - }); + constructor(channelName: string = BroadcastChannelName) { + super(isMainThread ? KnownConnectionIds.Main : threadId.toString()); + this.broadcastChannel = new BroadcastChannel(channelName); + this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this); + this.clientPorts = new Map(); + } + + dispose() { + this.clientPorts.clear(); + this.broadcastChannel.onmessage = () => {}; + this.broadcastChannel.close(); + } + + protected postMessage(message: Message) { + if (message.dest === KnownConnectionIds.All) { + const clientPorts = [...this.clientPorts.values()]; + clientPorts.forEach(c => c.postMessage(message)); + } else { + const clientPort = this.clientPorts.get(message.dest); + clientPort?.postMessage(message); + } } - protected postMessage(message: Message): void { - this.port.postMessage(message); + protected onBroadcastPort(message: Message): void { + if (message.params && message.src && message.params.port) { + const messagePort = message.params.port as MessagePort; + messagePort.on('message', this.handleMessage.bind(this)); + this.clientPorts.set(message.src, message.params.port as MessagePort); + } } } \ No newline at end of file diff --git a/sync-api-common/src/node/ril.ts b/sync-api-common/src/node/ril.ts index e8c29ec9..dc769b51 100644 --- a/sync-api-common/src/node/ril.ts +++ b/sync-api-common/src/node/ril.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import * as path from 'path'; import { TextDecoder } from 'util'; import { parentPort, Worker } from 'worker_threads'; @@ -18,10 +17,11 @@ class TestServiceConnection { + this.dispose(); return this.worker.terminate(); } } @@ -63,7 +63,7 @@ const _ril: RIL = Object.freeze({ if (!parentPort) { throw new Error(`No parent port defined. Shouldn't happen in test setup`); } - return new ClientConnection(parentPort); + return new ClientConnection(); } }), ServiceConnection: Object.freeze({ diff --git a/sync-api-common/tsconfig.base.json b/sync-api-common/tsconfig.base.json index 3924b7d9..a2c5c87d 100644 --- a/sync-api-common/tsconfig.base.json +++ b/sync-api-common/tsconfig.base.json @@ -7,7 +7,7 @@ "declaration": true, "stripInternal": true, "target": "es2020", - "lib": [ "es2020" ], + "lib": [ "es2020", "DOM" ], "module": "Node16", "moduleResolution": "Node16", } diff --git a/sync-api-tests/src/api.test.ts b/sync-api-tests/src/api.test.ts index 598760a0..19afe7b3 100644 --- a/sync-api-tests/src/api.test.ts +++ b/sync-api-tests/src/api.test.ts @@ -47,6 +47,9 @@ export function contribute(workerResolver: (testCase: string) => string, scheme: }); connection.signalReady(); }); + + connection.terminate(); + if (assertionError !== undefined) { throw new assert.AssertionError(assertionError); } diff --git a/sync-api-tests/tsconfig.base.json b/sync-api-tests/tsconfig.base.json index 3407ef0b..3376de3f 100644 --- a/sync-api-tests/tsconfig.base.json +++ b/sync-api-tests/tsconfig.base.json @@ -7,7 +7,7 @@ "declaration": true, "stripInternal": true, "target": "es2020", - "lib": [ "es2020" ], + "lib": [ "es2020", "DOM" ], "module": "Node16", "moduleResolution": "Node16", "types": [ diff --git a/testbeds/package-lock.json b/testbeds/package-lock.json index e4a7544c..ba709ed1 100644 --- a/testbeds/package-lock.json +++ b/testbeds/package-lock.json @@ -10,7 +10,7 @@ "vscode-uri": "3.0.3" }, "devDependencies": { - "@types/vscode": "^1.67.0" + "@types/vscode": "^1.71.0" } }, "../sync-api-client": { @@ -52,9 +52,10 @@ "devDependencies": {} }, "node_modules/@types/vscode": { - "version": "1.68.0", - "dev": true, - "license": "MIT" + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.72.0.tgz", + "integrity": "sha512-WvHluhUo+lQvE3I4wUagRpnkHuysB4qSyOQUyIAS9n9PYMJjepzTUD8Jyks0YeXoPD0UGctjqp2u84/b3v6Ydw==", + "dev": true }, "node_modules/vscode-uri": { "version": "3.0.3", @@ -63,7 +64,9 @@ }, "dependencies": { "@types/vscode": { - "version": "1.68.0", + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.72.0.tgz", + "integrity": "sha512-WvHluhUo+lQvE3I4wUagRpnkHuysB4qSyOQUyIAS9n9PYMJjepzTUD8Jyks0YeXoPD0UGctjqp2u84/b3v6Ydw==", "dev": true }, "vscode-uri": { diff --git a/testbeds/python/extension.ts b/testbeds/python/extension.ts index b3585fe5..dab70cbd 100644 --- a/testbeds/python/extension.ts +++ b/testbeds/python/extension.ts @@ -23,7 +23,7 @@ export async function activate(_context: ExtensionContext) { const key = Date.now(); const worker = new Worker(path.join(__dirname, './worker.js')); - const connection = new ServiceConnection(worker); + const connection = new ServiceConnection(); const apiService = new ApiService('Python Run', connection, { exitHandler: (_rval) => { connectionState.delete(key); @@ -41,7 +41,7 @@ export async function activate(_context: ExtensionContext) { commands.registerCommand('testbed-python.runInteractive', () => { const key = Date.now(); const worker = new Worker(path.join(__dirname, './worker.js')); - const connection = new ServiceConnection(worker); + const connection = new ServiceConnection(); const apiService = new ApiService('Python Shell', connection, { exitHandler: (_rval) => { connectionState.delete(key); diff --git a/testbeds/python/worker.ts b/testbeds/python/worker.ts index 8560a97b..04877e76 100644 --- a/testbeds/python/worker.ts +++ b/testbeds/python/worker.ts @@ -15,7 +15,7 @@ if (parentPort === null) { process.exit(); } -const connection = new ClientConnection(parentPort); +const connection = new ClientConnection(); connection.serviceReady().then(async (params) => { const name = 'Python Shell'; const apiClient = new ApiClient(connection); @@ -37,7 +37,7 @@ connection.serviceReady().then(async (params) => { mapDir.push({ name: path.posix.join(path.posix.sep, 'workspaces', folder.name), uri: folder.uri }); } } - const pythonRoot = URI.file(`/home/dirkb/Projects/dbaeumer/python-3.11.0rc`); + const pythonRoot = URI.file(`/home/rich/Python-3.11.0b5-wasm32-wasi-16`); mapDir.push({ name: path.posix.sep, uri: pythonRoot }); const exitHandler = (rval: number): void => { apiClient.process.procExit(rval);