| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {Base64} from 'js-base64'; |
| import RFB, {DataChannel} from '@novnc/novnc/core/rfb'; |
| import * as webviewShared from '../../src/features/device_management/webview_shared'; |
| |
| const vscode = acquireVsCodeApi<never>(); |
| |
| // Type-safe wrapper of vscode.postMessage(). |
| function postClientMessage(message: webviewShared.ClientMessage): void { |
| vscode.postMessage(message); |
| } |
| |
| // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState |
| const READY_STATE = { |
| connecting: 0, |
| open: 1, |
| closing: 2, |
| closed: 3, |
| } as const; |
| |
| // Implements DataChannel over the WebView's message passing mechanism. |
| class MessagePassingDataChannel implements DataChannel { |
| private static nextSocketId = 1; |
| |
| private readonly socketId = MessagePassingDataChannel.nextSocketId++; |
| private readyStateValue: number = READY_STATE.connecting; |
| // Cached for window.removeEventListener(). |
| private readonly onMessageListener = this.onMessage.bind(this); |
| |
| constructor() { |
| postClientMessage({ |
| type: 'socket', |
| subtype: 'open', |
| socketId: this.socketId, |
| }); |
| window.addEventListener('message', this.onMessageListener); |
| } |
| |
| get binaryType(): string { |
| return 'arraybuffer'; |
| } |
| |
| set binaryType(t: string) { |
| if (t !== 'arraybuffer') { |
| throw new Error( |
| `MessagePassingDataChannel: unsupported binary type ${t} requested` |
| ); |
| } |
| } |
| |
| readonly protocol = webviewShared.MESSAGE_PASSING_URL; |
| |
| get readyState(): number { |
| return this.readyStateValue; |
| } |
| |
| onopen: ((ev: Event) => void) | null = null; |
| onmessage: ((ev: MessageEvent) => void) | null = null; |
| onerror: ((ev: Event) => void) | null = null; |
| onclose: ((ev: CloseEvent) => void) | null = null; |
| |
| send(data: ArrayBuffer): void { |
| postClientMessage({ |
| type: 'socket', |
| subtype: 'data', |
| socketId: this.socketId, |
| data: Base64.fromUint8Array(new Uint8Array(data)), |
| }); |
| } |
| |
| close(): void { |
| postClientMessage({ |
| type: 'socket', |
| subtype: 'close', |
| socketId: this.socketId, |
| }); |
| window.removeEventListener('message', this.onMessageListener); |
| this.readyStateValue = READY_STATE.closed; |
| } |
| |
| private onMessage(ev: MessageEvent<webviewShared.ServerMessage>): void { |
| const message = ev.data; |
| const {type, subtype} = message; |
| if (type !== 'socket') { |
| return; |
| } |
| const {socketId} = message; |
| if (socketId !== this.socketId) { |
| return; |
| } |
| |
| switch (subtype) { |
| case 'open': { |
| const ev = new Event('open'); |
| this.readyStateValue = READY_STATE.open; |
| if (this.onopen) { |
| this.onopen(ev); |
| } |
| break; |
| } |
| case 'data': { |
| const {data} = message; |
| if (typeof data !== 'string') { |
| break; |
| } |
| const array = Base64.toUint8Array(data); |
| const ev = new MessageEvent('message', {data: array.buffer}); |
| if (this.onmessage) { |
| this.onmessage(ev); |
| } |
| break; |
| } |
| case 'error': { |
| const ev = new Event('error'); |
| this.readyStateValue = READY_STATE.closing; |
| if (this.onerror) { |
| this.onerror(ev); |
| } |
| break; |
| } |
| case 'close': { |
| this.readyStateValue = READY_STATE.closed; |
| const ev = new CloseEvent('close'); |
| if (this.onclose) { |
| this.onclose(ev); |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| function openDataChannel(url: string): DataChannel { |
| if (url === webviewShared.MESSAGE_PASSING_URL) { |
| return new MessagePassingDataChannel(); |
| } |
| return new WebSocket(url); |
| } |
| |
| function onReady(): void { |
| const spinner = document.getElementById('loading')!; |
| const container = document.getElementById('main')!; |
| const proxyUrl = container.dataset.webSocketProxyUrl!; |
| const rfb = new RFB(container, openDataChannel(proxyUrl)); |
| rfb.scaleViewport = true; |
| rfb.addEventListener('connect', () => { |
| spinner.classList.add('loaded'); |
| postClientMessage({type: 'event', subtype: 'connect'}); |
| }); |
| rfb.addEventListener('disconnect', () => { |
| postClientMessage({type: 'event', subtype: 'disconnect'}); |
| }); |
| } |
| |
| function main(): void { |
| postClientMessage({ |
| type: 'event', |
| subtype: 'ready', |
| }); |
| |
| window.addEventListener( |
| 'message', |
| (ev: MessageEvent<webviewShared.ServerMessage>) => { |
| const {type, subtype} = ev.data; |
| if (type === 'event' && subtype === 'ready') { |
| onReady(); |
| } |
| } |
| ); |
| } |
| |
| main(); |