[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:
Joey Arhar
2019-01-29 22:07:02 +00:00
committed by Commit Bot
parent 49cda6a73d
commit 19e85ccca5
12 changed files with 261 additions and 42 deletions
+2
View File
@@ -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",
+1
View File
@@ -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);
+37
View File
@@ -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);
}
};
+44 -19
View File
@@ -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);
}
};
+1
View File
@@ -182,6 +182,7 @@
"persistence"
],
"scripts": [
"BinaryResourceView.js",
"BlockedURLsPane.js",
"EventSourceMessagesView.js",
"HARWriter.js",
+15
View File
@@ -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;
}
};
+1 -16
View File
@@ -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);
}
+3 -2
View File
@@ -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} */
+8 -3
View File
@@ -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'});
+1
View File
@@ -41,6 +41,7 @@
"workspace_diff"
],
"scripts": [
"BinaryResourceViewFactory.js",
"SourcesTextEditor.js",
"FontView.js",
"ImageView.js",