Skip to content

Commit

Permalink
feat: EME Logger Manifest V3 Migration (#51)
Browse files Browse the repository at this point in the history
The Chrome browser will no longer run Manifest V2 extensions after January 2023 as per [The transition of Chrome extensions to Manifest V3](https://developer.chrome.com/blog/mv2-transition/). Hence, EME Logger's Manifest version needs to be updated from V2 to V3:

* log-window.js:
    * The background page is replaced with a service worker since the background page cannot be used with manifest v3.
    * `chrome.extension.getURL` is replaced with `chrome.runtime.getURL`.
    * Removed this line `window.EmeLogWindow = EmeLogWindow;` because `window` is not defined in the log_window.js since the script is running in a service worker (a separate thread). Now the singleton instance of EmeLogWindow (`EmeLogWindow.instance`) lives in the service worker.
    * Replaced `window.open` with `chrome.windows.create` to create a new window
    * `chrome.browerAction` is replaced with `chrome.action`.
    * Moved the logic handling DOM from the service worker (log-window.js) to the log window (log.js) and communicated between two scripts via chrome message handling APIs since a service worker cannot access the `document` or DOM object. As a result, `EmeLogWindow.appendLog()` and `clear()` code dealing with the document object has been moved to the log window script (log.js) 
    * Used `chrome.downloads.download` APIwith adding "downloads" to the "permission" section in the manifest file since `URL.createObjectURL` is not available in a service worker.
* manifest.json:
    * `web_accessible_resources` is now an array of objects instead of a single object.
    * `browser_action` is replaced with `action`.
    * Added "<all_urls>" into `matches` in the `web_accessible_resources` section because no log is getting collected due to the permission issue.
* log.js:
    * Used `chrome.runtime.onMessage.addListener` and `chrome.runtime.sendMessage` APIs to communicate because the pop-up window (log.js) cannot access the EmeLogWindow instance in the service worker (log-window.js).
    * Used `chrome.windows.onRemoved.addListener` to observe the termination of window because there is no `.closed `property in the window created by `chrome.windows.create`.
* content-script.js:
    * `chrome.extension.getURL` is replaced with `chrome.runtime.getURL`.
* spec/log-window-tests.js:
    * Added the mock & fake properties/methods for `chrome.runtime` and `chrome.windows` APIs because Jasmine-browser and Selenium-webdriver are not working with chrome APIs and we need test code changes to test async calls.
    * Updated the required dependency packages: `npm run build & npm test` complains about the out-dated versions of selenium-webdriver and other dependencies
    * Used Promise/async-wait patterns for `EmeLogWindow.instance.open()` API tests. Otherwise, EmeLogWindow.instance.isOpen()'s return value is not guaranteed since `.open()` is an async operation.
    * Added new test cases for `EmeLogWindow.instance.appendLog()` and `EmeLogWindow.instance.clear()` APIs
* spec/support/jasmine-browser.json:
    * Added "log.js" to the `srcFiles` section because we cannot directly test the `appendLog()` logic without the chrome messaging APIs that are not available on the Jasmine framework. As a workaround, log-window-tests.js calls the `appendLog()` method defined in the log.js first and passes it down `EmeLogWindow.appendLog()` in the tests.
* package-lock.json:
    * Updated selenium-webdriver to the latest version (4.3.1) to fix the Invalid URL issue by selenium-webdriver when running `npm test`. Other dependencies are updated together.

Co-authored-by: Sangbaek Park <[email protected]>
  • Loading branch information
beback4u and beback4u authored Aug 1, 2022
1 parent d5d15f6 commit c860eba
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 342 deletions.
4 changes: 2 additions & 2 deletions content-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// Load required scripts into the current web page.
const urls = ['/trace-anything.js', '/eme-trace-config.js'];
for (const url of urls) {
const absoluteUrl = chrome.extension.getURL(url);
const absoluteUrl = chrome.runtime.getURL(url);

// Insert a script tag and force it to load synchronously.
const script = document.createElement('script');
Expand All @@ -34,6 +34,6 @@ for (const url of urls) {
// message to the background page.
window.addEventListener('message', (event) => {
if (event.data.type == 'emeTraceLog') {
chrome.runtime.sendMessage({log: event.data.log});
chrome.runtime.sendMessage({ log: event.data.log });
}
});
262 changes: 43 additions & 219 deletions log-window.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @fileoverview Implements the log window.
* @fileoverview Creates and communicates with the actual log window.
*/

class EmeLogWindow {
Expand All @@ -26,261 +26,85 @@ class EmeLogWindow {
}

/** Open the log window. */
open() {
async open() {
if (!this.isOpen()) {
this.logWindow_ = window.open(
'log.html', 'EME Log', 'width=700,height=600');
this.logWindow_ = await chrome.windows.create({
url: chrome.runtime.getURL('log.html'),
type: 'popup',
height: 600,
width: 700,
});

// Inject a copy of this class into the window so that it can get the log
// URI later.
this.logWindow_.EmeLogWindow = EmeLogWindow;
}

// Bring the window to the foreground.
this.logWindow_.focus();
chrome.windows.update(this.logWindow_.id, { focused: true });
}

/**
* Close the log window.
* @param {Number} Window ID
*/
close(windowId) {
if (this.logWindow_ != null && this.logWindow_.id == windowId) {
this.logWindow_ = null;
this.clear();
}
}

/** @return {boolean} True if the log window is open. */
isOpen() {
return this.logWindow_ != null && !this.logWindow_.closed;
return this.logWindow_ != null;
}

/** @return {string} A URI to download the log as a text file. */
getTextLogUri() {
const blob = new Blob([this.textLogs_], {type: 'text/plain'});
return URL.createObjectURL(blob);
/** @return {string} The text log. */
getTextLogs() {
return this.textLogs_;
}

/** Clear the log window. */
clear() {
const document = this.logWindow_.document;

const list = document.getElementById('eme-log');
while (list.hasChildNodes()) {
list.removeChild(list.firstChild);
}

this.textLogs_ = '';
}

/**
* @param {Object} The serialized log to format in HTML.
*/
appendLog(log) {
if (!this.logWindow_) {
return;
}

const document = this.logWindow_.document;
const logElement = document.querySelector('#eme-log');
const li = document.createElement('li');
logElement.appendChild(li);

const heading = document.createElement('h3');
li.appendChild(heading);

const time = document.createElement('div');
time.classList.add('time');
heading.appendChild(time);
heading.appendChild(document.createElement('br'));

const instanceId = document.createElement('div');
instanceId.classList.add('instance-id');
heading.appendChild(instanceId);
heading.appendChild(document.createElement('br'));

const title = document.createElement('div');
title.classList.add('title');
heading.appendChild(title);

const timestamp = new Date(log.timestamp);
const formattedTimestamp = timestamp.toString();

time.textContent = formattedTimestamp;
if (log.duration) {
time.textContent += ` - duration: ${log.duration.toFixed(1)} ms`;
/** @param {string} The text log. */
appendLog(textLog) {
if (this.logWindow_) {
this.textLogs_ += textLog;
}

instanceId.textContent = log.instanceId;

const data = document.createElement('pre');
data.classList.add('data');
li.appendChild(data);

if (log.type == 'Warning') {
title.textContent = 'WARNING';
title.classList.add('warning');
data.textContent = log.message;
}

if (log.type == 'Constructor') {
title.textContent = `new ${log.className}`;
} else if (log.type == 'Method') {
title.textContent = `${log.className}.${log.methodName}`;
} else if (log.type == 'Getter' || log.type == 'Setter') {
title.textContent = `${log.className}.${log.memberName}`;
} else if (log.type == 'Event') {
title.textContent = `${log.className} ${log.eventName} Event`;
}

if (log.type == 'Constructor' || log.type == 'Method') {
const args = log.args.map(arg => prettyPrint(arg)).join(', ');
data.textContent = `${title.textContent}(${args})`;

if (log.threw) {
data.textContent += ` threw ${prettyPrint(log.threw)}`;
} else {
data.textContent += ` => ${prettyPrint(log.result)}`;
}
} else if (log.type == 'Getter') {
data.textContent = title.textContent;

if (log.threw) {
data.textContent += ` threw ${prettyPrint(log.threw)}`;
} else {
data.textContent += ` => ${prettyPrint(log.result)}`;
}
} else if (log.type == 'Setter') {
data.textContent = title.textContent;

if (log.threw) {
data.textContent += ` threw ${prettyPrint(log.threw)}`;
} else {
data.textContent += ` => ${prettyPrint(log.value)}`;
}
} else if (log.type == 'Event') {
data.textContent = `${log.className} `;
if (!log.event.__type__) {
// If the event object didn't properly inherit from Event, then we may
// be missing type info. Construct it now with the event name.
data.textContent += `${log.eventName} Event instance `;
}
data.textContent += prettyPrint(log.event);
if ('value' in log) {
data.textContent += '\nAssociated value: ' + prettyPrint(log.value);
}
}

const textBasedLog =
formattedTimestamp + '\n\n' +
instanceId.textContent + '\n' +
data.textContent + '\n\n\n\n';

this.textLogs_ += textBasedLog;
}
}

EmeLogWindow.instance = new EmeLogWindow();
window.EmeLogWindow = EmeLogWindow;


/**
* @param {number} byte
* @return {string}
*/
function byteToHex(byte) {
return '0x' + byte.toString(16).padStart(2, '0');
}

/**
* @param {*} obj
* @param {string} indentation
* @return {string}
*/
function prettyPrint(obj, indentation = '') {
if (obj == null) {
return obj;
}

// If it's a named type, unpack it and attach the name.
if (obj.__type__) {
let format = obj.__type__ + ' instance';

// This has fields like an object.
if (obj.__fields__) {
format += ' ' + prettyPrint(obj.__fields__, indentation);
}

// This has a data array like an ArrayBufferView.
// TODO: Handle formatting for 16-bit and 32-bit values?
if (obj.__data__) {
const data = obj.__data__.slice(); // Make a copy
if (data.length == 0) {
format += '[]';
} else {
format += ' ' + '[\n';
while (data.length) {
const row = data.splice(0, 16);
format += indentation + ' ';
format += row.map(byteToHex).join(', ');
format += ',\n';
}
format += indentation + ']';
}
}
return format;
}

if (Array.isArray(obj)) {
// More compact representations for empty or 1-element arrays.
if (obj.length == 0) {
return '[]';
}
if (obj.length == 1) {
return `[${prettyPrint(obj[0], indentation)}]`;
}

let insides = '';
for (const entry of obj) {
insides += indentation + ' ';
insides += prettyPrint(entry, indentation + ' ') + ',\n';
}
return `[\n${insides}${indentation}]`;
}

if (obj.constructor == Object) {
const keys = Object.keys(obj);

// More compact representations for empty or 1-element objects.
if (keys.length == 0) {
return '{}';
}
if (keys.length == 1) {
return `{${keys[0]}: ${prettyPrint(obj[keys[0]], indentation)}}`;
}

let insides = '';
for (const key of keys) {
insides += indentation + ' ' + key + ': ';
insides += prettyPrint(obj[key], indentation + ' ') + ',\n';
}
return `{\n${insides}${indentation}}`;
}

if (typeof obj == 'string') {
return `"${obj}"`;
}

return obj.toString();
}

// NOTE: These APIs are not defined in our test environment, but should always
// be present when this is run as a Chrome extension.
if (chrome.runtime !== undefined) {
/**
* Listens for messages from the content script to append a log item to the
* current frame and log file.
* When a window is closed, clear the handle and logs if it was the log window.
*/
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
const log = request.log;
EmeLogWindow.instance.appendLog(log);
chrome.windows.onRemoved.addListener((windowId) => {
EmeLogWindow.instance.close(windowId);
});

/**
* When the extension icon is clicked, open the log window if it doesn't exist,
* and bring it to the front otherwise.
*/
chrome.browserAction.onClicked.addListener((tab) => {
EmeLogWindow.instance.open();
chrome.action.onClicked.addListener(async (tab) => {
await EmeLogWindow.instance.open();
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EME_LOGGER_CLEAR') {
EmeLogWindow.instance.clear();
} else if (message.type === 'EME_LOGGER_GET_TEXT_LOGS') {
sendResponse({ textLogs: EmeLogWindow.instance.getTextLogs() });
} else if (message.type == 'EME_LOGGER_APPEND_LOG') {
EmeLogWindow.instance.appendLog(message.data);
}
});
}
Loading

0 comments on commit c860eba

Please sign in to comment.