diff --git a/Makefile.in b/Makefile.in index 0372837e6a9d..dd0d5ae5330f 100644 --- a/Makefile.in +++ b/Makefile.in @@ -305,6 +305,10 @@ endif update-packaging: $(MAKE) -C tools/update-packaging +.PHONY: package-generated-sources +package-generated-sources: + $(call py_action,package_generated_sources,'$(DIST)/$(PKG_PATH)$(GENERATED_SOURCE_FILE_PACKAGE)') + #XXX: this is a hack, since we don't want to clobber for MSVC # PGO support, but we can't do this test in client.mk ifneq ($(OS_ARCH)_$(GNU_CC), WINNT_) diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index 26fa38c2e722..b6c92df0972b 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -326,6 +326,10 @@ toolbarpaletteitem > toolbaritem[sdkstylewidget="true"][cui-areatype="toolbar"] display: -moz-box; } +.webextension-browser-action > .toolbarbutton-badge-stack > .toolbarbutton-icon { + width: 16px; +} + @media not all and (min-resolution: 1.1dppx) { .webextension-browser-action { list-style-image: var(--webextension-toolbar-image, inherit); diff --git a/browser/components/extensions/ext-browserAction.js b/browser/components/extensions/ext-browserAction.js index 0e0dd76f82b2..df1d19dfe500 100644 --- a/browser/components/extensions/ext-browserAction.js +++ b/browser/components/extensions/ext-browserAction.js @@ -177,7 +177,7 @@ this.browserAction = class extends ExtensionAPI { node.onmouseover = event => this.handleEvent(event); node.onmouseout = event => this.handleEvent(event); - this.updateButton(node, this.defaults); + this.updateButton(node, this.defaults, true); }, onViewShowing: async event => { @@ -429,10 +429,9 @@ this.browserAction = class extends ExtensionAPI { // Update the toolbar button |node| with the tab context data // in |tabData|. - updateButton(node, tabData) { + updateButton(node, tabData, sync = false) { let title = tabData.title || this.extension.name; - - node.ownerGlobal.requestAnimationFrame(() => { + let callback = () => { node.setAttribute("tooltiptext", title); node.setAttribute("label", title); @@ -467,7 +466,12 @@ this.browserAction = class extends ExtensionAPI { } node.setAttribute("style", style); - }); + }; + if (sync) { + callback(); + } else { + node.ownerGlobal.requestAnimationFrame(callback); + } } getIconData(icons) { diff --git a/browser/components/extensions/schemas/browser_action.json b/browser/components/extensions/schemas/browser_action.json index 5664dad5fc9d..0bf23a41c61d 100644 --- a/browser/components/extensions/schemas/browser_action.json +++ b/browser/components/extensions/schemas/browser_action.json @@ -151,7 +151,9 @@ { "$ref": "ImageDataType" }, { "type": "object", - "additionalProperties": {"$ref": "ImageDataType"} + "patternProperties": { + "^[1-9]\\d*$": {"$ref": "ImageDataType"} + } } ], "optional": true, @@ -162,7 +164,9 @@ { "type": "string" }, { "type": "object", - "additionalProperties": {"type": "string"} + "patternProperties": { + "^[1-9]\\d*$": { "type": "string" } + } } ], "optional": true, diff --git a/browser/components/extensions/schemas/page_action.json b/browser/components/extensions/schemas/page_action.json index 669b762e883c..dfb9e1519942 100644 --- a/browser/components/extensions/schemas/page_action.json +++ b/browser/components/extensions/schemas/page_action.json @@ -143,7 +143,9 @@ { "$ref": "ImageDataType" }, { "type": "object", - "additionalProperties": {"$ref": "ImageDataType"} + "patternProperties": { + "^[1-9]\\d*$": {"$ref": "ImageDataType"} + } } ], "optional": true, @@ -154,7 +156,9 @@ { "type": "string" }, { "type": "object", - "additionalProperties": {"type": "string"} + "patternProperties": { + "^[1-9]\\d*$": { "type": "string" } + } } ], "optional": true, diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js index 357ab1d287c2..a59706e49bc2 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js @@ -19,11 +19,10 @@ add_task(async function testInvalidIconSizes() { // helper function to run setIcon and check if it fails let assertSetIconThrows = function(detail, error, message) { detail.tabId = tabId; - promises.push( - browser.test.assertRejects( - browser[api].setIcon(detail), - /must be an integer/, - "setIcon with invalid icon size")); + browser.test.assertThrows( + () => browser[api].setIcon(detail), + /an unexpected .* property/, + "setIcon with invalid icon size"); }; let imageData = new ImageData(1, 1); diff --git a/browser/components/nsBrowserContentHandler.js b/browser/components/nsBrowserContentHandler.js index 0a68e07869d4..3a54e85262eb 100644 --- a/browser/components/nsBrowserContentHandler.js +++ b/browser/components/nsBrowserContentHandler.js @@ -138,14 +138,21 @@ function needHomepageOverride(prefb) { function getPostUpdateOverridePage(defaultOverridePage) { var um = Components.classes["@mozilla.org/updates/update-manager;1"] .getService(Components.interfaces.nsIUpdateManager); - try { - // If the updates.xml file is deleted then getUpdateAt will throw. - var update = um.getUpdateAt(0) + // The active update should be present when this code is called. If for + // whatever reason it isn't fallback to the latest update in the update + // history. + if (um.activeUpdate) { + var update = um.activeUpdate .QueryInterface(Components.interfaces.nsIPropertyBag); - } catch (e) { - // This should never happen. - Components.utils.reportError("Unable to find update: " + e); - return defaultOverridePage; + } else { + // If the updates.xml file is deleted then getUpdateAt will throw. + try { + update = um.getUpdateAt(0) + .QueryInterface(Components.interfaces.nsIPropertyBag); + } catch (e) { + Components.utils.reportError("Unable to find update: " + e); + return defaultOverridePage; + } } let actions = update.getProperty("actions"); diff --git a/browser/config/mozconfigs/linux32/artifact b/browser/config/mozconfigs/linux32/artifact index 560d86bb2c2b..5e394ecc1bd8 100644 --- a/browser/config/mozconfigs/linux32/artifact +++ b/browser/config/mozconfigs/linux32/artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/browser/config/mozconfigs/linux32/common-opt" . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/config/mozconfigs/linux32/debug-artifact b/browser/config/mozconfigs/linux32/debug-artifact index 52b49cee9a80..33c43f22a23c 100644 --- a/browser/config/mozconfigs/linux32/debug-artifact +++ b/browser/config/mozconfigs/linux32/debug-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/build/unix/mozconfig.linux32" . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/config/mozconfigs/linux64/artifact b/browser/config/mozconfigs/linux64/artifact index 51b82b384b9e..5f2673fb5343 100644 --- a/browser/config/mozconfigs/linux64/artifact +++ b/browser/config/mozconfigs/linux64/artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/browser/config/mozconfigs/linux64/common-opt" . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/config/mozconfigs/linux64/debug-artifact b/browser/config/mozconfigs/linux64/debug-artifact index db0a435ff4b2..5ee6c3db3d6e 100644 --- a/browser/config/mozconfigs/linux64/debug-artifact +++ b/browser/config/mozconfigs/linux64/debug-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/build/unix/mozconfig.linux" . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/config/mozconfigs/macosx64/artifact b/browser/config/mozconfigs/macosx64/artifact index 93497630ff7a..43a1e31361ba 100644 --- a/browser/config/mozconfigs/macosx64/artifact +++ b/browser/config/mozconfigs/macosx64/artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" # Needed to set SourceRepository in application.ini (used by Talos) export MOZILLA_OFFICIAL=1 diff --git a/browser/config/mozconfigs/macosx64/debug-artifact b/browser/config/mozconfigs/macosx64/debug-artifact index a625e9920762..fcf65b6d9afc 100644 --- a/browser/config/mozconfigs/macosx64/debug-artifact +++ b/browser/config/mozconfigs/macosx64/debug-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/build/macosx/mozconfig.common" . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/config/mozconfigs/win32/artifact b/browser/config/mozconfigs/win32/artifact index d18bedd6e892..2fc6ad43103f 100644 --- a/browser/config/mozconfigs/win32/artifact +++ b/browser/config/mozconfigs/win32/artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" # Needed to set SourceRepository in application.ini (used by Talos) export MOZILLA_OFFICIAL=1 diff --git a/browser/config/mozconfigs/win32/debug-artifact b/browser/config/mozconfigs/win32/debug-artifact index 1d7ab24ba3b1..20cdabd99762 100644 --- a/browser/config/mozconfigs/win32/debug-artifact +++ b/browser/config/mozconfigs/win32/debug-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/browser/config/mozconfigs/common" . "$topsrcdir/build/mozconfig.win-common" diff --git a/browser/config/mozconfigs/win64/artifact b/browser/config/mozconfigs/win64/artifact index 9934248448df..8b3ada2ec1c8 100644 --- a/browser/config/mozconfigs/win64/artifact +++ b/browser/config/mozconfigs/win64/artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" # Needed to set SourceRepository in application.ini (used by Talos) export MOZILLA_OFFICIAL=1 diff --git a/browser/config/mozconfigs/win64/debug-artifact b/browser/config/mozconfigs/win64/debug-artifact index bb9dc9fa8a99..d2bda199d30e 100644 --- a/browser/config/mozconfigs/win64/debug-artifact +++ b/browser/config/mozconfigs/win64/debug-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" . "$topsrcdir/browser/config/mozconfigs/win64/common-win64" . "$topsrcdir/browser/config/mozconfigs/common" diff --git a/browser/extensions/onboarding/content/onboarding.js b/browser/extensions/onboarding/content/onboarding.js index 8405ed411b13..00381e0341a6 100644 --- a/browser/extensions/onboarding/content/onboarding.js +++ b/browser/extensions/onboarding/content/onboarding.js @@ -20,6 +20,7 @@ const BRAND_SHORT_NAME = Services.strings .createBundle("chrome://branding/locale/brand.properties") .GetStringFromName("brandShortName"); const PROMPT_COUNT_PREF = "browser.onboarding.notification.prompt-count"; +const ONBOARDING_DIALOG_ID = "onboarding-overlay-dialog"; /** * Add any number of tours, key is the tourId, value should follow the format below @@ -368,6 +369,7 @@ class Onboarding { let { body } = this._window.document; this._overlayIcon = this._renderOverlayButton(); this._overlayIcon.addEventListener("click", this); + this._overlayIcon.addEventListener("keypress", this); body.insertBefore(this._overlayIcon, body.firstChild); this._overlay = this._renderOverlay(); @@ -436,6 +438,15 @@ class Onboarding { } } + /** + * Find a tour that should be selected. It is either a first tour that was not + * yet complete or the first one in the tab list. + */ + get selectedTour() { + return this._tours.find(tour => !this.isTourCompleted(tour.id)) || + this._tours[0]; + } + handleClick(target) { let { id, classList } = target; // Only containers receive pointer events in onboarding tour tab list, @@ -452,8 +463,7 @@ class Onboarding { // Let's toggle the overlay. case "onboarding-overlay": this.toggleOverlay(); - let selectedTour = this._tours.find(tour => !this.isTourCompleted(tour.id)) || this._tours[0]; - this.gotoPage(selectedTour.id); + this.gotoPage(this.selectedTour.id); break; case "onboarding-notification-close-btn": this.hideNotification(); @@ -477,8 +487,46 @@ class Onboarding { } } + /** + * Wrap keyboard focus within the dialog and focus on first element after last + * when moving forward or last element after first when moving backwards. Do + * nothing if focus is moving in the middle of the list of dialog's focusable + * elements. + * + * @param {DOMNode} current currently focused element + * @param {Boolean} back direction + * @return {DOMNode} newly focused element if any + */ + wrapMoveFocus(current, back) { + let elms = [...this._dialog.querySelectorAll( + `button, input[type="checkbox"], input[type="email"], [tabindex="0"]`)]; + let next; + if (back) { + if (elms.indexOf(current) === 0) { + next = elms[elms.length - 1]; + next.focus(); + } + } else if (elms.indexOf(current) === elms.length - 1) { + next = elms[0]; + next.focus(); + } + return next; + } + handleKeypress(event) { - let { target, key } = event; + let { target, key, shiftKey } = event; + + if (target === this._overlayIcon) { + if ([" ", "Enter"].includes(key)) { + // Remember that the dialog was opened with a keyboard. + this._overlayIcon.dataset.keyboardFocus = true; + this.handleClick(target); + event.preventDefault(); + } + + return; + } + // Current focused item can be tab container if previous navigation was done // via mouse. if (target.classList.contains("onboarding-tour-item-container")) { @@ -515,6 +563,16 @@ class Onboarding { } event.preventDefault(); break; + case "Escape": + this.toggleOverlay(); + break; + case "Tab": + let next = this.wrapMoveFocus(target, shiftKey); + // If focus was wrapped, prevent Tab key default action. + if (next) { + event.preventDefault(); + } + break; default: break; } @@ -564,6 +622,7 @@ class Onboarding { this.hideNotification(); this._overlay.classList.toggle("onboarding-opened"); + this.toggleModal(this._overlay.classList.contains("onboarding-opened")); let hiddenCheckbox = this._window.document.getElementById("onboarding-tour-hidden-checkbox"); if (hiddenCheckbox.checked) { @@ -571,6 +630,41 @@ class Onboarding { } } + /** + * Set modal dialog state and properties for accessibility purposes. + * @param {Boolean} opened whether the dialog is opened or closed. + */ + toggleModal(opened) { + let { document: doc } = this._window; + if (opened) { + // Set aria-hidden to true for the rest of the document. + [...doc.body.children].forEach( + child => child.id !== "onboarding-overlay" && + child.setAttribute("aria-hidden", true)); + // When dialog is opened with the keyboard, focus on the selected or + // first tour item. + if (this._overlayIcon.dataset.keyboardFocus) { + doc.getElementById(this.selectedTour.id).focus(); + } else { + // When dialog is opened with mouse, focus on the dialog itself to avoid + // visible keyboard focus styling. + this._dialog.focus(); + } + } else { + // Remove all set aria-hidden attributes. + [...doc.body.children].forEach( + child => child.removeAttribute("aria-hidden")); + // If dialog was opened with a keyboard, set the focus back on the overlay + // button. + if (this._overlayIcon.dataset.keyboardFocus) { + delete this._overlayIcon.dataset.keyboardFocus; + this._overlayIcon.focus(); + } else { + this._window.document.activeElement.blur(); + } + } + } + gotoPage(tourId) { let targetPageId = `${tourId}-page`; for (let page of this._tourPages) { @@ -868,7 +962,7 @@ class Onboarding { // We use `innerHTML` for more friendly reading. // The security should be fine because this is not from an external input. div.innerHTML = ` -
+
`; + this._dialog = div.querySelector(`[role="dialog"]`); + this._dialog.id = ONBOARDING_DIALOG_ID; + div.querySelector("label[for='onboarding-tour-hidden-checkbox']").textContent = this._bundle.GetStringFromName("onboarding.hidden-checkbox-label-text"); div.querySelector("#onboarding-header").textContent = @@ -898,7 +995,7 @@ class Onboarding { button.setAttribute("aria-label", tooltip); button.id = "onboarding-overlay-button"; button.setAttribute("aria-haspopup", true); - button.setAttribute("aria-controls", "onboarding-overlay-dialog"); + button.setAttribute("aria-controls", `${ONBOARDING_DIALOG_ID}`); let img = this._window.document.createElement("img"); img.id = "onboarding-overlay-button-icon"; img.setAttribute("role", "presentation"); @@ -959,11 +1056,10 @@ class Onboarding { this.markTourCompletionState(tour.id); } - let dialog = this._window.document.getElementById("onboarding-overlay-dialog"); let ul = this._window.document.getElementById("onboarding-tour-list"); ul.appendChild(itemsFrag); let footer = this._window.document.getElementById("onboarding-footer"); - dialog.insertBefore(pagesFrag, footer); + this._dialog.insertBefore(pagesFrag, footer); } _loadCSS() { diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js b/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js index b9539616b5c9..8711b0410a5e 100644 --- a/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_accessibility.js @@ -63,3 +63,30 @@ add_task(async function test_onboarding_notification_bar() { await BrowserTestUtils.removeTab(tab); }); + +add_task(async function test_onboarding_overlay_dialog() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + + info("Test accessibility and semantics of the dialog overlay"); + await assertModalDialog(browser, { visible: false }); + + info("Click on overlay button and check modal dialog state"); + await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-overlay-button", + {}, browser); + await promiseOnboardingOverlayOpened(browser); + await assertModalDialog(browser, + { visible: true, focusedId: "onboarding-overlay-dialog" }); + + info("Close the dialog and check modal dialog state"); + await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-overlay-close-btn", + {}, browser); + await promiseOnboardingOverlayClosed(browser); + await assertModalDialog(browser, { visible: false }); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js b/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js index def353ca297e..2e65cb59795a 100644 --- a/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js +++ b/browser/extensions/onboarding/test/browser/browser_onboarding_keyboard.js @@ -4,19 +4,29 @@ "use strict"; -function assertTourList(browser, args) { - return ContentTask.spawn(browser, args, ({ tourId, focusedId }) => { - let doc = content.document; - let items = [...doc.querySelectorAll(".onboarding-tour-item")]; - items.forEach(item => is(item.getAttribute("aria-selected"), - item.id === tourId ? "true" : "false", - "Active item should have aria-selected set to true and inactive to false")); - let focused = doc.getElementById(focusedId); - is(focused, doc.activeElement, `Focus should be set on ${focusedId}`); +function assertOverlayState(browser, args) { + return ContentTask.spawn(browser, args, ({ tourId, focusedId, visible }) => { + let { document: doc, window} = content; + if (tourId) { + let items = [...doc.querySelectorAll(".onboarding-tour-item")]; + items.forEach(item => is(item.getAttribute("aria-selected"), + item.id === tourId ? "true" : "false", + "Active item should have aria-selected set to true and inactive to false")); + } + if (focusedId) { + let focused = doc.getElementById(focusedId); + is(focused, doc.activeElement, `Focus should be set on ${focusedId}`); + } + if (visible !== undefined) { + let overlay = doc.getElementById("onboarding-overlay"); + is(window.getComputedStyle(overlay).getPropertyValue("display"), + visible ? "block" : "none", + `Onboarding overlay should be ${visible ? "visible" : "invisible"}`); + } }); } -const TEST_DATA = [ +const TOUR_LIST_TEST_DATA = [ { key: "VK_DOWN", expected: { tourId: TOUR_IDs[1], focusedId: TOUR_IDs[1] }}, { key: "VK_DOWN", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[2] }}, { key: "VK_DOWN", expected: { tourId: TOUR_IDs[3], focusedId: TOUR_IDs[3] }}, @@ -32,6 +42,21 @@ const TEST_DATA = [ { key: " ", expected: { tourId: TOUR_IDs[2], focusedId: TOUR_IDs[2] }} ]; +const BUTTONS_TEST_DATA = [ + { key: " ", expected: { focusedId: TOUR_IDs[0], visible: true }}, + { key: "VK_ESCAPE", expected: { focusedId: "onboarding-overlay-button", visible: false }}, + { key: "VK_RETURN", expected: { focusedId: TOUR_IDs[1], visible: true }}, + { key: "VK_TAB", options: { shiftKey: true }, expected: { focusedId: TOUR_IDs[0], visible: true }}, + { key: "VK_TAB", options: { shiftKey: true }, expected: { focusedId: "onboarding-overlay-close-btn", visible: true }}, + { key: " ", expected: { focusedId: "onboarding-overlay-button", visible: false }}, + { key: "VK_RETURN", expected: { focusedId: TOUR_IDs[1], visible: true }}, + { key: "VK_TAB", options: { shiftKey: true }, expected: { focusedId: TOUR_IDs[0], visible: true }}, + { key: "VK_TAB", options: { shiftKey: true }, expected: { focusedId: "onboarding-overlay-close-btn", visible: true }}, + { key: "VK_TAB", expected: { focusedId: TOUR_IDs[0], visible: true }}, + { key: "VK_TAB", options: { shiftKey: true }, expected: { focusedId: "onboarding-overlay-close-btn", visible: true }}, + { key: "VK_RETURN", expected: { focusedId: "onboarding-overlay-button", visible: false }} +]; + add_task(async function test_tour_list_keyboard_navigation() { resetOnboardingDefaultState(); @@ -48,14 +73,65 @@ add_task(async function test_tour_list_keyboard_navigation() { info("Set initial focus on the currently active tab"); await ContentTask.spawn(tab.linkedBrowser, {}, () => content.document.querySelector(".onboarding-active").focus()); - await assertTourList(tab.linkedBrowser, + await assertOverlayState(tab.linkedBrowser, { tourId: TOUR_IDs[0], focusedId: TOUR_IDs[0] }); - for (let { key, options = {}, expected } of TEST_DATA) { + for (let { key, options = {}, expected } of TOUR_LIST_TEST_DATA) { info(`Pressing ${key} to select ${expected.tourId} and have focus on ${expected.focusedId}`); await BrowserTestUtils.synthesizeKey(key, options, tab.linkedBrowser); - await assertTourList(tab.linkedBrowser, expected); + await assertOverlayState(tab.linkedBrowser, expected); } await BrowserTestUtils.removeTab(tab); }); + +add_task(async function test_buttons_keyboard_navigation() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + await promiseOnboardingOverlayLoaded(tab.linkedBrowser); + + info("Set keyboard focus on the onboarding overlay button"); + await ContentTask.spawn(tab.linkedBrowser, {}, () => + content.document.getElementById("onboarding-overlay-button").focus()); + await assertOverlayState(tab.linkedBrowser, + { focusedId: "onboarding-overlay-button", visible: false }); + + for (let { key, options = {}, expected } of BUTTONS_TEST_DATA) { + info(`Pressing ${key} to have ${expected.visible ? "visible" : "invisible"} overlay and have focus on ${expected.focusedId}`); + await BrowserTestUtils.synthesizeKey(key, options, tab.linkedBrowser); + await assertOverlayState(tab.linkedBrowser, expected); + } + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_overlay_dialog_keyboard_navigation() { + resetOnboardingDefaultState(); + + info("Wait for onboarding overlay loaded"); + let tab = await openTab(ABOUT_HOME_URL); + let browser = tab.linkedBrowser; + await promiseOnboardingOverlayLoaded(browser); + + info("Test accessibility and semantics of the dialog overlay"); + await assertModalDialog(browser, { visible: false }); + + info("Set keyboard focus on the onboarding overlay button"); + await ContentTask.spawn(browser, {}, () => + content.document.getElementById("onboarding-overlay-button").focus()); + info("Open dialog with keyboard and check the dialog state"); + await BrowserTestUtils.synthesizeKey(" ", {}, browser); + await promiseOnboardingOverlayOpened(browser); + await assertModalDialog(browser, + { visible: true, keyboardFocus: true, focusedId: TOUR_IDs[0] }); + + info("Close the dialog and check modal dialog state"); + await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, browser); + await promiseOnboardingOverlayClosed(browser); + await assertModalDialog(browser, + { visible: false, keyboardFocus: true, focusedId: "onboarding-overlay-button" }); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/onboarding/test/browser/head.js b/browser/extensions/onboarding/test/browser/head.js index b3704feafa55..87b36a46f9e4 100644 --- a/browser/extensions/onboarding/test/browser/head.js +++ b/browser/extensions/onboarding/test/browser/head.js @@ -87,21 +87,22 @@ function promiseOnboardingOverlayLoaded(browser) { } function promiseOnboardingOverlayOpened(browser) { - let condition = () => { - return ContentTask.spawn(browser, {}, function() { - return new Promise(resolve => { - let overlay = content.document.querySelector("#onboarding-overlay"); - if (overlay.classList.contains("onboarding-opened")) { - resolve(true); - return; - } - resolve(false); - }); - }) - }; - return BrowserTestUtils.waitForCondition( - condition, - "Should open onboarding overlay", + return BrowserTestUtils.waitForCondition(() => + ContentTask.spawn(browser, {}, () => + content.document.querySelector("#onboarding-overlay").classList.contains( + "onboarding-opened")), + "Should close onboarding overlay", + 100, + 30 + ); +} + +function promiseOnboardingOverlayClosed(browser) { + return BrowserTestUtils.waitForCondition(() => + ContentTask.spawn(browser, {}, () => + !content.document.querySelector("#onboarding-overlay").classList.contains( + "onboarding-opened")), + "Should close onboarding overlay", 100, 30 ); @@ -209,9 +210,17 @@ function assertOverlaySemantics(browser) { return ContentTask.spawn(browser, {}, function() { let doc = content.document; + info("Checking dialog"); + let dialog = doc.getElementById("onboarding-overlay-dialog"); + is(dialog.getAttribute("role"), "dialog", + "Dialog should have a dialog role attribute set"); + is(dialog.tabIndex, "-1", "Dialog should be focusable but not in tab order"); + is(dialog.getAttribute("aria-labelledby"), "onboarding-header", + "Dialog should be labaled by its header"); + info("Checking the tablist container"); is(doc.getElementById("onboarding-tour-list").getAttribute("role"), "tablist", - "Tour list should have a tablist role argument set"); + "Tour list should have a tablist role attribute set"); info("Checking each tour item that represents the tab"); let items = [...doc.querySelectorAll(".onboarding-tour-item")]; @@ -222,15 +231,43 @@ function assertOverlaySemantics(browser) { item.classList.contains("onboarding-active") ? "true" : "false", "Active item should have aria-selected set to true and inactive to false"); is(item.tabIndex, "0", "Item tab index must be set for keyboard accessibility"); - is(item.getAttribute("role"), "tab", "Item should have a tab role argument set"); + is(item.getAttribute("role"), "tab", "Item should have a tab role attribute set"); let tourPanelId = `${item.id}-page`; is(item.getAttribute("aria-controls"), tourPanelId, "Item should have aria-controls attribute point to its tabpanel"); let panel = doc.getElementById(tourPanelId); is(panel.getAttribute("role"), "tabpanel", - "Tour panel should have a tabpanel role argument set"); + "Tour panel should have a tabpanel role attribute set"); is(panel.getAttribute("aria-labelledby"), item.id, "Tour panel should have aria-labelledby attribute point to its tab"); }); }); } + +function assertModalDialog(browser, args) { + return ContentTask.spawn(browser, args, ({ keyboardFocus, visible, focusedId }) => { + let doc = content.document; + let overlayButton = doc.getElementById("onboarding-overlay-button"); + if (visible) { + [...doc.body.children].forEach(child => + child.id !== "onboarding-overlay" && + is(child.getAttribute("aria-hidden"), "true", + "Content should not be visible to screen reader")); + is(focusedId ? doc.getElementById(focusedId) : doc.body, + doc.activeElement, `Focus should be on ${focusedId || "body"}`); + is(keyboardFocus ? "true" : undefined, + overlayButton.dataset.keyboardFocus, + "Overlay button focus state is saved correctly"); + } else { + [...doc.body.children].forEach( + child => ok(!child.hasAttribute("aria-hidden"), + "Content should be visible to screen reader")); + if (keyboardFocus) { + is(overlayButton, doc.activeElement, + "Focus should be set on overlay button"); + } + ok(!overlayButton.dataset.keyboardFocus, + "Overlay button focus state should be cleared"); + } + }); +} diff --git a/build/moz-automation.mk b/build/moz-automation.mk index e7665a882ed3..0ba739038d46 100644 --- a/build/moz-automation.mk +++ b/build/moz-automation.mk @@ -27,6 +27,7 @@ tier_MOZ_AUTOMATION_INSTALLER = installer tier_MOZ_AUTOMATION_PACKAGE = package tier_MOZ_AUTOMATION_PACKAGE_TESTS = package-tests tier_MOZ_AUTOMATION_UPDATE_PACKAGING = update-packaging +tier_MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES = package-generated-sources tier_MOZ_AUTOMATION_UPLOAD_SYMBOLS = uploadsymbols tier_MOZ_AUTOMATION_UPLOAD = upload @@ -41,6 +42,7 @@ moz_automation_symbols = \ MOZ_AUTOMATION_PACKAGE \ MOZ_AUTOMATION_INSTALLER \ MOZ_AUTOMATION_UPDATE_PACKAGING \ + MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES \ MOZ_AUTOMATION_L10N_CHECK \ MOZ_AUTOMATION_UPLOAD \ $(NULL) @@ -60,6 +62,7 @@ automation/upload: automation/package automation/upload: automation/package-tests automation/upload: automation/buildsymbols automation/upload: automation/update-packaging +automation/upload: automation/package-generated-sources # The installer and packager all run stage-package, and may conflict # with each other. diff --git a/build/moz.configure/warnings.configure b/build/moz.configure/warnings.configure index 34e8c84f3f54..f06fce8de942 100644 --- a/build/moz.configure/warnings.configure +++ b/build/moz.configure/warnings.configure @@ -42,6 +42,7 @@ add_gcc_warning('-Wtype-limits') # catches some dead code add_gcc_warning('-Wunreachable-code') +check_and_add_gcc_warning('-Wunreachable-code-return') # catches treating string literals as non-const add_gcc_warning('-Wwrite-strings', cxx_compiler) diff --git a/build/mozconfig.artifact.automation b/build/mozconfig.artifact.automation new file mode 100644 index 000000000000..90bb2ab41500 --- /dev/null +++ b/build/mozconfig.artifact.automation @@ -0,0 +1,6 @@ +# Common options for artifact builds to set automation steps. +# This gets included before mozconfig.automation. + +MOZ_AUTOMATION_BUILD_SYMBOLS=0 +MOZ_AUTOMATION_L10N_CHECK=0 +MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES=0 diff --git a/build/mozconfig.automation b/build/mozconfig.automation index c959c3fc859d..f85cafcb2657 100644 --- a/build/mozconfig.automation +++ b/build/mozconfig.automation @@ -15,6 +15,7 @@ mk_add_options "export MOZ_AUTOMATION_PACKAGE=${MOZ_AUTOMATION_PACKAGE-1}" mk_add_options "export MOZ_AUTOMATION_PACKAGE_TESTS=${MOZ_AUTOMATION_PACKAGE_TESTS-1}" mk_add_options "export MOZ_AUTOMATION_INSTALLER=${MOZ_AUTOMATION_INSTALLER-0}" mk_add_options "export MOZ_AUTOMATION_UPDATE_PACKAGING=${MOZ_AUTOMATION_UPDATE_PACKAGING-0}" +mk_add_options "export MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES=${MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES-1}" mk_add_options "export MOZ_AUTOMATION_UPLOAD=${MOZ_AUTOMATION_UPLOAD-1}" mk_add_options "export MOZ_AUTOMATION_UPLOAD_SYMBOLS=${MOZ_AUTOMATION_UPLOAD_SYMBOLS-0}" diff --git a/build/upload_generated_sources.py b/build/upload_generated_sources.py new file mode 100644 index 000000000000..b24f50937520 --- /dev/null +++ b/build/upload_generated_sources.py @@ -0,0 +1,156 @@ +#!/usr/bin/env/python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import argparse +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +import gzip +import io +import logging +from mozbuild.base import MozbuildObject +from mozbuild.generated_sources import ( + get_filename_with_digest, + get_s3_region_and_bucket, +) +import os +from Queue import Queue +import requests +import sys +import tarfile +from threading import Event, Thread +import time + +# Arbitrary, should probably measure this. +NUM_WORKER_THREADS = 10 +log = logging.getLogger('upload-generated-sources') +log.setLevel(logging.INFO) + + +@contextmanager +def timed(): + ''' + Yield a function that provides the elapsed time in seconds since this + function was called. + ''' + start = time.time() + def elapsed(): + return time.time() - start + yield elapsed + + +def gzip_compress(data): + ''' + Apply gzip compression to `data` and return the result as a `BytesIO`. + ''' + b = io.BytesIO() + with gzip.GzipFile(fileobj=b, mode='w') as f: + f.write(data) + b.flush() + b.seek(0) + return b + + +def upload_worker(queue, event, bucket, session_args): + ''' + Get `(name, contents)` entries from `queue` and upload `contents` + to S3 with gzip compression using `name` as the key, prefixed with + the SHA-512 digest of `contents` as a hex string. If an exception occurs, + set `event`. + ''' + try: + import boto3 + session = boto3.session.Session(**session_args) + s3 = session.client('s3') + while True: + if event.is_set(): + # Some other thread hit an exception. + return + (name, contents) = queue.get() + pathname = get_filename_with_digest(name, contents) + compressed = gzip_compress(contents) + extra_args = { + 'ContentEncoding': 'gzip', + 'ContentType': 'text/plain', + } + log.info('Uploading "{}" ({} bytes)'.format(pathname, len(compressed.getvalue()))) + with timed() as elapsed: + s3.upload_fileobj(compressed, bucket, pathname, ExtraArgs=extra_args) + log.info('Finished uploading "{}" in {:0.3f}s'.format(pathname, elapsed())) + queue.task_done() + except Exception: + log.exception('Thread encountered exception:') + event.set() + + +def do_work(artifact, region, bucket): + session_args = {'region_name': region} + session = requests.Session() + if 'TASK_ID' in os.environ: + level = os.environ.get('MOZ_SCM_LEVEL', '1') + secrets_url = 'http://taskcluster/secrets/v1/secret/project/releng/gecko/build/level-{}/gecko-generated-sources-upload'.format(level) + log.info('Using AWS credentials from the secrets service: "{}"'.format(secrets_url)) + res = session.get(secrets_url) + res.raise_for_status() + secret = res.json() + session_args.update( + aws_access_key_id=secret['secret']['AWS_ACCESS_KEY_ID'], + aws_secret_access_key=secret['secret']['AWS_SECRET_ACCESS_KEY'], + ) + else: + log.info('Trying to use your AWS credentials..') + + + # First, fetch the artifact containing the sources. + log.info('Fetching generated sources artifact: "{}"'.format(artifact)) + with timed() as elapsed: + res = session.get(artifact) + log.info('Fetch HTTP status: {}, {} bytes downloaded in {:0.3f}s'.format(res.status_code, len(res.content), elapsed())) + res.raise_for_status() + # Create a queue and worker threads for uploading. + q = Queue() + event = Event() + log.info('Creating {} worker threads'.format(NUM_WORKER_THREADS)) + for i in range(NUM_WORKER_THREADS): + t = Thread(target=upload_worker, args=(q, event, bucket, session_args)) + t.daemon = True + t.start() + with tarfile.open(fileobj=io.BytesIO(res.content), mode='r|gz') as tar: + # Next, process each file. + for entry in tar: + if event.is_set(): + break + log.info('Queueing "{}"'.format(entry.name)) + q.put((entry.name, tar.extractfile(entry).read())) + # Wait until all uploads are finished. + # We don't use q.join() here because we want to also monitor event. + while q.unfinished_tasks: + if event.wait(0.1): + log.error('Worker thread encountered exception, exiting...') + break + + +def main(argv): + logging.basicConfig(format='%(levelname)s - %(threadName)s - %(message)s') + parser = argparse.ArgumentParser( + description='Upload generated source files in ARTIFACT to BUCKET in S3.') + parser.add_argument('artifact', + help='generated-sources artifact from build task') + args = parser.parse_args(argv) + region, bucket = get_s3_region_and_bucket() + + config = MozbuildObject.from_environment() + config._activate_virtualenv() + config.virtualenv_manager.install_pip_package('boto3==1.4.4') + + with timed() as elapsed: + do_work(region=region, bucket=bucket, artifact=args.artifact) + log.info('Finished in {:.03f}s'.format(elapsed())) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js index 12da760bc7e8..0a6eb5a47c55 100644 --- a/devtools/client/inspector/rules/rules.js +++ b/devtools/client/inspector/rules/rules.js @@ -1004,12 +1004,7 @@ CssRuleView.prototype = { container.hidden = false; this.element.appendChild(container); - header.addEventListener("dblclick", () => { - this._toggleContainerVisibility(twisty, container, isPseudo, - !this.showPseudoElements); - }); - - twisty.addEventListener("click", () => { + header.addEventListener("click", () => { this._toggleContainerVisibility(twisty, container, isPseudo, !this.showPseudoElements); }); diff --git a/js/public/Class.h b/js/public/Class.h index 057e1e3677d4..e64ab86e8e0c 100644 --- a/js/public/Class.h +++ b/js/public/Class.h @@ -860,7 +860,7 @@ static const uint32_t JSCLASS_FOREGROUND_FINALIZE = 1 << (JSCLASS_HIGH_FLAGS // application. static const uint32_t JSCLASS_GLOBAL_APPLICATION_SLOTS = 5; static const uint32_t JSCLASS_GLOBAL_SLOT_COUNT = - JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 37; + JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 36; #define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n) \ (JSCLASS_IS_GLOBAL | JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n))) diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp index cb34c261eb76..9a7bd176561b 100644 --- a/js/src/builtin/Object.cpp +++ b/js/src/builtin/Object.cpp @@ -905,8 +905,14 @@ obj_watch(JSContext* cx, unsigned argc, Value* vp) if (!obj) return false; - if (!GlobalObject::warnOnceAboutWatch(cx, obj)) - return false; + if (!cx->compartment()->warnedAboutObjectWatch) { + if (!JS_ReportErrorFlagsAndNumberASCII(cx, JSREPORT_WARNING, GetErrorMessage, nullptr, + JSMSG_OBJECT_WATCH_DEPRECATED)) + { + return false; + } + cx->compartment()->warnedAboutObjectWatch = true; + } if (args.length() <= 1) { ReportMissingArg(cx, args.calleev(), 1); @@ -937,8 +943,14 @@ obj_unwatch(JSContext* cx, unsigned argc, Value* vp) if (!obj) return false; - if (!GlobalObject::warnOnceAboutWatch(cx, obj)) - return false; + if (!cx->compartment()->warnedAboutObjectWatch) { + if (!JS_ReportErrorFlagsAndNumberASCII(cx, JSREPORT_WARNING, GetErrorMessage, nullptr, + JSMSG_OBJECT_WATCH_DEPRECATED)) + { + return false; + } + cx->compartment()->warnedAboutObjectWatch = true; + } RootedId id(cx); if (args.length() != 0) { diff --git a/js/src/gc/StoreBuffer.cpp b/js/src/gc/StoreBuffer.cpp index 3739222ec21e..4d35db550503 100644 --- a/js/src/gc/StoreBuffer.cpp +++ b/js/src/gc/StoreBuffer.cpp @@ -132,10 +132,8 @@ js::gc::AllocateWholeCellSet(Arena* arena) AutoEnterOOMUnsafeRegion oomUnsafe; Nursery& nursery = zone->group()->nursery(); void* data = nursery.allocateBuffer(zone, sizeof(ArenaCellSet)); - if (!data) { + if (!data) oomUnsafe.crash("Failed to allocate WholeCellSet"); - return nullptr; - } if (nursery.freeSpace() < ArenaCellSet::NurseryFreeThresholdBytes) zone->group()->storeBuffer().setAboutToOverflow(JS::gcreason::FULL_WHOLE_CELL_BUFFER); diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp index d275b2fd9ed9..ffcf08835078 100644 --- a/js/src/jit/MCallOptimize.cpp +++ b/js/src/jit/MCallOptimize.cpp @@ -1617,8 +1617,7 @@ IonBuilder::inlineConstantStringSplitString(CallInfo& callInfo) MDefinition* array = current->peek(-1); if (!initLength) { - if (!array->isResumePoint()) - MOZ_TRY(resumeAfter(array->toNewArray())); + MOZ_TRY(resumeAfter(array->toNewArray())); return InliningStatus_Inlined; } diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp index 6b6fdd5845b4..f3fe65e559ae 100644 --- a/js/src/jit/MIR.cpp +++ b/js/src/jit/MIR.cpp @@ -4132,7 +4132,7 @@ MResumePoint::Copy(TempAllocator& alloc, MResumePoint* src) } MResumePoint::MResumePoint(MBasicBlock* block, jsbytecode* pc, Mode mode) - : MNode(block), + : MNode(block, Kind::ResumePoint), pc_(pc), instruction_(nullptr), mode_(mode) @@ -4149,7 +4149,7 @@ MResumePoint::init(TempAllocator& alloc) MResumePoint* MResumePoint::caller() const { - return block_->callerResumePoint(); + return block()->callerResumePoint(); } void diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h index be037366674d..bcf62571b675 100644 --- a/js/src/jit/MIR.h +++ b/js/src/jit/MIR.h @@ -301,37 +301,61 @@ typedef InlineList::iterator MUseIterator; class MNode : public TempObject { protected: - MBasicBlock* block_; // Containing basic block. - - public: - enum Kind { - Definition, + enum class Kind { + Definition = 0, ResumePoint }; - MNode() - : block_(nullptr) + private: + static const uintptr_t KindMask = 0x1; + uintptr_t blockAndKind_; + + Kind kind() const { + return Kind(blockAndKind_ & KindMask); + } + + protected: + explicit MNode(const MNode& other) + : blockAndKind_(other.blockAndKind_) { } - explicit MNode(MBasicBlock* block) - : block_(block) - { } + MNode(MBasicBlock* block, Kind kind) + { + setBlockAndKind(block, kind); + } - virtual Kind kind() const = 0; + void setBlockAndKind(MBasicBlock* block, Kind kind) { + blockAndKind_ = uintptr_t(block) | uintptr_t(kind); + MOZ_ASSERT(this->block() == block); + } + MBasicBlock* definitionBlock() const { + MOZ_ASSERT(isDefinition()); + static_assert(unsigned(Kind::Definition) == 0, "Code below relies on low bit being 0"); + return reinterpret_cast(blockAndKind_); + } + MBasicBlock* resumePointBlock() const { + MOZ_ASSERT(isResumePoint()); + static_assert(unsigned(Kind::ResumePoint) == 1, "Code below relies on low bit being 1"); + // Use a subtraction: if the caller does block()->foo, the compiler + // will be able to fold it with the load. + return reinterpret_cast(blockAndKind_ - 1); + } + + public: // Returns the definition at a given operand. virtual MDefinition* getOperand(size_t index) const = 0; virtual size_t numOperands() const = 0; virtual size_t indexOf(const MUse* u) const = 0; bool isDefinition() const { - return kind() == Definition; + return kind() == Kind::Definition; } bool isResumePoint() const { - return kind() == ResumePoint; + return kind() == Kind::ResumePoint; } MBasicBlock* block() const { - return block_; + return reinterpret_cast(blockAndKind_ & ~KindMask); } MBasicBlock* caller() const; @@ -515,16 +539,21 @@ class MDefinition : public MNode flags_ |= flags; } + // Calling isDefinition or isResumePoint on MDefinition is unnecessary. + bool isDefinition() const = delete; + bool isResumePoint() const = delete; + protected: void setBlock(MBasicBlock* block) { - block_ = block; + setBlockAndKind(block, Kind::Definition); } static HashNumber addU32ToHash(HashNumber hash, uint32_t data); public: MDefinition() - : id_(0), + : MNode(nullptr, Kind::Definition), + id_(0), flags_(0), range_(nullptr), resultType_(MIRType::None), @@ -535,7 +564,8 @@ class MDefinition : public MNode // Copying a definition leaves the list of uses and the block empty. explicit MDefinition(const MDefinition& other) - : id_(0), + : MNode(other), + id_(0), flags_(other.flags_), range_(other.range_), resultType_(other.resultType_), @@ -565,6 +595,10 @@ class MDefinition : public MNode // be worthwhile. virtual bool possiblyCalls() const { return false; } + MBasicBlock* block() const { + return definitionBlock(); + } + void setTrackedSite(const BytecodeSite* site) { MOZ_ASSERT(site); trackedSite_ = site; @@ -700,12 +734,8 @@ class MDefinition : public MNode virtual void collectRangeInfoPreTrunc() { } - MNode::Kind kind() const override { - return MNode::Definition; - } - uint32_t id() const { - MOZ_ASSERT(block_); + MOZ_ASSERT(block()); return id_; } void setId(uint32_t id) { @@ -13330,6 +13360,14 @@ class MResumePoint final : MResumePoint(MBasicBlock* block, jsbytecode* pc, Mode mode); void inherit(MBasicBlock* state); + void setBlock(MBasicBlock* block) { + setBlockAndKind(block, Kind::ResumePoint); + } + + // Calling isDefinition or isResumePoint on MResumePoint is unnecessary. + bool isDefinition() const = delete; + bool isResumePoint() const = delete; + protected: // Initializes operands_ to an empty array of a fixed length. // The array may then be filled in by inherit(). @@ -13354,9 +13392,10 @@ class MResumePoint final : const MDefinitionVector& operands); static MResumePoint* Copy(TempAllocator& alloc, MResumePoint* src); - MNode::Kind kind() const override { - return MNode::ResumePoint; + MBasicBlock* block() const { + return resumePointBlock(); } + size_t numAllocatedOperands() const { return operands_.length(); } diff --git a/js/src/jit/MIRGraph.cpp b/js/src/jit/MIRGraph.cpp index a5d94ee4ec66..4465aafcb52e 100644 --- a/js/src/jit/MIRGraph.cpp +++ b/js/src/jit/MIRGraph.cpp @@ -325,7 +325,7 @@ MBasicBlock::NewWithResumePoint(MIRGraph& graph, const CompileInfo& info, MOZ_ASSERT(!resumePoint->instruction()); resumePoint->block()->discardResumePoint(resumePoint, RefType_None); - resumePoint->block_ = block; + resumePoint->setBlock(block); block->addResumePoint(resumePoint); block->entryResumePoint_ = resumePoint; @@ -927,9 +927,9 @@ void MBasicBlock::discardDef(MDefinition* at) { if (at->isPhi()) - at->block_->discardPhi(at->toPhi()); + at->block()->discardPhi(at->toPhi()); else - at->block_->discard(at->toInstruction()); + at->block()->discard(at->toInstruction()); } void diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp index a6650e338b6c..40158d410ce9 100644 --- a/js/src/jscompartment.cpp +++ b/js/src/jscompartment.cpp @@ -60,6 +60,7 @@ JSCompartment::JSCompartment(Zone* zone, const JS::CompartmentOptions& options = warnedAboutExprClosure(false), warnedAboutForEach(false), warnedAboutLegacyGenerator(false), + warnedAboutObjectWatch(false), warnedAboutStringGenericsMethods(0), #ifdef DEBUG firedOnNewGlobalObject(false), diff --git a/js/src/jscompartment.h b/js/src/jscompartment.h index 86033866681f..ebabf8c598b6 100644 --- a/js/src/jscompartment.h +++ b/js/src/jscompartment.h @@ -625,6 +625,7 @@ struct JSCompartment bool warnedAboutExprClosure : 1; bool warnedAboutForEach : 1; bool warnedAboutLegacyGenerator : 1; + bool warnedAboutObjectWatch : 1; uint32_t warnedAboutStringGenericsMethods; #ifdef DEBUG diff --git a/js/src/tests/js1_2/Objects/watch-deprecated.js b/js/src/tests/js1_2/Objects/watch-deprecated.js new file mode 100644 index 000000000000..cb15c2d22f9d --- /dev/null +++ b/js/src/tests/js1_2/Objects/watch-deprecated.js @@ -0,0 +1,23 @@ +// |reftest| skip-if(!xulRuntime.shell) + +enableLastWarning(); + +var g = newGlobal(); +g.eval("({}).watch('x', function(){})"); + +var warning = getLastWarning(); +assertEq(warning.name, "Warning"); +assertEq(warning.message.includes("watch"), true, "warning should mention watch"); + +clearLastWarning(); + +g = newGlobal(); +g.eval("({}).unwatch('x')"); + +warning = getLastWarning(); +assertEq(warning.name, "Warning"); +assertEq(warning.message.includes("watch"), true, "warning should mention watch"); + +clearLastWarning(); + +reportCompare(0, 0); diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index 94f6fe2e7ad2..0bdb7b654718 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -543,28 +543,6 @@ GlobalObject::isRuntimeCodeGenEnabled(JSContext* cx, Handle globa return !v.isFalse(); } -/* static */ bool -GlobalObject::warnOnceAbout(JSContext* cx, HandleObject obj, WarnOnceFlag flag, - unsigned errorNumber) -{ - Rooted global(cx, &obj->global()); - HeapSlot& v = global->getSlotRef(WARNED_ONCE_FLAGS); - MOZ_ASSERT_IF(!v.isUndefined(), v.toInt32()); - int32_t flags = v.isUndefined() ? 0 : v.toInt32(); - if (!(flags & flag)) { - if (!JS_ReportErrorFlagsAndNumberASCII(cx, JSREPORT_WARNING, GetErrorMessage, nullptr, - errorNumber)) - { - return false; - } - if (v.isUndefined()) - v.init(global, HeapSlot::Slot, WARNED_ONCE_FLAGS, Int32Value(flags | flag)); - else - v.set(global, HeapSlot::Slot, WARNED_ONCE_FLAGS, Int32Value(flags | flag)); - } - return true; -} - /* static */ JSFunction* GlobalObject::createConstructor(JSContext* cx, Native ctor, JSAtom* nameArg, unsigned length, gc::AllocKind kind, const JSJitInfo* jitInfo) diff --git a/js/src/vm/GlobalObject.h b/js/src/vm/GlobalObject.h index e35e42ecd65e..a312d8a70a71 100644 --- a/js/src/vm/GlobalObject.h +++ b/js/src/vm/GlobalObject.h @@ -100,7 +100,6 @@ class GlobalObject : public NativeObject IMPORT_ENTRY_PROTO, EXPORT_ENTRY_PROTO, REGEXP_STATICS, - WARNED_ONCE_FLAGS, RUNTIME_CODEGEN_ENABLED, DEBUGGERS, INTRINSICS, @@ -120,18 +119,6 @@ class GlobalObject : public NativeObject static_assert(JSCLASS_GLOBAL_SLOT_COUNT == RESERVED_SLOTS, "global object slot counts are inconsistent"); - enum WarnOnceFlag : int32_t { - WARN_WATCH_DEPRECATED = 1 << 0, - }; - - // Emit the specified warning if the given slot in |obj|'s global isn't - // true, then set the slot to true. Thus calling this method warns once - // for each global object it's called on, and every other call does - // nothing. - static bool - warnOnceAbout(JSContext* cx, HandleObject obj, WarnOnceFlag flag, unsigned errorNumber); - - public: LexicalEnvironmentObject& lexicalEnvironment() const; GlobalScope& emptyGlobalScope() const; @@ -751,15 +738,6 @@ class GlobalObject : public NativeObject static bool isRuntimeCodeGenEnabled(JSContext* cx, Handle global); - // Warn about use of the deprecated watch/unwatch functions in the global - // in which |obj| was created, if no prior warning was given. - static bool warnOnceAboutWatch(JSContext* cx, HandleObject obj) { - // Temporarily disabled until we've provided a watch/unwatch workaround for - // debuggers like Firebug (bug 934669). - //return warnOnceAbout(cx, obj, WARN_WATCH_DEPRECATED, JSMSG_OBJECT_WATCH_DEPRECATED); - return true; - } - static bool getOrCreateEval(JSContext* cx, Handle global, MutableHandleObject eval); diff --git a/mobile/android/config/mozconfigs/android-api-15-frontend/nightly b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly index b5a911bf6f0a..c1f945db8cf7 100644 --- a/mobile/android/config/mozconfigs/android-api-15-frontend/nightly +++ b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly @@ -7,6 +7,7 @@ MOZ_AUTOMATION_PACKAGE_TESTS=0 MOZ_AUTOMATION_UPDATE_PACKAGING=0 MOZ_AUTOMATION_UPLOAD=0 MOZ_AUTOMATION_UPLOAD_SYMBOLS=0 +MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES=0 NO_CACHE=1 NO_NDK=1 diff --git a/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly b/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly index 6909331379ce..5f4a07aa24ee 100644 --- a/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly +++ b/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly @@ -7,6 +7,7 @@ MOZ_AUTOMATION_PACKAGE_TESTS=0 MOZ_AUTOMATION_UPDATE_PACKAGING=0 MOZ_AUTOMATION_UPLOAD=0 MOZ_AUTOMATION_UPLOAD_SYMBOLS=0 +MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES=0 NO_CACHE=1 NO_NDK=1 diff --git a/mobile/android/config/mozconfigs/android-api-15-gradle/nightly-artifact b/mobile/android/config/mozconfigs/android-api-15-gradle/nightly-artifact index cca82959c400..977d80a7b9b8 100644 --- a/mobile/android/config/mozconfigs/android-api-15-gradle/nightly-artifact +++ b/mobile/android/config/mozconfigs/android-api-15-gradle/nightly-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" NO_CACHE=1 NO_NDK=1 diff --git a/mobile/android/config/mozconfigs/android-api-15/debug-artifact b/mobile/android/config/mozconfigs/android-api-15/debug-artifact index c4923d02f84a..7f1c2313a64a 100644 --- a/mobile/android/config/mozconfigs/android-api-15/debug-artifact +++ b/mobile/android/config/mozconfigs/android-api-15/debug-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" NO_CACHE=1 NO_NDK=1 diff --git a/mobile/android/config/mozconfigs/android-api-15/nightly-artifact b/mobile/android/config/mozconfigs/android-api-15/nightly-artifact index 990905688f1f..6be333459505 100644 --- a/mobile/android/config/mozconfigs/android-api-15/nightly-artifact +++ b/mobile/android/config/mozconfigs/android-api-15/nightly-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" NO_CACHE=1 NO_NDK=1 diff --git a/mobile/android/config/mozconfigs/android-x86/nightly-artifact b/mobile/android/config/mozconfigs/android-x86/nightly-artifact index 2655d6cf5fb2..09fefe23f774 100644 --- a/mobile/android/config/mozconfigs/android-x86/nightly-artifact +++ b/mobile/android/config/mozconfigs/android-x86/nightly-artifact @@ -1,5 +1,4 @@ -MOZ_AUTOMATION_BUILD_SYMBOLS=0 -MOZ_AUTOMATION_L10N_CHECK=0 +. "$topsrcdir/build/mozconfig.artifact.automation" NO_CACHE=1 NO_NDK=1 diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index b0b01e8c9637..06bfb0cacc84 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -899,6 +899,7 @@ pref("gfx.webrender.force-angle", true); pref("gfx.webrender.highlight-painted-layers", false); pref("gfx.webrender.layers-free", false); pref("gfx.webrender.profiler.enabled", false); +pref("gfx.webrender.blob-images", false); // Whether webrender should be used as much as possible. pref("gfx.webrendest.enabled", false); diff --git a/netwerk/protocol/gio/nsGIOProtocolHandler.cpp b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp index 05830d928694..c90b50febaa8 100644 --- a/netwerk/protocol/gio/nsGIOProtocolHandler.cpp +++ b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp @@ -85,8 +85,6 @@ MapGIOResult(gint code) default: return NS_ERROR_FAILURE; } - - return NS_ERROR_FAILURE; } static nsresult diff --git a/python/mozbuild/mozbuild/action/package_generated_sources.py b/python/mozbuild/mozbuild/action/package_generated_sources.py new file mode 100644 index 000000000000..59b1f889622e --- /dev/null +++ b/python/mozbuild/mozbuild/action/package_generated_sources.py @@ -0,0 +1,30 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import argparse +import json +import os.path +import sys + +import buildconfig +from mozpack.archive import create_tar_gz_from_files +from mozbuild.generated_sources import get_generated_sources + + +def main(argv): + parser = argparse.ArgumentParser( + description='Produce archive of generated sources') + parser.add_argument('outputfile', help='File to write output to') + args = parser.parse_args(argv) + + + files = dict(get_generated_sources()) + with open(args.outputfile, 'wb') as fh: + create_tar_gz_from_files(fh, files, compresslevel=5) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/generated_sources.py b/python/mozbuild/mozbuild/generated_sources.py new file mode 100644 index 000000000000..be3711039f54 --- /dev/null +++ b/python/mozbuild/mozbuild/generated_sources.py @@ -0,0 +1,73 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import hashlib +import json +import os + +from mozpack.files import FileFinder +import mozpack.path as mozpath + + +def sha512_digest(data): + ''' + Generate the SHA-512 digest of `data` and return it as a hex string. + ''' + return hashlib.sha512(data).hexdigest() + + +def get_filename_with_digest(name, contents): + ''' + Return the filename that will be used to store the generated file + in the S3 bucket, consisting of the SHA-512 digest of `contents` + joined with the relative path `name`. + ''' + digest = sha512_digest(contents) + return mozpath.join(digest, name) + + +def get_generated_sources(): + ''' + Yield tuples of `(objdir-rel-path, file)` for generated source files + in this objdir, where `file` is either an absolute path to the file or + a `mozpack.File` instance. + ''' + import buildconfig + + # First, get the list of generated sources produced by the build backend. + gen_sources = os.path.join(buildconfig.topobjdir, 'generated-sources.json') + with open(gen_sources, 'rb') as f: + data = json.load(f) + for f in data['sources']: + yield f, mozpath.join(buildconfig.topobjdir, f) + # Next, return all the files in $objdir/ipc/ipdl/_ipdlheaders. + base = 'ipc/ipdl/_ipdlheaders' + finder = FileFinder(mozpath.join(buildconfig.topobjdir, base)) + for p, f in finder.find('**/*.h'): + yield mozpath.join(base, p), f + # Next, return any Rust source files that were generated into the Rust + # object directory. + rust_build_kind = 'debug' if buildconfig.substs.get('MOZ_DEBUG_RUST') else 'release' + base = mozpath.join('toolkit/library', + buildconfig.substs['RUST_TARGET'], + rust_build_kind, + 'build') + finder = FileFinder(mozpath.join(buildconfig.topobjdir, base)) + for p, f in finder.find('**/*.rs'): + yield mozpath.join(base, p), f + + +def get_s3_region_and_bucket(): + ''' + Return a tuple of (region, bucket) giving the AWS region and S3 + bucket to which generated sources should be uploaded. + ''' + region = 'us-west-2' + level = os.environ.get('MOZ_SCM_LEVEL', '1') + bucket = { + '1': 'gecko-generated-sources-l1', + '2': 'gecko-generated-sources-l2', + '3': 'gecko-generated-sources', + }[level] + return (region, bucket) diff --git a/security/sandbox/linux/moz.build b/security/sandbox/linux/moz.build index 98e53312ef85..7d9cb44ee24f 100644 --- a/security/sandbox/linux/moz.build +++ b/security/sandbox/linux/moz.build @@ -82,9 +82,10 @@ if CONFIG['MOZ_GMP_SANDBOX']: # consistent. See also the comment in SandboxLogging.h. SOURCES['../chromium/base/strings/safe_sprintf.cc'].flags += ['-DNDEBUG'] -# Keep clang from warning about intentional 'switch' fallthrough in icu_utf.cc: if CONFIG['CLANG_CXX']: + # Keep clang from warning about intentional 'switch' fallthrough in icu_utf.cc: SOURCES['../chromium/base/third_party/icu/icu_utf.cc'].flags += ['-Wno-implicit-fallthrough'] + SOURCES['../chromium/sandbox/linux/seccomp-bpf/trap.cc'].flags += ['-Wno-unreachable-code-return'] if CONFIG['GNU_CXX']: CXXFLAGS += ['-Wno-shadow'] diff --git a/taskcluster/ci/upload-generated-sources/kind.yml b/taskcluster/ci/upload-generated-sources/kind.yml new file mode 100644 index 000000000000..af79b2494565 --- /dev/null +++ b/taskcluster/ci/upload-generated-sources/kind.yml @@ -0,0 +1,35 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +loader: taskgraph.loader.single_dep:loader + +transforms: + - taskgraph.transforms.upload_generated_sources:transforms + - taskgraph.transforms.job:transforms + - taskgraph.transforms.task:transforms + +kind-dependencies: + - build + +only-for-attributes: + - nightly + +job-template: + description: Upload generated source files from build + attributes: + nightly: true + worker-type: aws-provisioner-v1/gecko-t-linux-xlarge + treeherder: + symbol: Ugs + kind: build + worker: + docker-image: {in-tree: "lint"} + max-run-time: 600 + run: + using: run-task + command: > + cd /home/worker/checkouts/gecko && + ./mach python build/upload_generated_sources.py ${ARTIFACT_URL} + scopes: + - secrets:get:project/releng/gecko/build/level-{level}/gecko-generated-sources-upload diff --git a/taskcluster/docs/kinds.rst b/taskcluster/docs/kinds.rst index 035db56beb57..40410f98242a 100644 --- a/taskcluster/docs/kinds.rst +++ b/taskcluster/docs/kinds.rst @@ -67,6 +67,11 @@ upload-symbols Upload-symbols tasks run after builds and upload the symbols files generated by build tasks to Socorro for later use in crash analysis. +upload-generated-sources +-------------- + +Upload-generated-sources tasks run after builds and upload source files that were generated as part of the build process to an s3 bucket for later use in links from crash reports or when debugging shipped builds. + valgrind -------- diff --git a/taskcluster/taskgraph/transforms/upload_generated_sources.py b/taskcluster/taskgraph/transforms/upload_generated_sources.py new file mode 100644 index 000000000000..ccfa5ffb8d90 --- /dev/null +++ b/taskcluster/taskgraph/transforms/upload_generated_sources.py @@ -0,0 +1,43 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +""" +Transform the upload-generated-files task description template, + taskcluster/ci/upload-generated-sources/kind.yml +into an actual task description. +""" + +from __future__ import absolute_import, print_function, unicode_literals + +from taskgraph.transforms.base import TransformSequence +from taskgraph.util.taskcluster import get_artifact_url + + +transforms = TransformSequence() + + +@transforms.add +def add_task_info(config, jobs): + for job in jobs: + dep_task = job['dependent-task'] + del job['dependent-task'] + + # Add a dependency on the build task. + job['dependencies'] = {'build': dep_task.label} + # Label the job to match the build task it's uploading from. + job['label'] = dep_task.label.replace("build-", "upload-generated-sources-") + # Copy over some bits of metdata from the build task. + dep_th = dep_task.task['extra']['treeherder'] + job.setdefault('attributes', {}) + job['attributes']['build_platform'] = dep_task.attributes.get('build_platform') + plat = '{}/{}'.format(dep_th['machine']['platform'], dep_task.attributes.get('build_type')) + job['treeherder']['platform'] = plat + job['treeherder']['tier'] = dep_th['tier'] + # Add an environment variable pointing at the artifact from the build. + artifact_url = get_artifact_url('', + 'public/build/target.generated-files.tar.gz') + job['worker'].setdefault('env', {})['ARTIFACT_URL'] = { + 'task-reference': artifact_url + } + + yield job diff --git a/testing/web-platform/meta/MANIFEST.json b/testing/web-platform/meta/MANIFEST.json index a640a0ad6012..030d34424c5f 100644 --- a/testing/web-platform/meta/MANIFEST.json +++ b/testing/web-platform/meta/MANIFEST.json @@ -405911,6 +405911,12 @@ {} ] ], + "webdriver/tests/minimize_window.py": [ + [ + "/webdriver/tests/minimize_window.py", + {} + ] + ], "webdriver/tests/navigation/current_url.py": [ [ "/webdriver/tests/navigation/current_url.py", @@ -626276,6 +626282,10 @@ "d783d0dd370f58b264ef238d8da5cd8601dc3c7f", "testharness" ], + "webdriver/tests/minimize_window.py": [ + "99fa058dd78c2f3a2d2c34dc7096edb9ca12ac4f", + "wdspec" + ], "webdriver/tests/navigation/current_url.py": [ "cec2987258d9c807a247da9e0216b3af1f171484", "wdspec" diff --git a/testing/web-platform/tests/webdriver/tests/minimize_window.py b/testing/web-platform/tests/webdriver/tests/minimize_window.py new file mode 100644 index 000000000000..39c3aac1e253 --- /dev/null +++ b/testing/web-platform/tests/webdriver/tests/minimize_window.py @@ -0,0 +1,56 @@ +from tests.support.inline import inline +from tests.support.asserts import assert_error, assert_success + +alert_doc = inline("") + +# 10.7.4 Minimize Window +def test_minimize_no_browsing_context(session, create_window): + # Step 1 + session.window_handle = create_window() + session.close() + result = session.transport.send("POST", "session/%s/window/minimize" % session.session_id) + assert_error(result, "no such window") + + +def test_handle_user_prompt(session): + # Step 2 + session.url = alert_doc + result = session.transport.send("POST", "session/%s/window/minimize" % session.session_id) + assert_error(result, "unexpected alert open") + + +def test_minimize(session): + before_size = session.window.size + assert session.window.state == "normal" + + # step 4 + result = session.transport.send("POST", "session/%s/window/minimize" % session.session_id) + assert_success(result) + + assert session.window.state == "minimized" + + +def test_payload(session): + before_size = session.window.size + assert session.window.state == "normal" + + result = session.transport.send("POST", "session/%s/window/minimize" % session.session_id) + + # step 5 + assert result.status == 200 + assert isinstance(result.body["value"], dict) + + resp = result.body["value"] + assert "width" in resp + assert "height" in resp + assert "x" in resp + assert "y" in resp + assert "state" in resp + assert isinstance(resp["width"], (int, float)) + assert isinstance(resp["height"], (int, float)) + assert isinstance(resp["x"], (int, float)) + assert isinstance(resp["y"], (int, float)) + assert isinstance(resp["state"], basestring) + + assert session.window.state == "minimized" + diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 02920eca5a33..aadd93426ebf 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -483,106 +483,119 @@ this.ExtensionData = class { }; } - parseManifest() { - return Promise.all([ + async parseManifest() { + let [manifest] = await Promise.all([ this.readJSON("manifest.json"), Management.lazyInit(), - ]).then(([manifest]) => { - this.manifest = manifest; - this.rawManifest = manifest; + ]); - if (manifest && manifest.default_locale) { - return this.initLocale(); + this.manifest = manifest; + this.rawManifest = manifest; + + if (manifest && manifest.default_locale) { + await this.initLocale(); + } + + let context = { + url: this.baseURI && this.baseURI.spec, + + principal: this.principal, + + logError: error => { + this.manifestWarning(error); + }, + + preprocessors: {}, + }; + + if (this.manifest.theme) { + let invalidProps = validateThemeManifest(Object.getOwnPropertyNames(this.manifest)); + + if (invalidProps.length) { + let message = `Themes defined in the manifest may only contain static resources. ` + + `If you would like to use additional properties, please use the "theme" permission instead. ` + + `(the invalid properties found are: ${invalidProps})`; + this.manifestError(message); } - }).then(() => { - let context = { - url: this.baseURI && this.baseURI.spec, + } - principal: this.principal, + if (this.localeData) { + context.preprocessors.localize = (value, context) => this.localize(value); + } - logError: error => { - this.manifestWarning(error); - }, + let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context); + if (normalized.error) { + this.manifestError(normalized.error); + return null; + } - preprocessors: {}, - }; + manifest = normalized.value; - if (this.manifest.theme) { - let invalidProps = validateThemeManifest(Object.getOwnPropertyNames(this.manifest)); + let id; + try { + if (manifest.applications.gecko.id) { + id = manifest.applications.gecko.id; + } + } catch (e) { + // Errors are handled by the type checks above. + } - if (invalidProps.length) { - let message = `Themes defined in the manifest may only contain static resources. ` + - `If you would like to use additional properties, please use the "theme" permission instead. ` + - `(the invalid properties found are: ${invalidProps})`; - this.manifestError(message); + if (!this.id) { + this.id = id; + } + + let apiNames = new Set(); + let dependencies = new Set(); + let originPermissions = new Set(); + let permissions = new Set(); + + for (let perm of manifest.permissions) { + if (perm === "geckoProfiler") { + const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", ""); + if (!acceptedExtensions.split(",").includes(id)) { + this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler."); + continue; } } - if (this.localeData) { - context.preprocessors.localize = (value, context) => this.localize(value); + let type = classifyPermission(perm); + if (type.origin) { + let matcher = new MatchPattern(perm, {ignorePath: true}); + + perm = matcher.pattern; + originPermissions.add(perm); + } else if (type.api) { + apiNames.add(type.api); } - let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context); - if (normalized.error) { - this.manifestError(normalized.error); - return null; - } + permissions.add(perm); + } - let manifest = normalized.value; - - let id; - try { - if (manifest.applications.gecko.id) { - id = manifest.applications.gecko.id; - } - } catch (e) { - // Errors are handled by the type checks above. - } - - let apiNames = new Set(); - let dependencies = new Set(); - let hostPermissions = new Set(); - let permissions = new Set(); - - for (let perm of manifest.permissions) { - if (perm === "geckoProfiler") { - const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", ""); - if (!acceptedExtensions.split(",").includes(id)) { - this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler."); - continue; - } - } - - let type = classifyPermission(perm); - if (type.origin) { - let matcher = new MatchPattern(perm, {ignorePath: true}); - - perm = matcher.pattern; - hostPermissions.add(perm); - } else if (type.api) { - apiNames.add(type.api); - } + if (this.id) { + // An extension always gets permission to its own url. + let matcher = new MatchPattern(this.getURL(), {ignorePath: true}); + originPermissions.add(matcher.pattern); + // Apply optional permissions + let perms = await ExtensionPermissions.get(this); + for (let perm of perms.permissions) { permissions.add(perm); } - - // An extension always gets permission to its own url. - if (this.id) { - let matcher = new MatchPattern(this.getURL(), {ignorePath: true}); - hostPermissions.add(matcher.pattern); + for (let origin of perms.origins) { + originPermissions.add(origin); } + } - for (let api of apiNames) { - dependencies.add(`${api}@experiments.addons.mozilla.org`); - } + for (let api of apiNames) { + dependencies.add(`${api}@experiments.addons.mozilla.org`); + } - // Normalize all patterns to contain a single leading / - let webAccessibleResources = (manifest.web_accessible_resources || []) - .map(path => path.replace(/^\/*/, "/")); + // Normalize all patterns to contain a single leading / + let webAccessibleResources = (manifest.web_accessible_resources || []) + .map(path => path.replace(/^\/*/, "/")); - return {apiNames, dependencies, hostPermissions, id, manifest, permissions, - webAccessibleResources}; - }); + return {apiNames, dependencies, originPermissions, id, manifest, permissions, + webAccessibleResources}; } // Reads the extension's |manifest.json| file, and stores its @@ -608,7 +621,7 @@ this.ExtensionData = class { this.permissions = manifestData.permissions; this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res)); - this.whiteListedHosts = new MatchPatternSet(manifestData.hostPermissions); + this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions); return this.manifest; } @@ -821,7 +834,8 @@ this.Extension = class extends ExtensionData { this.id = addonData.id; this.version = addonData.version; - this.baseURI = Services.io.newURI(this.getURL("")).QueryInterface(Ci.nsIURL); + this.baseURL = this.getURL(""); + this.baseURI = Services.io.newURI(this.baseURL).QueryInterface(Ci.nsIURL); this.principal = this.createPrincipal(); this.views = new Set(); this._backgroundPageFrameLoader = null; @@ -849,12 +863,14 @@ this.Extension = class extends ExtensionData { if (permissions.origins.length > 0) { let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); - this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins], + this.whiteListedHosts = new MatchPatternSet(new Set([...patterns, ...permissions.origins]), {ignorePath: true}); } this.policy.permissions = Array.from(this.permissions); this.policy.allowedOrigins = this.whiteListedHosts; + + this.cachePermissions(); }); this.on("remove-permissions", (ignoreEvent, permissions) => { @@ -871,6 +887,8 @@ this.Extension = class extends ExtensionData { this.policy.permissions = Array.from(this.permissions); this.policy.allowedOrigins = this.whiteListedHosts; + + this.cachePermissions(); }); /* eslint-enable mozilla/balanced-listeners */ } @@ -943,7 +961,17 @@ this.Extension = class extends ExtensionData { let uri = Services.io.newURI(url); let common = this.baseURI.getCommonBaseSpec(uri); - return common == this.baseURI.spec; + return common == this.baseURL; + } + + checkLoadURL(url, options = {}) { + // As an optimization, f the URL starts with the extension's base URL, + // don't do any further checks. It's always allowed to load it. + if (url.startsWith(this.baseURL)) { + return true; + } + + return ExtensionUtils.checkLoadURL(url, this.principal, options); } async promiseLocales(locale) { @@ -961,9 +989,20 @@ this.Extension = class extends ExtensionData { }); } + get manifestCacheKey() { + return [this.id, this.version, Services.locale.getAppLocaleAsLangTag()]; + } + parseManifest() { - return StartupCache.manifests.get([this.id, this.version, Services.locale.getAppLocaleAsLangTag()], - () => super.parseManifest()); + return StartupCache.manifests.get(this.manifestCacheKey, () => super.parseManifest()); + } + + async cachePermissions() { + let manifestData = await this.parseManifest(); + + manifestData.originPermissions = this.whiteListedHosts.patterns.map(pat => pat.pattern); + manifestData.permissions = this.permissions; + return StartupCache.manifests.set(this.manifestCacheKey, manifestData); } async loadManifest() { @@ -1171,10 +1210,7 @@ this.Extension = class extends ExtensionData { TelemetryStopwatch.start("WEBEXT_EXTENSION_STARTUP_MS", this); try { - let [perms] = await Promise.all([ - ExtensionPermissions.get(this), - this.loadManifest(), - ]); + await this.loadManifest(); if (!this.hasShutdown) { await this.initLocale(); @@ -1190,17 +1226,6 @@ this.Extension = class extends ExtensionData { GlobalManager.init(this); - // Apply optional permissions - for (let perm of perms.permissions) { - this.permissions.add(perm); - } - if (perms.origins.length > 0) { - let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); - - this.whiteListedHosts = new MatchPatternSet([...patterns, ...perms.origins], - {ignorePath: true}); - } - this.policy.active = false; this.policy = processScript.initExtension(this); diff --git a/toolkit/components/extensions/ExtensionChild.jsm b/toolkit/components/extensions/ExtensionChild.jsm index 800b6d78c317..3dcee5a9368e 100644 --- a/toolkit/components/extensions/ExtensionChild.jsm +++ b/toolkit/components/extensions/ExtensionChild.jsm @@ -338,6 +338,7 @@ class Messenger { return Promise.reject({message: error.message}); } }); + holder = null; return this.context.wrapPromise(promise, responseCallback); } @@ -374,12 +375,16 @@ class Messenger { }); let message = holder.deserialize(this.context.cloneScope); + holder = null; + sender = Cu.cloneInto(sender, this.context.cloneScope); sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope); // Note: We intentionally do not use runSafe here so that any // errors are propagated to the message sender. let result = fire.raw(message, sender, sendResponse); + message = null; + if (result instanceof this.context.cloneScope.Promise) { return result; } else if (result === true) { @@ -511,6 +516,7 @@ class BrowserExtensionContent extends EventEmitter { this.localeData = new LocaleData(data.localeData); this.manifest = data.manifest; + this.baseURL = data.baseURL; this.baseURI = Services.io.newURI(data.baseURL); // Only used in addon processes. @@ -813,9 +819,6 @@ class ChildAPIManager { * @param {function(*)} [callback] The callback to be called when the function * completes. * @param {object} [options] Extra options. - * @param {boolean} [options.noClone = false] If true, do not clone - * the arguments into an extension sandbox before calling the API - * method. * @returns {Promise|undefined} Must be void if `callback` is set, and a * promise otherwise. The promise is resolved when the function completes. */ @@ -829,7 +832,6 @@ class ChildAPIManager { callId, path, args, - noClone: options.noClone || false, }); return this.context.wrapPromise(deferred.promise, callback); diff --git a/toolkit/components/extensions/ExtensionCommon.jsm b/toolkit/components/extensions/ExtensionCommon.jsm index a9a075c9aed1..be4bbba38df9 100644 --- a/toolkit/components/extensions/ExtensionCommon.jsm +++ b/toolkit/components/extensions/ExtensionCommon.jsm @@ -42,11 +42,10 @@ var { EventEmitter, ExtensionError, defineLazyGetter, + filterStack, getConsole, getInnerWindowID, getUniqueId, - runSafeSync, - runSafeSyncWithoutClone, instanceOf, } = ExtensionUtils; @@ -250,48 +249,55 @@ class BaseContext { throw new Error("Not implemented"); } - runSafe(...args) { + runSafe(callback, ...args) { + return this.applySafe(callback, args); + } + + runSafeWithoutClone(callback, ...args) { + return this.applySafeWithoutClone(callback, args); + } + + applySafe(callback, args) { if (this.unloaded) { Cu.reportError("context.runSafe called after context unloaded"); } else if (!this.active) { Cu.reportError("context.runSafe called while context is inactive"); } else { - return runSafeSync(this, ...args); + try { + let {cloneScope} = this; + args = args.map(arg => Cu.cloneInto(arg, cloneScope)); + } catch (e) { + Cu.reportError(e); + dump(`runSafe failure: cloning into ${this.cloneScope}: ${e}\n\n${filterStack(Error())}`); + } + + return this.applySafeWithoutClone(callback, args); } } - runSafeWithoutClone(...args) { + applySafeWithoutClone(callback, args) { if (this.unloaded) { Cu.reportError("context.runSafeWithoutClone called after context unloaded"); } else if (!this.active) { Cu.reportError("context.runSafeWithoutClone called while context is inactive"); } else { - return runSafeSyncWithoutClone(...args); + try { + return Reflect.apply(callback, null, args); + } catch (e) { + dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`); + Cu.reportError(e); + } } } checkLoadURL(url, options = {}) { - let ssm = Services.scriptSecurityManager; - - let flags = ssm.STANDARD; - if (!options.allowScript) { - flags |= ssm.DISALLOW_SCRIPT; - } - if (!options.allowInheritsPrincipal) { - flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; - } - if (options.dontReportErrors) { - flags |= ssm.DONT_REPORT_ERRORS; + // As an optimization, f the URL starts with the extension's base URL, + // don't do any further checks. It's always allowed to load it. + if (url.startsWith(this.extension.baseURL)) { + return true; } - try { - ssm.checkLoadURIWithPrincipal(this.principal, - Services.io.newURI(url), - flags); - } catch (e) { - return false; - } - return true; + return ExtensionUtils.checkLoadURL(url, this.principal, options); } /** @@ -430,9 +436,9 @@ class BaseContext { * belonging to the target scope. Otherwise, undefined. */ wrapPromise(promise, callback = null) { - let runSafe = this.runSafe.bind(this); - if (promise instanceof this.cloneScope.Promise) { - runSafe = this.runSafeWithoutClone.bind(this); + let applySafe = this.applySafe.bind(this); + if (Cu.getGlobalForObject(promise) === this.cloneScope) { + applySafe = this.applySafeWithoutClone.bind(this); } if (callback) { @@ -443,11 +449,11 @@ class BaseContext { } else if (!this.active) { dump(`Promise resolved while context is inactive\n`); } else if (args instanceof NoCloneSpreadArgs) { - this.runSafeWithoutClone(callback, ...args.unwrappedValues); + this.applySafeWithoutClone(callback, args.unwrappedValues); } else if (args instanceof SpreadArgs) { - runSafe(callback, ...args); + applySafe(callback, args); } else { - runSafe(callback, args); + applySafe(callback, [args]); } }, error => { @@ -457,7 +463,7 @@ class BaseContext { } else if (!this.active) { dump(`Promise rejected while context is inactive\n`); } else { - this.runSafeWithoutClone(callback); + this.applySafeWithoutClone(callback, []); } }); }); @@ -471,11 +477,11 @@ class BaseContext { dump(`Promise resolved while context is inactive\n`); } else if (value instanceof NoCloneSpreadArgs) { let values = value.unwrappedValues; - this.runSafeWithoutClone(resolve, values.length == 1 ? values[0] : values); + this.applySafeWithoutClone(resolve, values.length == 1 ? [values[0]] : [values]); } else if (value instanceof SpreadArgs) { - runSafe(resolve, value.length == 1 ? value[0] : value); + applySafe(resolve, value.length == 1 ? value : [value]); } else { - runSafe(resolve, value); + applySafe(resolve, [value]); } }, value => { @@ -484,7 +490,7 @@ class BaseContext { } else if (!this.active) { dump(`Promise rejected while context is inactive: ${value && value.message}\n`); } else { - this.runSafeWithoutClone(reject, this.normalizeError(value)); + this.applySafeWithoutClone(reject, [this.normalizeError(value)]); } }); }); diff --git a/toolkit/components/extensions/ExtensionParent.jsm b/toolkit/components/extensions/ExtensionParent.jsm index 82cd1a6de4fc..ba34d0e26aaf 100644 --- a/toolkit/components/extensions/ExtensionParent.jsm +++ b/toolkit/components/extensions/ExtensionParent.jsm @@ -45,6 +45,7 @@ var { } = ExtensionCommon; var { + DefaultMap, DefaultWeakMap, ExtensionError, MessageManagerProxy, @@ -413,6 +414,12 @@ class ProxyContextParent extends BaseContext { return this.sandbox; } + runSafe(...args) { + // There's no need to clone when calling listeners for a proxied + // context. + return this.runSafeWithoutClone(...args); + } + get xulBrowser() { return this.messageManagerProxy.eventTarget; } @@ -725,7 +732,7 @@ ParentAPIManager = { }; try { - let args = data.noClone ? data.args : Cu.cloneInto(data.args, context.sandbox); + let args = data.args; let pendingBrowser = context.pendingEventBrowser; let fun = await context.apiCan.asyncFindAPIPath(data.path); let result = context.withPendingBrowser(pendingBrowser, @@ -781,7 +788,7 @@ ParentAPIManager = { context.listenerProxies.set(data.listenerId, listener); - let args = Cu.cloneInto(data.args, context.sandbox); + let args = data.args; let promise = context.apiCan.asyncFindAPIPath(data.path); // Store pending listener additions so we can be sure they're all @@ -1214,13 +1221,11 @@ function extensionNameFromURI(uri) { return GlobalManager.getExtension(id).name; } -const INTEGER = /^[1-9]\d*$/; - // Manages icon details for toolbar buttons in the |pageAction| and // |browserAction| APIs. let IconDetails = { // WeakMap Map object>> - iconCache: new DefaultWeakMap(() => new Map()), + iconCache: new DefaultWeakMap(() => new DefaultMap(() => new Map())), // Normalizes the various acceptable input formats into an object // with icon size as key and icon URL as value. @@ -1232,16 +1237,21 @@ let IconDetails = { // If no context is specified, instead of throwing an error, this // function simply logs a warning message. normalize(details, extension, context = null) { - if (!details.imageData && typeof details.path === "string") { - let icons = this.iconCache.get(extension); + if (!details.imageData && details.path) { + // Pick a cache key for the icon paths. If the path is a string, + // use it directly. Otherwise, stringify the path object. + let key = details.path; + if (typeof key !== "string") { + key = uneval(key); + } - let baseURI = context ? context.uri : extension.baseURI; - let url = baseURI.resolve(details.path); + let icons = this.iconCache.get(extension) + .get(context && context.uri.spec); - let icon = icons.get(url); + let icon = icons.get(key); if (!icon) { icon = this._normalize(details, extension, context); - icons.set(url, icon); + icons.set(key, icon); } return icon; } @@ -1261,9 +1271,6 @@ let IconDetails = { } for (let size of Object.keys(imageData)) { - if (!INTEGER.test(size)) { - throw new ExtensionError(`Invalid icon size ${size}, must be an integer`); - } result[size] = imageData[size]; } } @@ -1276,17 +1283,13 @@ let IconDetails = { } for (let size of Object.keys(path)) { - if (!INTEGER.test(size)) { - throw new ExtensionError(`Invalid icon size ${size}, must be an integer`); - } - let url = baseURI.resolve(path[size]); // The Chrome documentation specifies these parameters as // relative paths. We currently accept absolute URLs as well, // which means we need to check that the extension is allowed // to load them. This will throw an error if it's not allowed. - this._checkURL(url, extension.principal); + this._checkURL(url, extension); result[size] = url; } @@ -1297,8 +1300,8 @@ let IconDetails = { let lightURL = baseURI.resolve(light); let darkURL = baseURI.resolve(dark); - this._checkURL(lightURL, extension.principal); - this._checkURL(darkURL, extension.principal); + this._checkURL(lightURL, extension); + this._checkURL(darkURL, extension); let defaultURL = result[size]; result[size] = { @@ -1324,12 +1327,8 @@ let IconDetails = { // Checks if the extension is allowed to load the given URL with the specified principal. // This will throw an error if the URL is not allowed. - _checkURL(url, principal) { - try { - Services.scriptSecurityManager.checkLoadURIWithPrincipal( - principal, Services.io.newURI(url), - Services.scriptSecurityManager.DISALLOW_SCRIPT); - } catch (e) { + _checkURL(url, extension) { + if (!extension.checkLoadURL(url, {allowInheritsPrincipal: true})) { throw new ExtensionError(`Illegal URL ${url}`); } }, @@ -1531,6 +1530,13 @@ class CacheStore { return result; } + async set(path, value) { + let [store, key] = await this.getStore(path); + + store.set(key, value); + StartupCache.save(); + } + async getAll() { let [store] = await this.getStore(); diff --git a/toolkit/components/extensions/ExtensionUtils.jsm b/toolkit/components/extensions/ExtensionUtils.jsm index 058e87b91766..c9c39979022a 100644 --- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -652,7 +652,32 @@ class MessageManagerProxy { } } +function checkLoadURL(url, principal, options) { + let ssm = Services.scriptSecurityManager; + + let flags = ssm.STANDARD; + if (!options.allowScript) { + flags |= ssm.DISALLOW_SCRIPT; + } + if (!options.allowInheritsPrincipal) { + flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; + } + if (options.dontReportErrors) { + flags |= ssm.DONT_REPORT_ERRORS; + } + + try { + ssm.checkLoadURIWithPrincipal(principal, + Services.io.newURI(url), + flags); + } catch (e) { + return false; + } + return true; +} + this.ExtensionUtils = { + checkLoadURL, defineLazyGetter, flushJarCache, getConsole, diff --git a/toolkit/components/extensions/MessageChannel.jsm b/toolkit/components/extensions/MessageChannel.jsm index 004fc67b6df4..a874b003c1b8 100644 --- a/toolkit/components/extensions/MessageChannel.jsm +++ b/toolkit/components/extensions/MessageChannel.jsm @@ -522,6 +522,7 @@ this.MessageChannel = { let channelId = ExtensionUtils.getUniqueId(); let message = {messageName, channelId, sender, recipient, data, responseType}; + data = null; if (responseType == this.RESPONSE_NONE) { try { @@ -561,6 +562,7 @@ this.MessageChannel = { } catch (e) { deferred.reject(e); } + message = null; return deferred.promise; }, @@ -595,6 +597,7 @@ this.MessageChannel = { return Promise.reject(e); } }); + data = null; responses = responses.filter(response => response !== undefined); switch (responseType) { @@ -634,6 +637,7 @@ this.MessageChannel = { Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e); }); }); + data = null; // Note: Unhandled messages are silently dropped. return; } @@ -649,11 +653,12 @@ this.MessageChannel = { deferred.reject = reject; this._callHandlers(handlers, data).then(resolve, reject); + data = null; }).then( value => { let response = { result: this.RESULT_SUCCESS, - messageName: data.channelId, + messageName: deferred.channelId, recipient: {}, value, }; @@ -673,7 +678,7 @@ this.MessageChannel = { let response = { result: this.RESULT_ERROR, - messageName: data.channelId, + messageName: deferred.channelId, recipient: {}, error: {}, }; diff --git a/toolkit/components/extensions/ext-c-storage.js b/toolkit/components/extensions/ext-c-storage.js index 9e210fc70bee..000bd074d57a 100644 --- a/toolkit/components/extensions/ext-c-storage.js +++ b/toolkit/components/extensions/ext-c-storage.js @@ -94,7 +94,7 @@ this.storage = class extends ExtensionAPI { try { let result = await context.childManager.callParentAsyncFunction("storage.local.get", [ serialize(keys), - ], null, {noClone: true}).then(deserialize); + ]).then(deserialize); TelemetryStopwatch.finish(storageGetHistogram, stopwatchKey); return result; } catch (e) { @@ -108,7 +108,7 @@ this.storage = class extends ExtensionAPI { try { let result = await context.childManager.callParentAsyncFunction("storage.local.set", [ serialize(items), - ], null, {noClone: true}); + ]); TelemetryStopwatch.finish(storageSetHistogram, stopwatchKey); return result; } catch (e) { diff --git a/toolkit/crashreporter/tools/symbolstore.py b/toolkit/crashreporter/tools/symbolstore.py index 64be72505f09..ed98d522d4ec 100755 --- a/toolkit/crashreporter/tools/symbolstore.py +++ b/toolkit/crashreporter/tools/symbolstore.py @@ -41,6 +41,12 @@ import multiprocessing from optparse import OptionParser +from mozbuild.util import memoize +from mozbuild.generated_sources import ( + get_filename_with_digest, + get_generated_sources, + get_s3_region_and_bucket, +) from mozpack.copier import FileRegistry from mozpack.manifests import ( InstallManifest, @@ -331,6 +337,15 @@ def make_file_mapping(install_manifests): file_mapping[abs_dest] = src.path return file_mapping +@memoize +def get_generated_file_s3_path(filename, rel_path, bucket): + """Given a filename, return a path formatted similarly to + GetVCSFilename but representing a file available in an s3 bucket.""" + with open(filename, 'rb') as f: + path = get_filename_with_digest(rel_path, f.read()) + return 's3:{bucket}:{path}:'.format(bucket=bucket, path=path) + + def GetPlatformSpecificDumper(**kwargs): """This function simply returns a instance of a subclass of Dumper that is appropriate for the current platform.""" @@ -376,6 +391,8 @@ class Dumper: copy_debug=False, vcsinfo=False, srcsrv=False, + generated_files=None, + s3_bucket=None, file_mapping=None): # popen likes absolute paths, at least on windows self.dump_syms = os.path.abspath(dump_syms) @@ -389,6 +406,8 @@ class Dumper: self.copy_debug = copy_debug self.vcsinfo = vcsinfo self.srcsrv = srcsrv + self.generated_files = generated_files or {} + self.s3_bucket = s3_bucket self.file_mapping = file_mapping or {} # Add a static mapping for Rust sources. target_os = buildconfig.substs['OS_ARCH'] @@ -496,7 +515,12 @@ class Dumper: if filename in self.file_mapping: filename = self.file_mapping[filename] if self.vcsinfo: - (filename, rootname) = GetVCSFilename(filename, self.srcdirs) + gen_path = self.generated_files.get(filename) + if gen_path and self.s3_bucket: + filename = get_generated_file_s3_path(filename, gen_path, self.s3_bucket) + rootname = '' + else: + (filename, rootname) = GetVCSFilename(filename, self.srcdirs) # sets vcs_root in case the loop through files were to end on an empty rootname if vcs_root is None: if rootname: @@ -667,6 +691,7 @@ class Dumper_Win32(Dumper): os.remove(stream_output_path) return result + class Dumper_Linux(Dumper): objcopy = os.environ['OBJCOPY'] if 'OBJCOPY' in os.environ else 'objcopy' def ShouldProcess(self, file): @@ -840,6 +865,9 @@ to canonical locations in the source repository. Specify parser.error(str(e)) exit(1) file_mapping = make_file_mapping(manifests) + generated_files = {os.path.join(buildconfig.topobjdir, f): f + for (f, _) in get_generated_sources()} + _, bucket = get_s3_region_and_bucket() dumper = GetPlatformSpecificDumper(dump_syms=args[0], symbol_path=args[1], copy_debug=options.copy_debug, @@ -847,6 +875,8 @@ to canonical locations in the source repository. Specify srcdirs=options.srcdir, vcsinfo=options.vcsinfo, srcsrv=options.srcsrv, + generated_files=generated_files, + s3_bucket=bucket, file_mapping=file_mapping) dumper.Process(args[2]) diff --git a/toolkit/crashreporter/tools/unit-symbolstore.py b/toolkit/crashreporter/tools/unit-symbolstore.py index c8452aefcee2..3461456861d3 100644 --- a/toolkit/crashreporter/tools/unit-symbolstore.py +++ b/toolkit/crashreporter/tools/unit-symbolstore.py @@ -214,6 +214,28 @@ class TestGetVCSFilename(HelperMixin, unittest.TestCase): symbolstore.GetVCSFilename(filename, [self.test_dir])[0]) +# SHA-512 of a zero-byte file +EMPTY_SHA512 = 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e' + + +class TestGeneratedFilePath(HelperMixin, unittest.TestCase): + def setUp(self): + HelperMixin.setUp(self) + + def tearDown(self): + HelperMixin.tearDown(self) + + def test_generated_file_path(self): + # Make an empty generated file + g = os.path.join(self.test_dir, 'generated') + rel_path = 'a/b/generated' + with open(g, 'wb') as f: + pass + expected = 's3:bucket:{}/{}:'.format(EMPTY_SHA512, + rel_path) + self.assertEqual(expected, symbolstore.get_generated_file_s3_path(g, rel_path, 'bucket')) + + if target_platform() == 'WINNT': class TestFixFilenameCase(HelperMixin, unittest.TestCase): def test_fix_filename_case(self): diff --git a/toolkit/mozapps/installer/package-name.mk b/toolkit/mozapps/installer/package-name.mk index 90d707f1bca4..d572c87a7e3a 100644 --- a/toolkit/mozapps/installer/package-name.mk +++ b/toolkit/mozapps/installer/package-name.mk @@ -80,6 +80,9 @@ PKG_SRCPACK_PATH = SYMBOL_FULL_ARCHIVE_BASENAME = $(PKG_BASENAME).crashreporter-symbols-full SYMBOL_ARCHIVE_BASENAME = $(PKG_BASENAME).crashreporter-symbols +# Generated file package naming +GENERATED_SOURCE_FILE_PACKAGE = $(PKG_BASENAME).generated-files.tar.gz + # Code coverage package naming CODE_COVERAGE_ARCHIVE_BASENAME = $(PKG_BASENAME).code-coverage-gcno diff --git a/toolkit/mozapps/installer/upload-files.mk b/toolkit/mozapps/installer/upload-files.mk index 5ca364277987..f5237976893f 100644 --- a/toolkit/mozapps/installer/upload-files.mk +++ b/toolkit/mozapps/installer/upload-files.mk @@ -397,6 +397,7 @@ UPLOAD_FILES= \ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(WP_TEST_PACKAGE)) \ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(GTEST_PACKAGE)) \ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(SYMBOL_ARCHIVE_BASENAME).zip) \ + $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(GENERATED_SOURCE_FILE_PACKAGE)) \ $(call QUOTED_WILDCARD,$(MOZ_SOURCESTAMP_FILE)) \ $(call QUOTED_WILDCARD,$(MOZ_BUILDINFO_FILE)) \ $(call QUOTED_WILDCARD,$(MOZ_BUILDID_INFO_TXT_FILE)) \ diff --git a/tools/fuzzing/libfuzzer/moz.build b/tools/fuzzing/libfuzzer/moz.build index b4e1bce4e60e..9a6ab630534e 100644 --- a/tools/fuzzing/libfuzzer/moz.build +++ b/tools/fuzzing/libfuzzer/moz.build @@ -34,4 +34,7 @@ SOURCES += [ 'FuzzerUtilLinux.cpp', 'FuzzerUtilPosix.cpp', 'FuzzerUtilWindows.cpp' -] \ No newline at end of file +] + +if CONFIG['CLANG_CXX']: + CXXFLAGS += ['-Wno-unreachable-code-return'] diff --git a/tools/update-packaging/Makefile.in b/tools/update-packaging/Makefile.in index efd1c2391dfc..2f59a18ecc6b 100644 --- a/tools/update-packaging/Makefile.in +++ b/tools/update-packaging/Makefile.in @@ -57,7 +57,7 @@ endif '$(DIST)/$(COMPLETE_MAR)' \ '$(PACKAGE_DIR)' ifdef MOZ_SIGN_CMD - $(MOZ_SIGN_CMD) -f mar '$(DIST)/$(COMPLETE_MAR)' + $(MOZ_SIGN_CMD) -f mar_sha384 '$(DIST)/$(COMPLETE_MAR)' endif partial-patch:: $(dir-stage) @@ -69,5 +69,5 @@ partial-patch:: $(dir-stage) '$(SRC_BUILD)' \ '$(DST_BUILD)' ifdef MOZ_SIGN_CMD - $(MOZ_SIGN_CMD) -f mar '$(STAGE_DIR)/$(PKG_UPDATE_BASENAME).partial.$(SRC_BUILD_ID)-$(DST_BUILD_ID).mar' + $(MOZ_SIGN_CMD) -f mar_sha384 '$(STAGE_DIR)/$(PKG_UPDATE_BASENAME).partial.$(SRC_BUILD_ID)-$(DST_BUILD_ID).mar' endif diff --git a/xpcom/threads/PrioritizedEventQueue.cpp b/xpcom/threads/PrioritizedEventQueue.cpp index 57fcba9232d4..1c422ec63c44 100644 --- a/xpcom/threads/PrioritizedEventQueue.cpp +++ b/xpcom/threads/PrioritizedEventQueue.cpp @@ -275,6 +275,7 @@ class PrioritizedEventQueue::EnablePrioritizationRunnable final public: explicit EnablePrioritizationRunnable(PrioritizedEventQueue* aQueue) : Runnable("EnablePrioritizationRunnable") + , mQueue(aQueue) {} NS_IMETHOD Run() override @@ -289,6 +290,8 @@ public: } private: + // This is a weak pointer. It's guaranteed to stay alive until this runnable + // runs since it functions as the event loop in which the runnable is posted. PrioritizedEventQueue* mQueue; };