blob: ee753dfe89764bdce13d2d6d42cefb4194f334b0 [file] [log] [blame] [edit]
// 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();