chrome-devtools-frontend
Version:
Chrome DevTools UI
245 lines (213 loc) • 8.21 kB
text/typescript
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../core/i18n/i18n.js';
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import type * as Platform from '../platform/platform.js';
import * as ProtocolClient from '../protocol_client/protocol_client.js';
import * as Root from '../root/root.js';
import {RehydratingConnectionTransport} from './RehydratingConnection.js';
const UIStrings = {
/**
* @description Text on the remote debugging window to indicate the connection is lost
*/
websocketDisconnected: 'WebSocket disconnected',
} as const;
const str_ = i18n.i18n.registerUIStrings('core/sdk/Connections.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class MainConnection implements ProtocolClient.ConnectionTransport.ConnectionTransport {
onMessage: ((arg0: Object|string) => void)|null = null;
#onDisconnect: ((arg0: string) => void)|null = null;
#messageBuffer = '';
#messageSize = 0;
readonly #eventListeners: Common.EventTarget.EventDescriptor[];
constructor() {
this.#eventListeners = [
Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
Host.InspectorFrontendHostAPI.Events.DispatchMessage, this.dispatchMessage, this),
Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
Host.InspectorFrontendHostAPI.Events.DispatchMessageChunk, this.dispatchMessageChunk, this),
];
}
setOnMessage(onMessage: (arg0: Object|string) => void): void {
this.onMessage = onMessage;
}
setOnDisconnect(onDisconnect: (arg0: string) => void): void {
this.#onDisconnect = onDisconnect;
}
sendRawMessage(message: string): void {
if (this.onMessage) {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.sendMessageToBackend(message);
}
}
private dispatchMessage(event: Common.EventTarget.EventTargetEvent<string>): void {
if (this.onMessage) {
this.onMessage.call(null, event.data);
}
}
private dispatchMessageChunk(
event: Common.EventTarget.EventTargetEvent<Host.InspectorFrontendHostAPI.DispatchMessageChunkEvent>): void {
const {messageChunk, messageSize} = event.data;
if (messageSize) {
this.#messageBuffer = '';
this.#messageSize = messageSize;
}
this.#messageBuffer += messageChunk;
if (this.#messageBuffer.length === this.#messageSize && this.onMessage) {
this.onMessage.call(null, this.#messageBuffer);
this.#messageBuffer = '';
this.#messageSize = 0;
}
}
async disconnect(): Promise<void> {
const onDisconnect = this.#onDisconnect;
Common.EventTarget.removeEventListeners(this.#eventListeners);
this.#onDisconnect = null;
this.onMessage = null;
if (onDisconnect) {
onDisconnect.call(null, 'force disconnect');
}
}
}
export class WebSocketTransport implements ProtocolClient.ConnectionTransport.ConnectionTransport {
#socket: WebSocket|null;
onMessage: ((arg0: Object|string) => void)|null = null;
#onDisconnect: ((arg0: string) => void)|null = null;
#onWebSocketDisconnect: ((message: Platform.UIString.LocalizedString) => void)|null;
#connected = false;
#messages: string[] = [];
constructor(
url: Platform.DevToolsPath.UrlString,
onWebSocketDisconnect: (message: Platform.UIString.LocalizedString) => void) {
this.#socket = new WebSocket(url);
this.#socket.onerror = this.onError.bind(this);
this.#socket.onopen = this.onOpen.bind(this);
this.#socket.onmessage = (messageEvent: MessageEvent<string>): void => {
if (this.onMessage) {
this.onMessage.call(null, messageEvent.data);
}
};
this.#socket.onclose = this.onClose.bind(this);
this.#onWebSocketDisconnect = onWebSocketDisconnect;
}
setOnMessage(onMessage: (arg0: Object|string) => void): void {
this.onMessage = onMessage;
}
setOnDisconnect(onDisconnect: (arg0: string) => void): void {
this.#onDisconnect = onDisconnect;
}
private onError(): void {
if (this.#onWebSocketDisconnect) {
this.#onWebSocketDisconnect.call(null, i18nString(UIStrings.websocketDisconnected));
}
if (this.#onDisconnect) {
// This is called if error occurred while connecting.
this.#onDisconnect.call(null, 'connection failed');
}
this.close();
}
private onOpen(): void {
this.#connected = true;
if (this.#socket) {
this.#socket.onerror = console.error;
for (const message of this.#messages) {
this.#socket.send(message);
}
}
this.#messages = [];
}
private onClose(): void {
if (this.#onWebSocketDisconnect) {
this.#onWebSocketDisconnect.call(null, i18nString(UIStrings.websocketDisconnected));
}
if (this.#onDisconnect) {
this.#onDisconnect.call(null, 'websocket closed');
}
this.close();
}
private close(callback?: (() => void)): void {
if (this.#socket) {
this.#socket.onerror = null;
this.#socket.onopen = null;
this.#socket.onclose = callback || null;
this.#socket.onmessage = null;
this.#socket.close();
this.#socket = null;
}
this.#onWebSocketDisconnect = null;
}
sendRawMessage(message: string): void {
if (this.#connected && this.#socket) {
this.#socket.send(message);
} else {
this.#messages.push(message);
}
}
disconnect(): Promise<void> {
return new Promise(fulfill => {
this.close(() => {
if (this.#onDisconnect) {
this.#onDisconnect.call(null, 'force disconnect');
}
fulfill();
});
});
}
}
export class StubTransport implements ProtocolClient.ConnectionTransport.ConnectionTransport {
onMessage: ((arg0: Object|string) => void)|null = null;
#onDisconnect: ((arg0: string) => void)|null = null;
setOnMessage(onMessage: (arg0: Object|string) => void): void {
this.onMessage = onMessage;
}
setOnDisconnect(onDisconnect: (arg0: string) => void): void {
this.#onDisconnect = onDisconnect;
}
sendRawMessage(message: string): void {
window.setTimeout(this.respondWithError.bind(this, message), 0);
}
private respondWithError(message: string): void {
const messageObject = JSON.parse(message);
const error = {
message: 'This is a stub connection, can\'t dispatch message.',
code: ProtocolClient.CDPConnection.CDPErrorStatus.DEVTOOLS_STUB_ERROR,
data: messageObject,
};
if (this.onMessage) {
this.onMessage.call(null, {id: messageObject.id, error});
}
}
async disconnect(): Promise<void> {
if (this.#onDisconnect) {
this.#onDisconnect.call(null, 'force disconnect');
}
this.#onDisconnect = null;
this.onMessage = null;
}
}
export async function initMainConnection(
createRootTarget: () => Promise<void>,
onConnectionLost: (message: Platform.UIString.LocalizedString) => void): Promise<void> {
ProtocolClient.ConnectionTransport.ConnectionTransport.setFactory(createMainTransport.bind(null, onConnectionLost));
await createRootTarget();
Host.InspectorFrontendHost.InspectorFrontendHostInstance.connectionReady();
}
function createMainTransport(onConnectionLost: (message: Platform.UIString.LocalizedString) => void):
ProtocolClient.ConnectionTransport.ConnectionTransport {
if (Root.Runtime.Runtime.isTraceApp()) {
return new RehydratingConnectionTransport(onConnectionLost);
}
const wsParam = Root.Runtime.Runtime.queryParam('ws');
const wssParam = Root.Runtime.Runtime.queryParam('wss');
if (wsParam || wssParam) {
const ws = (wsParam ? `ws://${wsParam}` : `wss://${wssParam}`) as Platform.DevToolsPath.UrlString;
return new WebSocketTransport(ws, onConnectionLost);
}
const notEmbeddedOrWs = Host.InspectorFrontendHost.InspectorFrontendHostInstance.isHostedMode();
if (notEmbeddedOrWs) {
// eg., hosted mode (e.g. `http://localhost:9222/devtools/inspector.html`) without a WebSocket URL,
return new StubTransport();
}
return new MainConnection();
}