mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-09-30 01:07:01 +08:00
[DevTools] Add binary websocket message viewer behind experiment
Screenshots: https://imgur.com/a/0rtukft Bug: 605262 Change-Id: I1049d2196e51193419b9dd7ebbe7105a8655a631 Reviewed-on: https://chromium-review.googlesource.com/c/1321570 Commit-Queue: Joey Arhar <jarhar@chromium.org> Reviewed-by: Dmitry Gozman <dgozman@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#627166} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 9eb859773e133754b856482a4b6ea79ba39412fe
This commit is contained in:
@@ -402,6 +402,7 @@ all_devtools_files = [
|
||||
"front_end/mobile_throttling/ThrottlingSettingsTab.js",
|
||||
"front_end/ndb_app.json",
|
||||
"front_end/network/blockedURLsPane.css",
|
||||
"front_end/network/BinaryResourceView.js",
|
||||
"front_end/network/BlockedURLsPane.js",
|
||||
"front_end/network/eventSourceMessagesView.css",
|
||||
"front_end/network/EventSourceMessagesView.js",
|
||||
@@ -647,6 +648,7 @@ all_devtools_files = [
|
||||
"front_end/snippets/module.json",
|
||||
"front_end/snippets/ScriptSnippetFileSystem.js",
|
||||
"front_end/snippets/SnippetsQuickOpen.js",
|
||||
"front_end/source_frame/BinaryResourceViewFactory.js",
|
||||
"front_end/source_frame/fontView.css",
|
||||
"front_end/source_frame/FontView.js",
|
||||
"front_end/source_frame/imageView.css",
|
||||
|
||||
@@ -106,6 +106,7 @@ Main.Main = class {
|
||||
_initializeExperiments() {
|
||||
// Keep this sorted alphabetically: both keys and values.
|
||||
Runtime.experiments.register('applyCustomStylesheet', 'Allow custom UI themes');
|
||||
Runtime.experiments.register('binaryWebsocketViewer', 'Binary WebSocket Message Viewer');
|
||||
Runtime.experiments.register('blackboxJSFramesOnTimeline', 'Blackbox JavaScript frames on Timeline', true);
|
||||
Runtime.experiments.register('consoleBelowPrompt', 'Console eager evaluation');
|
||||
Runtime.experiments.register('consoleKeyboardNavigation', 'Console keyboard navigation', true);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
Network.BinaryResourceView = class extends UI.VBox {
|
||||
/**
|
||||
* @param {string} base64content
|
||||
* @param {string} contentUrl
|
||||
* @param {!Common.ResourceType} resourceType
|
||||
*/
|
||||
constructor(base64content, contentUrl, resourceType) {
|
||||
super();
|
||||
|
||||
if (!base64content.length) {
|
||||
new UI.EmptyWidget('No data present in selected item').show(this.element);
|
||||
return;
|
||||
}
|
||||
|
||||
const binaryResourceViewFactory =
|
||||
new SourceFrame.BinaryResourceViewFactory(base64content, contentUrl, resourceType);
|
||||
|
||||
const tabbedPane = new UI.TabbedPane();
|
||||
tabbedPane.appendTab(
|
||||
'base64-tab-id', 'Base64', binaryResourceViewFactory.createBase64View(),
|
||||
ls`View selected binary WebSocket message as a Base64 string`);
|
||||
tabbedPane.appendTab(
|
||||
'hex-tab-id', 'Hex', binaryResourceViewFactory.createHexView(),
|
||||
ls`View selected binary WebSocket message as a hexadecimal string`);
|
||||
tabbedPane.appendTab(
|
||||
'hex-viewer-tab-id', 'Hex Viewer', binaryResourceViewFactory.createHexViewerView(),
|
||||
ls`View selected binary WebSocket message in a hex viewer`);
|
||||
tabbedPane.appendTab(
|
||||
'utf8-id', 'UTF-8', binaryResourceViewFactory.createUtf8View(),
|
||||
ls`View selected binary WebSocket message decoded as UTF-8`);
|
||||
tabbedPane.show(this.element);
|
||||
}
|
||||
};
|
||||
@@ -166,19 +166,25 @@ Network.ResourceWebSocketFrameView = class extends UI.VBox {
|
||||
* @param {!Common.Event} event
|
||||
*/
|
||||
async _onFrameSelected(event) {
|
||||
const selectedNode = /** @type {!Network.ResourceWebSocketFrameNode} */ (event.data);
|
||||
this._currentSelectedNode = selectedNode;
|
||||
const contentProvider = selectedNode.contentProvider();
|
||||
let content = await contentProvider.requestContent();
|
||||
if (await contentProvider.contentEncoded())
|
||||
content = window.atob(content);
|
||||
this._currentSelectedNode = /** @type {!Network.ResourceWebSocketFrameNode} */ (event.data);
|
||||
const content = this._currentSelectedNode.dataText();
|
||||
|
||||
if (Runtime.experiments.isEnabled('binaryWebsocketViewer')) {
|
||||
if (this._currentSelectedNode.opCode() === Network.ResourceWebSocketFrameView.OpCodes.BinaryFrame) {
|
||||
this._splitWidget.setSidebarWidget(
|
||||
new Network.BinaryResourceView(content, this._request.url(), Common.resourceTypes.WebSocket));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const jsonView = await SourceFrame.JSONView.createView(content);
|
||||
if (this._currentSelectedNode !== selectedNode)
|
||||
return;
|
||||
if (jsonView)
|
||||
if (jsonView) {
|
||||
this._splitWidget.setSidebarWidget(jsonView);
|
||||
else
|
||||
this._splitWidget.setSidebarWidget(new SourceFrame.ResourceSourceFrame(contentProvider));
|
||||
return;
|
||||
}
|
||||
|
||||
this._splitWidget.setSidebarWidget(new SourceFrame.ResourceSourceFrame(
|
||||
Common.StaticContentProvider.fromString(this._request.url(), Common.resourceTypes.WebSocket, content)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,8 +250,7 @@ Network.ResourceWebSocketFrameNode = class extends DataGrid.SortableDataGridNode
|
||||
* @param {!SDK.NetworkRequest.WebSocketFrame} frame
|
||||
*/
|
||||
constructor(url, frame) {
|
||||
let dataText = frame.text;
|
||||
const length = frame.text.length;
|
||||
let length = frame.text.length;
|
||||
const time = new Date(frame.time * 1000);
|
||||
const timeText = ('0' + time.getHours()).substr(-2) + ':' + ('0' + time.getMinutes()).substr(-2) + ':' +
|
||||
('0' + time.getSeconds()).substr(-2) + '.' + ('00' + time.getMilliseconds()).substr(-3);
|
||||
@@ -253,11 +258,24 @@ Network.ResourceWebSocketFrameNode = class extends DataGrid.SortableDataGridNode
|
||||
timeNode.createTextChild(timeText);
|
||||
timeNode.title = time.toLocaleString();
|
||||
|
||||
let dataText = frame.text;
|
||||
let description = Network.ResourceWebSocketFrameView.opCodeDescription(frame.opCode, frame.mask);
|
||||
const isTextFrame = frame.opCode === Network.ResourceWebSocketFrameView.OpCodes.TextFrame;
|
||||
if (!isTextFrame)
|
||||
dataText = Network.ResourceWebSocketFrameView.opCodeDescription(frame.opCode, frame.mask);
|
||||
|
||||
super({data: dataText, length: length, time: timeNode});
|
||||
if (isTextFrame) {
|
||||
description = dataText;
|
||||
|
||||
} else if (
|
||||
Runtime.experiments.isEnabled('binaryWebsocketViewer') &&
|
||||
frame.opCode === Network.ResourceWebSocketFrameView.OpCodes.BinaryFrame) {
|
||||
length = base64ToSize(frame.text) + ' bytes';
|
||||
description = 'Binary Message';
|
||||
|
||||
} else {
|
||||
dataText = description;
|
||||
}
|
||||
|
||||
super({data: description, length: length, time: timeNode});
|
||||
|
||||
this._url = url;
|
||||
this._frame = frame;
|
||||
@@ -289,10 +307,17 @@ Network.ResourceWebSocketFrameNode = class extends DataGrid.SortableDataGridNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Common.ContentProvider}
|
||||
* @return {string}
|
||||
*/
|
||||
contentProvider() {
|
||||
return Common.StaticContentProvider.fromString(this._url, Common.resourceTypes.WebSocket, this._dataText);
|
||||
dataText() {
|
||||
return this._dataText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Network.ResourceWebSocketFrameView.OpCodes}
|
||||
*/
|
||||
opCode() {
|
||||
return /** @type {!Network.ResourceWebSocketFrameView.OpCodes} */ (this._frame.opCode);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"persistence"
|
||||
],
|
||||
"scripts": [
|
||||
"BinaryResourceView.js",
|
||||
"BlockedURLsPane.js",
|
||||
"EventSourceMessagesView.js",
|
||||
"HARWriter.js",
|
||||
|
||||
@@ -1446,3 +1446,18 @@ function singleton(constructorFunction) {
|
||||
constructorFunction[_singletonSymbol] = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} content
|
||||
* @return {number}
|
||||
*/
|
||||
function base64ToSize(content) {
|
||||
if (!content)
|
||||
return 0;
|
||||
let size = content.length * 3 / 4;
|
||||
if (content[content.length - 1] === '=')
|
||||
size--;
|
||||
if (content.length > 1 && content[content.length - 2] === '=')
|
||||
size--;
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
SourceFrame.BinaryResourceViewFactory = class {
|
||||
/**
|
||||
* @param {string} base64content
|
||||
* @param {string} contentUrl
|
||||
* @param {!Common.ResourceType} resourceType
|
||||
*/
|
||||
constructor(base64content, contentUrl, resourceType) {
|
||||
this._base64content = base64content;
|
||||
this._contentUrl = contentUrl;
|
||||
this._resourceType = resourceType;
|
||||
/** @type {?Promise<!Uint8Array>} */
|
||||
this._arrayPromise = null;
|
||||
}
|
||||
|
||||
async _fetchContentAsArray() {
|
||||
if (!this._arrayPromise) {
|
||||
this._arrayPromise = new Promise(async resolve => {
|
||||
const fetchResponse = await fetch('data:;base64,' + this._base64content);
|
||||
resolve(new Uint8Array(await fetchResponse.arrayBuffer()));
|
||||
});
|
||||
}
|
||||
return await this._arrayPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SourceFrame.ResourceSourceFrame}
|
||||
*/
|
||||
createBase64View() {
|
||||
return new SourceFrame.ResourceSourceFrame(
|
||||
Common.StaticContentProvider.fromString(this._contentUrl, this._resourceType, this._base64content),
|
||||
/* autoPrettyPrint */ false, {lineNumbers: false, lineWrapping: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SourceFrame.ResourceSourceFrame}
|
||||
*/
|
||||
createHexView() {
|
||||
const hexContentProvider = new Common.StaticContentProvider(
|
||||
this._contentUrl, this._resourceType,
|
||||
async () => SourceFrame.BinaryResourceViewFactory.uint8ArrayToHexString(await this._fetchContentAsArray()));
|
||||
return new SourceFrame.ResourceSourceFrame(
|
||||
hexContentProvider,
|
||||
/* autoPrettyPrint */ false, {lineNumbers: false, lineWrapping: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SourceFrame.ResourceSourceFrame}
|
||||
*/
|
||||
createHexViewerView() {
|
||||
const hexViewerContentProvider = new Common.StaticContentProvider(
|
||||
this._contentUrl, this._resourceType,
|
||||
async () => SourceFrame.BinaryResourceViewFactory.uint8ArrayToHexViewer(await this._fetchContentAsArray()));
|
||||
return new SourceFrame.ResourceSourceFrame(
|
||||
hexViewerContentProvider,
|
||||
/* autoPrettyPrint */ false, {lineNumbers: false, lineWrapping: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SourceFrame.ResourceSourceFrame}
|
||||
*/
|
||||
createUtf8View() {
|
||||
const utf8ContentProvider = new Common.StaticContentProvider(
|
||||
this._contentUrl, this._resourceType,
|
||||
async () => new TextDecoder('utf8').decode(await this._fetchContentAsArray()));
|
||||
return new SourceFrame.ResourceSourceFrame(
|
||||
utf8ContentProvider,
|
||||
/* autoPrettyPrint */ false, {lineNumbers: true, lineWrapping: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Uint8Array} uint8Array
|
||||
* @return {string}
|
||||
*/
|
||||
static uint8ArrayToHexString(uint8Array) {
|
||||
let output = '';
|
||||
for (let i = 0; i < uint8Array.length; i++)
|
||||
output += SourceFrame.BinaryResourceViewFactory.numberToHex(uint8Array[i], 2);
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} number
|
||||
* @param {number} padding
|
||||
* @return {string}
|
||||
*/
|
||||
static numberToHex(number, padding) {
|
||||
let hex = number.toString(16);
|
||||
while (hex.length < padding)
|
||||
hex = '0' + hex;
|
||||
return hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Uint8Array} array
|
||||
* @return {string}
|
||||
*/
|
||||
static uint8ArrayToHexViewer(array) {
|
||||
let output = '';
|
||||
let line = 0;
|
||||
|
||||
while ((line * 16) < array.length) {
|
||||
const lineArray = array.slice(line * 16, (line + 1) * 16);
|
||||
|
||||
// line number
|
||||
output += SourceFrame.BinaryResourceViewFactory.numberToHex(line, 8) + ':';
|
||||
|
||||
// hex
|
||||
let hexColsPrinted = 0;
|
||||
for (let i = 0; i < lineArray.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
output += ' ';
|
||||
hexColsPrinted++;
|
||||
}
|
||||
output += SourceFrame.BinaryResourceViewFactory.numberToHex(lineArray[i], 2);
|
||||
hexColsPrinted += 2;
|
||||
}
|
||||
|
||||
// hex-ascii padding
|
||||
while (hexColsPrinted < 42) {
|
||||
output += ' ';
|
||||
hexColsPrinted++;
|
||||
}
|
||||
|
||||
// ascii
|
||||
for (let i = 0; i < lineArray.length; i++) {
|
||||
const code = lineArray[i];
|
||||
if (code >= 32 && code <= 126) {
|
||||
// printable ascii character
|
||||
output += String.fromCharCode(code);
|
||||
} else {
|
||||
// non-printable
|
||||
output += '.';
|
||||
}
|
||||
}
|
||||
|
||||
output += '\n';
|
||||
line++;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
};
|
||||
@@ -103,28 +103,13 @@ SourceFrame.ImageView = class extends UI.SimpleView {
|
||||
imageSrc = this._url;
|
||||
const loadPromise = new Promise(x => this._imagePreviewElement.onload = x);
|
||||
this._imagePreviewElement.src = imageSrc;
|
||||
const size = content && !contentEncoded ? content.length : this._base64ToSize(content);
|
||||
const size = content && !contentEncoded ? content.length : base64ToSize(content);
|
||||
this._sizeLabel.setText(Number.bytesToString(size));
|
||||
await loadPromise;
|
||||
this._dimensionsLabel.setText(
|
||||
Common.UIString('%d × %d', this._imagePreviewElement.naturalWidth, this._imagePreviewElement.naturalHeight));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} content
|
||||
* @return {number}
|
||||
*/
|
||||
_base64ToSize(content) {
|
||||
if (!content)
|
||||
return 0;
|
||||
let size = content.length * 3 / 4;
|
||||
if (content[content.length - 1] === '=')
|
||||
size--;
|
||||
if (content.length > 1 && content[content.length - 2] === '=')
|
||||
size--;
|
||||
return size;
|
||||
}
|
||||
|
||||
_contextMenu(event) {
|
||||
const contextMenu = new UI.ContextMenu(event);
|
||||
if (!this._parsedURL.isDataURL())
|
||||
|
||||
@@ -34,14 +34,15 @@ SourceFrame.ResourceSourceFrame = class extends SourceFrame.SourceFrame {
|
||||
/**
|
||||
* @param {!Common.ContentProvider} resource
|
||||
* @param {boolean=} autoPrettyPrint
|
||||
* @param {!UI.TextEditor.Options=} codeMirrorOptions
|
||||
*/
|
||||
constructor(resource, autoPrettyPrint) {
|
||||
constructor(resource, autoPrettyPrint, codeMirrorOptions) {
|
||||
super(async () => {
|
||||
let content = await resource.requestContent();
|
||||
if (await resource.contentEncoded())
|
||||
content = window.atob(content);
|
||||
return content;
|
||||
});
|
||||
}, codeMirrorOptions);
|
||||
this._resource = resource;
|
||||
this.setCanPrettyPrint(this._resource.contentType().isDocumentOrScriptOrStyleSheet(), autoPrettyPrint);
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
SourceFrame.SourceFrame = class extends UI.SimpleView {
|
||||
/**
|
||||
* @param {function(): !Promise<?string>} lazyContent
|
||||
* @param {!UI.TextEditor.Options=} codeMirrorOptions
|
||||
*/
|
||||
constructor(lazyContent) {
|
||||
constructor(lazyContent, codeMirrorOptions) {
|
||||
super(Common.UIString('Source'));
|
||||
|
||||
this._lazyContent = lazyContent;
|
||||
@@ -56,7 +57,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView {
|
||||
this._shouldAutoPrettyPrint = false;
|
||||
this._prettyToggle.setVisible(false);
|
||||
|
||||
this._textEditor = new SourceFrame.SourcesTextEditor(this);
|
||||
this._textEditor = new SourceFrame.SourcesTextEditor(this, codeMirrorOptions);
|
||||
this._textEditor.show(this.element);
|
||||
|
||||
/** @type {?number} */
|
||||
|
||||
@@ -7,14 +7,19 @@
|
||||
SourceFrame.SourcesTextEditor = class extends TextEditor.CodeMirrorTextEditor {
|
||||
/**
|
||||
* @param {!SourceFrame.SourcesTextEditorDelegate} delegate
|
||||
* @param {!UI.TextEditor.Options=} codeMirrorOptions
|
||||
*/
|
||||
constructor(delegate) {
|
||||
super({
|
||||
constructor(delegate, codeMirrorOptions) {
|
||||
const defaultCodeMirrorOptions = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
bracketMatchingSetting: Common.moduleSetting('textEditorBracketMatching'),
|
||||
padBottom: true
|
||||
});
|
||||
};
|
||||
if (codeMirrorOptions)
|
||||
Object.assign(defaultCodeMirrorOptions, codeMirrorOptions);
|
||||
|
||||
super(defaultCodeMirrorOptions);
|
||||
|
||||
this.codeMirror().addKeyMap({'Enter': 'smartNewlineAndIndent', 'Esc': 'sourcesDismiss'});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"workspace_diff"
|
||||
],
|
||||
"scripts": [
|
||||
"BinaryResourceViewFactory.js",
|
||||
"SourcesTextEditor.js",
|
||||
"FontView.js",
|
||||
"ImageView.js",
|
||||
|
||||
Reference in New Issue
Block a user