Bug 1480612 - Part 2: Session restore, fixes for screen listing and args, session store fixes and tests. r=agashlin, a=RyanVM
authorAdam Gashlin <agashlin@mozilla.com>
Fri, 08 Jun 2018 15:16:16 -0700
changeset 473824 c7b8ddc5a878
parent 473823 1b3c76905dab
child 473825 338feb0782ff
push id1751
push userryanvm@gmail.com
push date2018-08-07 17:02 +0000
treeherdermozilla-release@975058795980 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersagashlin, RyanVM
bugs1480612, 603903, 1463560, 1458119, 1466071
milestone61.0.2
Bug 1480612 - Part 2: Session restore, fixes for screen listing and args, session store fixes and tests. r=agashlin, a=RyanVM This is a roll-up of patches landed for Firefox 62+ in bug 603903, bug 1463560, bug 1458119, and bug 1466071.
browser/app/profile/firefox.js
browser/components/nsBrowserGlue.js
browser/components/sessionstore/SessionStore.jsm
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/nsSessionStartup.js
browser/modules/BrowserWindowTracker.jsm
testing/firefox-ui/tests/functional/sessionstore/manifest.ini
testing/firefox-ui/tests/functional/sessionstore/session_store_test_case.py
testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_restart_and_quit.py
testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_windows_shutdown.py
toolkit/xre/nsAppRunner.cpp
toolkit/xre/nsAppRunner.h
toolkit/xre/nsUpdateDriver.cpp
widget/windows/ScreenHelperWin.cpp
widget/windows/nsWindow.cpp
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -854,16 +854,17 @@ pref("browser.rights.3.shown", false);
 
 #ifdef DEBUG
 // Don't show the about:rights notification in debug builds.
 pref("browser.rights.override", true);
 #endif
 
 pref("browser.sessionstore.resume_from_crash", true);
 pref("browser.sessionstore.resume_session_once", false);
+pref("browser.sessionstore.resuming_after_os_restart", false);
 
 // Minimal interval between two save operations in milliseconds (while the user is active).
 pref("browser.sessionstore.interval", 15000); // 15 seconds
 
 // Minimal interval between two save operations in milliseconds (while the user is idle).
 pref("browser.sessionstore.interval.idle", 3600000); // 1h
 
 // Time (ms) before we assume that the user is idle and that we don't need to
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1333,17 +1333,17 @@ BrowserGlue.prototype = {
     //    - The quit dialog will be shown
     // 2. aQuitType == "lastwindow" && browser.tabs.warnOnClose == true
     //    - The "closing multiple tabs" dialog will be shown
     //
     // aQuitType == "lastwindow" is overloaded. "lastwindow" is used to indicate
     // "the last window is closing but we're not quitting (a non-browser window is open)"
     // and also "we're quitting by closing the last window".
 
-    if (aQuitType == "restart")
+    if (aQuitType == "restart" || aQuitType == "os-restart")
       return;
 
     var windowcount = 0;
     var pagecount = 0;
     var browserEnum = Services.wm.getEnumerator("navigator:browser");
     let allWindowsPrivate = true;
     while (browserEnum.hasMoreElements()) {
       // XXXbz should we skip closed windows here?
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -1621,17 +1621,23 @@ var SessionStoreInternal = {
           // Set up the list of promises that will signal a complete sessionstore
           // shutdown: either all data is saved, or we crashed or the message IPC
           // channel went away in the meantime.
           let promises = [this.flushAllWindowsAsync(progress)];
 
           const observeTopic = topic => {
             let deferred = PromiseUtils.defer();
             const cleanup = () => Services.obs.removeObserver(deferred.resolve, topic);
-            Services.obs.addObserver(deferred.resolve, topic);
+            Services.obs.addObserver(subject => {
+              // Skip abort on ipc:content-shutdown if not abnormal/crashed
+              subject.QueryInterface(Ci.nsIPropertyBag2);
+              if (!(topic == "ipc:content-shutdown" && !subject.get("abnormal"))) {
+                deferred.resolve();
+              }
+            }, topic);
             deferred.promise.then(cleanup, cleanup);
             return deferred;
           };
 
           // Build a list of deferred executions that require cleanup once the
           // Promise race is won.
           // Ensure that the timer fires earlier than the AsyncShutdown crash timer.
           let waitTimeMaxMs = Math.max(0, AsyncShutdown.DELAY_CRASH_MS - 10000);
@@ -1722,18 +1728,22 @@ var SessionStoreInternal = {
   },
 
   /**
    * On quitting application
    * @param aData
    *        String type of quitting
    */
   onQuitApplication: function ssi_onQuitApplication(aData) {
-    if (aData == "restart") {
+    if (aData == "restart" || aData == "os-restart") {
       if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
+        if (aData == "os-restart" &&
+            !this._prefBranch.getBoolPref("sessionstore.resume_session_once")) {
+          this._prefBranch.setBoolPref("sessionstore.resuming_after_os_restart", true);
+        }
         this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
       }
 
       // The browser:purge-session-history notification fires after the
       // quit-application notification so unregister the
       // browser:purge-session-history notification to prevent clearing
       // session data on disk on a restart.  It is also unnecessary to
       // perform any other sanitization processing on a restart as the
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -729,20 +729,20 @@ var MessageQueue = {
   /**
    * Whether or not sending batched messages on a timer is disabled. This should
    * only be used for debugging or testing. If you need to access this value,
    * you should probably use the timeoutDisabled getter.
    */
   _timeoutDisabled: false,
 
   /**
-   * The idle callback ID referencing an active idle callback. When no idle
-   * callback is pending, this is null.
-   * */
-  _idleCallbackID: null,
+   * True if there is already a send pending idle dispatch, set to prevent
+   * scheduling more than one. If false there may or may not be one scheduled.
+   */
+  _idleScheduled: false,
 
   /**
    * True if batched messages are not being fired on a timer. This should only
    * ever be true when debugging or during tests.
    */
   get timeoutDisabled() {
     return this._timeoutDisabled;
   },
@@ -777,20 +777,17 @@ var MessageQueue = {
     Services.prefs.removeObserver(PREF_INTERVAL, this);
     this.cleanupTimers();
   },
 
   /**
    * Cleanup pending idle callback and timer.
    */
   cleanupTimers() {
-    if (this._idleCallbackID) {
-      content.cancelIdleCallback(this._idleCallbackID);
-      this._idleCallbackID = null;
-    }
+    this._idleScheduled = false;
     if (this._timeout) {
       clearTimeout(this._timeout);
       this._timeout = null;
     }
   },
 
   observe(subject, topic, data) {
     if (topic == "nsPref:changed") {
@@ -832,36 +829,36 @@ var MessageQueue = {
   },
 
   /**
    * Sends queued data when the remaining idle time is enough or waiting too
    * long; otherwise, request an idle time again. If the |deadline| is not
    * given, this function is going to schedule the first request.
    *
    * @param deadline (object)
-   *        An IdleDeadline object passed by requestIdleCallback().
+   *        An IdleDeadline object passed by idleDispatch().
    */
   sendWhenIdle(deadline) {
     if (!content) {
       // The frameloader is being torn down. Nothing more to do.
       return;
     }
 
     if (deadline) {
       if (deadline.didTimeout || deadline.timeRemaining() > MessageQueue.NEEDED_IDLE_PERIOD_MS) {
         MessageQueue.send();
         return;
       }
-    } else if (MessageQueue._idleCallbackID) {
+    } else if (MessageQueue._idleScheduled) {
       // Bail out if there's a pending run.
       return;
     }
-    MessageQueue._idleCallbackID =
-      content.requestIdleCallback(MessageQueue.sendWhenIdle, {timeout: MessageQueue._timeoutWaitIdlePeriodMs});
-   },
+    ChromeUtils.idleDispatch(MessageQueue.sendWhenIdle, {timeout: MessageQueue._timeoutWaitIdlePeriodMs});
+    MessageQueue._idleScheduled = true;
+  },
 
   /**
    * Sends queued data to the chrome process.
    *
    * @param options (object)
    *        {flushID: 123} to specify that this is a flush
    *        {isFinal: true} to signal this is the final message sent on unload
    */
--- a/browser/components/sessionstore/nsSessionStartup.js
+++ b/browser/components/sessionstore/nsSessionStartup.js
@@ -97,16 +97,27 @@ SessionStartup.prototype = {
 
     // do not need to initialize anything in auto-started private browsing sessions
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       this._initialized = true;
       gOnceInitializedDeferred.resolve();
       return;
     }
 
+    if (Services.prefs.getBoolPref("browser.sessionstore.resuming_after_os_restart")) {
+      if (!Services.appinfo.restartedByOS) {
+        // We had set resume_session_once in order to resume after an OS restart,
+        // but we aren't automatically started by the OS (or else appinfo.restartedByOS
+        // would have been set). Therefore we should clear resume_session_once
+        // to avoid forcing a resume for a normal startup.
+        Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false);
+      }
+      Services.prefs.setBoolPref("browser.sessionstore.resuming_after_os_restart", false);
+    }
+
     this._resumeSessionEnabled =
       Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
       Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
 
     SessionFile.read().then(
       this._onSessionFileRead.bind(this),
       console.error
     );
--- a/browser/modules/BrowserWindowTracker.jsm
+++ b/browser/modules/BrowserWindowTracker.jsm
@@ -206,17 +206,19 @@ this.BrowserWindowTracker = {
   /**
    * Iterator property that yields window objects by z-index, in reverse order.
    * This means that the lastly focused window will the first item that is yielded.
    * Note: we only know the order of windows we're actively tracking, which
    * basically means _only_ browser windows.
    */
   orderedWindows: {
     * [Symbol.iterator]() {
-      for (let window of _trackedWindows)
+      // Clone the windows array immediately as it may change during iteration,
+      // we'd rather have an outdated order than skip/revisit windows.
+      for (let window of [..._trackedWindows])
         yield window;
     }
   },
 
   track(window) {
     return WindowHelper.addWindow(window);
   }
 };
--- a/testing/firefox-ui/tests/functional/sessionstore/manifest.ini
+++ b/testing/firefox-ui/tests/functional/sessionstore/manifest.ini
@@ -1,4 +1,6 @@
 [DEFAULT]
 tags = local
 
 [test_restore_windows_after_restart_and_quit.py]
+[test_restore_windows_after_windows_shutdown.py]
+skip-if = os != "win"
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/functional/sessionstore/session_store_test_case.py
@@ -0,0 +1,285 @@
+# 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 firefox_puppeteer import PuppeteerMixin
+from marionette_harness import MarionetteTestCase
+
+
+class SessionStoreTestCase(PuppeteerMixin, MarionetteTestCase):
+
+    def setUp(self, startup_page=1, include_private=True, no_auto_updates=True):
+        super(SessionStoreTestCase, self).setUp()
+        # Each list element represents a window of tabs loaded at
+        # some testing URL
+        self.test_windows = set([
+            # Window 1. Note the comma after the absolute_url call -
+            # this is Python's way of declaring a 1 item tuple.
+            (self.marionette.absolute_url('layout/mozilla.html'), ),
+
+            # Window 2
+            (self.marionette.absolute_url('layout/mozilla_organizations.html'),
+             self.marionette.absolute_url('layout/mozilla_community.html')),
+
+            # Window 3
+            (self.marionette.absolute_url('layout/mozilla_governance.html'),
+             self.marionette.absolute_url('layout/mozilla_grants.html')),
+        ])
+
+        self.private_windows = set([
+            (self.marionette.absolute_url('layout/mozilla_mission.html'),
+             self.marionette.absolute_url('layout/mozilla_organizations.html')),
+
+            (self.marionette.absolute_url('layout/mozilla_projects.html'),
+             self.marionette.absolute_url('layout/mozilla_mission.html')),
+        ])
+
+        self.marionette.enforce_gecko_prefs({
+            # Set browser restore previous session pref,
+            # depending on what the test requires.
+            'browser.startup.page': startup_page,
+            # Make the content load right away instead of waiting for
+            # the user to click on the background tabs
+            'browser.sessionstore.restore_on_demand': False,
+            # Avoid race conditions by having the content process never
+            # send us session updates unless the parent has explicitly asked
+            # for them via the TabStateFlusher.
+            'browser.sessionstore.debug.no_auto_updates': no_auto_updates,
+        })
+
+        self.all_windows = self.test_windows.copy()
+        self.open_windows(self.test_windows)
+
+        if include_private:
+            self.all_windows.update(self.private_windows)
+            self.open_windows(self.private_windows, is_private=True)
+
+    def tearDown(self):
+        try:
+            # Create a fresh profile for subsequent tests.
+            self.restart(clean=True)
+        finally:
+            super(SessionStoreTestCase, self).tearDown()
+
+    def open_windows(self, window_sets, is_private=False):
+        """ Opens a set of windows with tabs pointing at some
+        URLs.
+
+        @param window_sets (list)
+               A set of URL tuples. Each tuple within window_sets
+               represents a window, and each URL in the URL
+               tuples represents what will be loaded in a tab.
+
+               Note that if is_private is False, then the first
+               URL tuple will be opened in the current window, and
+               subequent tuples will be opened in new windows.
+
+               Example:
+
+               set(
+                   (self.marionette.absolute_url('layout/mozilla_1.html'),
+                    self.marionette.absolute_url('layout/mozilla_2.html')),
+
+                   (self.marionette.absolute_url('layout/mozilla_3.html'),
+                    self.marionette.absolute_url('layout/mozilla_4.html')),
+               )
+
+               This would take the currently open window, and load
+               mozilla_1.html and mozilla_2.html in new tabs. It would
+               then open a new, second window, and load tabs at
+               mozilla_3.html and mozilla_4.html.
+        @param is_private (boolean, optional)
+               Whether or not any new windows should be a private browsing
+               windows.
+        """
+
+        if (is_private):
+            win = self.browser.open_browser(is_private=True)
+            win.switch_to()
+        else:
+            win = self.browser
+
+        for index, urls in enumerate(window_sets):
+            if index > 0:
+                win = self.browser.open_browser(is_private=is_private)
+            win.switch_to()
+            self.open_tabs(win, urls)
+
+    def open_tabs(self, win, urls):
+        """ Opens a set of URLs inside a window in new tabs.
+
+        @param win (browser window)
+               The browser window to load the tabs in.
+        @param urls (tuple)
+               A tuple of URLs to load in this window. The
+               first URL will be loaded in the currently selected
+               browser tab. Subsequent URLs will be loaded in
+               new tabs.
+        """
+        # If there are any remaining URLs for this window,
+        # open some new tabs and navigate to them.
+        with self.marionette.using_context('content'):
+            if isinstance(urls, str):
+                self.marionette.navigate(urls)
+            else:
+                for index, url in enumerate(urls):
+                    if index > 0:
+                        with self.marionette.using_context('chrome'):
+                            win.tabbar.open_tab()
+                    self.marionette.navigate(url)
+
+    def convert_open_windows_to_set(self):
+        windows = self.puppeteer.windows.all
+
+        # There's no guarantee that Marionette will return us an
+        # iterator for the opened windows that will match the
+        # order within our window list. Instead, we'll convert
+        # the list of URLs within each open window to a set of
+        # tuples that will allow us to do a direct comparison
+        # while allowing the windows to be in any order.
+
+        opened_windows = set()
+        for win in windows:
+            urls = tuple()
+            for tab in win.tabbar.tabs:
+                urls = urls + tuple([tab.location])
+            opened_windows.add(urls)
+        return opened_windows
+
+    def simulate_os_shutdown(self):
+        """ Simulate an OS shutdown.
+
+        :raises: Exception: if not supported on the current platform
+        :raises: WindowsError: if a Windows API call failed
+        """
+
+        if self.marionette.session_capabilities['platformName'] != 'windows_nt':
+            raise Exception('Unsupported platform for simulate_os_shutdown')
+
+        self._shutdown_with_windows_restart_manager(self.marionette.process_id)
+
+    def _shutdown_with_windows_restart_manager(self, pid):
+        """ Shut down a process using the Windows Restart Manager.
+
+        When Windows shuts down, it uses a protocol including the
+        WM_QUERYENDSESSION and WM_ENDSESSION messages to give
+        applications a chance to shut down safely. The best way to
+        simulate this is via the Restart Manager, which allows a process
+        (such as an installer) to use the same mechanism to shut down
+        any other processes which are using registered resources.
+
+        This function starts a Restart Manager session, registers the
+        process as a resource, and shuts down the process.
+
+        :param pid: The process id (int) of the process to shutdown
+
+        :raises: WindowsError: if a Windows API call fails
+        """
+
+        import ctypes
+        from ctypes import Structure, POINTER, WINFUNCTYPE, windll, pointer, WinError
+        from ctypes.wintypes import HANDLE, DWORD, BOOL, WCHAR, UINT, ULONG, LPCWSTR
+
+        # set up Windows SDK types
+        OpenProcess = windll.kernel32.OpenProcess
+        OpenProcess.restype = HANDLE
+        OpenProcess.argtypes = [DWORD,  # dwDesiredAccess
+                                BOOL,   # bInheritHandle
+                                DWORD]  # dwProcessId
+        PROCESS_QUERY_INFORMATION = 0x0400
+
+        class FILETIME(Structure):
+            _fields_ = [('dwLowDateTime', DWORD),
+                        ('dwHighDateTime', DWORD)]
+        LPFILETIME = POINTER(FILETIME)
+
+        GetProcessTimes = windll.kernel32.GetProcessTimes
+        GetProcessTimes.restype = BOOL
+        GetProcessTimes.argtypes = [HANDLE,      # hProcess
+                                    LPFILETIME,  # lpCreationTime
+                                    LPFILETIME,  # lpExitTime
+                                    LPFILETIME,  # lpKernelTime
+                                    LPFILETIME]  # lpUserTime
+
+        ERROR_SUCCESS = 0
+
+        class RM_UNIQUE_PROCESS(Structure):
+            _fields_ = [('dwProcessId', DWORD),
+                        ('ProcessStartTime', FILETIME)]
+
+        RmStartSession = windll.rstrtmgr.RmStartSession
+        RmStartSession.restype = DWORD
+        RmStartSession.argtypes = [POINTER(DWORD),  # pSessionHandle
+                                   DWORD,           # dwSessionFlags
+                                   POINTER(WCHAR)]  # strSessionKey
+
+        class GUID(ctypes.Structure):
+            _fields_ = [('Data1', ctypes.c_ulong),
+                        ('Data2', ctypes.c_ushort),
+                        ('Data3', ctypes.c_ushort),
+                        ('Data4', ctypes.c_ubyte * 8)]
+        CCH_RM_SESSION_KEY = ctypes.sizeof(GUID) * 2
+
+        RmRegisterResources = windll.rstrtmgr.RmRegisterResources
+        RmRegisterResources.restype = DWORD
+        RmRegisterResources.argtypes = [DWORD,             # dwSessionHandle
+                                        UINT,              # nFiles
+                                        POINTER(LPCWSTR),  # rgsFilenames
+                                        UINT,              # nApplications
+                                        POINTER(RM_UNIQUE_PROCESS),  # rgApplications
+                                        UINT,              # nServices
+                                        POINTER(LPCWSTR)]  # rgsServiceNames
+
+        RM_WRITE_STATUS_CALLBACK = WINFUNCTYPE(None, UINT)
+        RmShutdown = windll.rstrtmgr.RmShutdown
+        RmShutdown.restype = DWORD
+        RmShutdown.argtypes = [DWORD,  # dwSessionHandle
+                               ULONG,  # lActionFlags
+                               RM_WRITE_STATUS_CALLBACK]  # fnStatus
+
+        RmEndSession = windll.rstrtmgr.RmEndSession
+        RmEndSession.restype = DWORD
+        RmEndSession.argtypes = [DWORD]  # dwSessionHandle
+
+        # Get the info needed to uniquely identify the process
+        hProc = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)
+        if not hProc:
+            raise WinError()
+
+        creationTime = FILETIME()
+        exitTime = FILETIME()
+        kernelTime = FILETIME()
+        userTime = FILETIME()
+        if not GetProcessTimes(hProc,
+                               pointer(creationTime),
+                               pointer(exitTime),
+                               pointer(kernelTime),
+                               pointer(userTime)):
+            raise WinError()
+
+        # Start the Restart Manager Session
+        dwSessionHandle = DWORD()
+        sessionKeyType = WCHAR * (CCH_RM_SESSION_KEY + 1)
+        sessionKey = sessionKeyType()
+        if RmStartSession(pointer(dwSessionHandle), 0, sessionKey) != ERROR_SUCCESS:
+            raise WinError()
+
+        try:
+            UProcs_count = 1
+            UProcsArrayType = RM_UNIQUE_PROCESS * UProcs_count
+            UProcs = UProcsArrayType(RM_UNIQUE_PROCESS(pid, creationTime))
+
+            # Register the process as a resource
+            if RmRegisterResources(dwSessionHandle,
+                                   0, None,
+                                   UProcs_count, UProcs,
+                                   0, None) != ERROR_SUCCESS:
+                raise WinError()
+
+            # Shut down all processes using registered resources
+            if RmShutdown(dwSessionHandle, 0,
+                          ctypes.cast(None, RM_WRITE_STATUS_CALLBACK)) != ERROR_SUCCESS:
+                raise WinError()
+
+        finally:
+            RmEndSession(dwSessionHandle)
--- a/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_restart_and_quit.py
+++ b/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_restart_and_quit.py
@@ -1,161 +1,34 @@
 # 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 firefox_puppeteer import PuppeteerMixin
-from marionette_harness import MarionetteTestCase
+# add this directory to the path
+import sys
+import os
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
 
 
-class TestBaseRestoreWindows(PuppeteerMixin, MarionetteTestCase):
-
-    def setUp(self, startup_page=1):
-        super(TestBaseRestoreWindows, self).setUp()
-        # Each list element represents a window of tabs loaded at
-        # some testing URL
-        self.test_windows = set([
-            # Window 1. Note the comma after the absolute_url call -
-            # this is Python's way of declaring a 1 item tuple.
-            (self.marionette.absolute_url('layout/mozilla.html'), ),
-
-            # Window 2
-            (self.marionette.absolute_url('layout/mozilla_organizations.html'),
-             self.marionette.absolute_url('layout/mozilla_community.html')),
-
-            # Window 3
-            (self.marionette.absolute_url('layout/mozilla_governance.html'),
-             self.marionette.absolute_url('layout/mozilla_grants.html')),
-        ])
-
-        self.private_windows = set([
-            (self.marionette.absolute_url('layout/mozilla_mission.html'),
-             self.marionette.absolute_url('layout/mozilla_organizations.html')),
-
-            (self.marionette.absolute_url('layout/mozilla_projects.html'),
-             self.marionette.absolute_url('layout/mozilla_mission.html')),
-        ])
-
-        self.all_windows = self.test_windows | self.private_windows
+class TestSessionStoreEnabledAllWindows(SessionStoreTestCase):
 
-        self.marionette.enforce_gecko_prefs({
-            # Set browser restore previous session pref,
-            # depending on what the test requires.
-            'browser.startup.page': startup_page,
-            # Make the content load right away instead of waiting for
-            # the user to click on the background tabs
-            'browser.sessionstore.restore_on_demand': False,
-            # Avoid race conditions by having the content process never
-            # send us session updates unless the parent has explicitly asked
-            # for them via the TabStateFlusher.
-            'browser.sessionstore.debug.no_auto_updates': True,
-        })
-
-        self.open_windows(self.test_windows)
-        self.open_windows(self.private_windows, is_private=True)
-
-    def tearDown(self):
-        try:
-            # Create a fresh profile for subsequent tests.
-            self.restart(clean=True)
-        finally:
-            super(TestBaseRestoreWindows, self).tearDown()
-
-    def open_windows(self, window_sets, is_private=False):
-        """ Opens a set of windows with tabs pointing at some
-        URLs.
-
-        @param window_sets (list)
-               A set of URL tuples. Each tuple within window_sets
-               represents a window, and each URL in the URL
-               tuples represents what will be loaded in a tab.
-
-               Note that if is_private is False, then the first
-               URL tuple will be opened in the current window, and
-               subequent tuples will be opened in new windows.
-
-               Example:
+    def setUp(self, include_private=True):
+        """ Setup for the test, enabling session restore.
 
-               set(
-                   (self.marionette.absolute_url('layout/mozilla_1.html'),
-                    self.marionette.absolute_url('layout/mozilla_2.html')),
-
-                   (self.marionette.absolute_url('layout/mozilla_3.html'),
-                    self.marionette.absolute_url('layout/mozilla_4.html')),
-               )
-
-               This would take the currently open window, and load
-               mozilla_1.html and mozilla_2.html in new tabs. It would
-               then open a new, second window, and load tabs at
-               mozilla_3.html and mozilla_4.html.
-        @param is_private (boolean, optional)
-               Whether or not any new windows should be a private browsing
-               windows.
+        :param include_private: Whether to open private windows.
         """
-
-        if (is_private):
-            win = self.browser.open_browser(is_private=True)
-            win.switch_to()
-        else:
-            win = self.browser
-
-        for index, urls in enumerate(window_sets):
-            if index > 0:
-                win = self.browser.open_browser(is_private=is_private)
-            win.switch_to()
-            self.open_tabs(win, urls)
-
-    def open_tabs(self, win, urls):
-        """ Opens a set of URLs inside a window in new tabs.
-
-        @param win (browser window)
-               The browser window to load the tabs in.
-        @param urls (tuple)
-               A tuple of URLs to load in this window. The
-               first URL will be loaded in the currently selected
-               browser tab. Subsequent URLs will be loaded in
-               new tabs.
-        """
-        # If there are any remaining URLs for this window,
-        # open some new tabs and navigate to them.
-        with self.marionette.using_context('content'):
-            if isinstance(urls, str):
-                self.marionette.navigate(urls)
-            else:
-                for index, url in enumerate(urls):
-                    if index > 0:
-                        with self.marionette.using_context('chrome'):
-                            win.tabbar.open_tab()
-                    self.marionette.navigate(url)
-
-    def convert_open_windows_to_set(self):
-        windows = self.puppeteer.windows.all
-
-        # There's no guarantee that Marionette will return us an
-        # iterator for the opened windows that will match the
-        # order within our window list. Instead, we'll convert
-        # the list of URLs within each open window to a set of
-        # tuples that will allow us to do a direct comparison
-        # while allowing the windows to be in any order.
-
-        opened_windows = set()
-        for win in windows:
-            urls = tuple()
-            for tab in win.tabbar.tabs:
-                urls = urls + tuple([tab.location])
-            opened_windows.add(urls)
-        return opened_windows
-
-
-class TestSessionStoreEnabled(TestBaseRestoreWindows):
-    def setUp(self):
-        super(TestSessionStoreEnabled, self).setUp(startup_page=3)
+        super(TestSessionStoreEnabledAllWindows, self).setUp(
+            include_private=include_private, startup_page=3)
 
     def test_with_variety(self):
-        """ Opens a set of windows, both standard and private, with
+        """ Test opening and restoring both standard and private windows.
+
+        Opens a set of windows, both standard and private, with
         some number of tabs in them. Once the tabs have loaded, restarts
         the browser, and then ensures that the standard tabs have been
         restored, and that the private ones have not.
         """
         current_windows_set = self.convert_open_windows_to_set()
         self.assertEqual(current_windows_set, self.all_windows,
                          msg='Not all requested windows have been opened. Expected {}, got {}.'
                          .format(self.all_windows, current_windows_set))
@@ -166,17 +39,24 @@ class TestSessionStoreEnabled(TestBaseRe
 
         current_windows_set = self.convert_open_windows_to_set()
         self.assertEqual(current_windows_set, self.test_windows,
                          msg="""Non private browsing windows should have
                          been restored. Expected {}, got {}.
                          """.format(self.test_windows, current_windows_set))
 
 
-class TestSessionStoreDisabled(TestBaseRestoreWindows):
+class TestSessionStoreEnabledNoPrivateWindows(TestSessionStoreEnabledAllWindows):
+
+    def setUp(self):
+        super(TestSessionStoreEnabledNoPrivateWindows, self).setUp(include_private=False)
+
+
+class TestSessionStoreDisabled(SessionStoreTestCase):
+
     def test_no_restore_with_quit(self):
         current_windows_set = self.convert_open_windows_to_set()
         self.assertEqual(current_windows_set, self.all_windows,
                          msg='Not all requested windows have been opened. Expected {}, got {}.'
                          .format(self.all_windows, current_windows_set))
 
         self.marionette.quit(in_app=True)
         self.marionette.start_session()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_windows_shutdown.py
@@ -0,0 +1,45 @@
+# 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/.
+
+# add this directory to the path
+import sys
+import os
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
+
+
+class TestSessionStoreWindowsShutdown(SessionStoreTestCase):
+
+    def setUp(self):
+        super(TestSessionStoreWindowsShutdown, self).setUp(startup_page=3, no_auto_updates=False)
+
+    def test_with_variety(self):
+        """ Test restoring a set of windows across Windows shutdown.
+
+        Opens a set of windows, both standard and private, with
+        some number of tabs in them. Once the tabs have loaded, shuts down
+        the browser with the Windows Restart Manager, restarts the browser,
+        and then ensures that the standard tabs have been restored,
+        and that the private ones have not.
+
+        This specifically exercises the Windows synchronous shutdown
+        mechanism, which terminates the process in response to the
+        Restart Manager's WM_ENDSESSION message.
+        """
+
+        current_windows_set = self.convert_open_windows_to_set()
+        self.assertEqual(current_windows_set, self.all_windows,
+                         msg='Not all requested windows have been opened. Expected {}, got {}.'
+                         .format(self.all_windows, current_windows_set))
+
+        self.marionette.quit(in_app=True, callback=lambda: self.simulate_os_shutdown())
+        self.marionette.start_session()
+        self.marionette.set_context('chrome')
+
+        current_windows_set = self.convert_open_windows_to_set()
+        self.assertEqual(current_windows_set, self.test_windows,
+                         msg="""Non private browsing windows should have
+                         been restored. Expected {}, got {}.
+                         """.format(self.test_windows, current_windows_set))
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -2299,16 +2299,24 @@ ShowProfileManager(nsIToolkitProfileServ
   SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD);
   SaveWordToEnv("XRE_PROFILE_NAME", profileName);
 
   bool offline = false;
   aProfileSvc->GetStartOffline(&offline);
   if (offline) {
     SaveToEnv("XRE_START_OFFLINE=1");
   }
+  if (gRestartedByOS) {
+    // Re-add this argument when actually starting the application.
+    char** newArgv = (char**) realloc(gRestartArgv, sizeof(char*) * (gRestartArgc + 2));
+    NS_ENSURE_TRUE(newArgv, NS_ERROR_OUT_OF_MEMORY);
+    gRestartArgv = newArgv;
+    gRestartArgv[gRestartArgc++] = const_cast<char*>("-os-restarted");
+    gRestartArgv[gRestartArgc] = nullptr;
+  }
 
   return LaunchChild(aNative);
 }
 
 /**
  * Get the currently running profile using its root directory.
  *
  * @param aProfileSvc         The profile service
--- a/toolkit/xre/nsAppRunner.h
+++ b/toolkit/xre/nsAppRunner.h
@@ -45,16 +45,17 @@ extern nsXREDirProvider* gDirServiceProv
 // NOTE: gAppData will be null in embedded contexts.
 extern const mozilla::XREAppData* gAppData;
 extern bool gSafeMode;
 
 extern int    gArgc;
 extern char **gArgv;
 extern int    gRestartArgc;
 extern char **gRestartArgv;
+extern bool gRestartedByOS;
 extern bool gLogConsoleErrors;
 extern nsString gAbsoluteArgv0Path;
 
 extern bool gIsGtest;
 
 /**
  * Create the nativeappsupport implementation.
  *
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -601,36 +601,42 @@ ApplyUpdate(nsIFile *greDir, nsIFile *up
   } else {
     // Signal the updater application that it should stage the update.
     pid.AssignASCII("-1");
   }
 
   int argc = 5;
   if (restart) {
     argc = appArgc + 6;
+    if (gRestartedByOS) {
+      argc += 1;
+    }
   }
   char **argv = new char*[argc + 1];
   if (!argv) {
     return;
   }
   argv[0] = (char*) updaterPath.get();
   argv[1] = (char*) updateDirPath.get();
   argv[2] = (char*) installDirPath.get();
   argv[3] = (char*) applyToDirPath.get();
   argv[4] = (char*) pid.get();
   if (restart && appArgc) {
     argv[5] = workingDirPath;
     argv[6] = (char*) appFilePath.get();
     for (int i = 1; i < appArgc; ++i) {
       argv[6 + i] = appArgv[i];
     }
-    argv[argc] = nullptr;
-  } else {
-    argv[5] = nullptr;
+    if (gRestartedByOS) {
+      // We haven't truly started up, restore this argument so that we will have
+      // it upon restart.
+      argv[6 + appArgc] = const_cast<char*>("-os-restarted");
+    }
   }
+  argv[argc] = nullptr;
 
   if (restart && gSafeMode) {
     PR_SetEnv("MOZ_SAFE_MODE_RESTART=1");
   }
 
 #if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX)
   AppendToLibPath(installDirPath.get());
 #endif
--- a/widget/windows/ScreenHelperWin.cpp
+++ b/widget/windows/ScreenHelperWin.cpp
@@ -11,48 +11,57 @@
 #include "WinUtils.h"
 
 static mozilla::LazyLogModule sScreenLog("WidgetScreen");
 
 namespace mozilla {
 namespace widget {
 
 BOOL CALLBACK
-CollectMonitors(HMONITOR aMon, HDC hDCScreen, LPRECT, LPARAM ioParam)
+CollectMonitors(HMONITOR aMon, HDC, LPRECT, LPARAM ioParam)
 {
   auto screens = reinterpret_cast<nsTArray<RefPtr<Screen>>*>(ioParam);
   BOOL success = FALSE;
-  MONITORINFO info;
-  info.cbSize = sizeof(MONITORINFO);
+  MONITORINFOEX info;
+  info.cbSize = sizeof(MONITORINFOEX);
   success = ::GetMonitorInfoW(aMon, &info);
   if (!success) {
     MOZ_LOG(sScreenLog, LogLevel::Error, ("GetMonitorInfoW failed"));
     return TRUE; // continue the enumeration
   }
+
   double scale = WinUtils::LogToPhysFactor(aMon);
   DesktopToLayoutDeviceScale contentsScaleFactor;
   if (WinUtils::IsPerMonitorDPIAware()) {
     contentsScaleFactor.scale = 1.0;
   } else {
     contentsScaleFactor.scale = scale;
   }
   CSSToLayoutDeviceScale defaultCssScaleFactor(scale);
   LayoutDeviceIntRect rect(info.rcMonitor.left, info.rcMonitor.top,
                            info.rcMonitor.right - info.rcMonitor.left,
                            info.rcMonitor.bottom - info.rcMonitor.top);
   LayoutDeviceIntRect availRect(info.rcWork.left, info.rcWork.top,
                                 info.rcWork.right - info.rcWork.left,
                                 info.rcWork.bottom - info.rcWork.top);
-  uint32_t pixelDepth = ::GetDeviceCaps(hDCScreen, BITSPIXEL);
+
+  HDC hDC = CreateDC(nullptr, info.szDevice, nullptr, nullptr);
+  if (!hDC) {
+    MOZ_LOG(sScreenLog, LogLevel::Error, ("CollectMonitors CreateDC failed"));
+    return TRUE;
+  }
+  uint32_t pixelDepth = ::GetDeviceCaps(hDC, BITSPIXEL);
+  DeleteDC(hDC);
   if (pixelDepth == 32) {
     // If a device uses 32 bits per pixel, it's still only using 8 bits
     // per color component, which is what our callers want to know.
     // (Some devices report 32 and some devices report 24.)
     pixelDepth = 24;
   }
+
   float dpi = WinUtils::MonitorDPI(aMon);
   MOZ_LOG(sScreenLog, LogLevel::Debug,
            ("New screen [%d %d %d %d (%d %d %d %d) %d %f %f %f]",
             rect.X(), rect.Y(), rect.Width(), rect.Height(),
             availRect.X(), availRect.Y(), availRect.Width(), availRect.Height(),
             pixelDepth, contentsScaleFactor.scale, defaultCssScaleFactor.scale,
             dpi));
   auto screen = new Screen(rect, availRect,
@@ -69,22 +78,19 @@ CollectMonitors(HMONITOR aMon, HDC hDCSc
 }
 
 void
 ScreenHelperWin::RefreshScreens()
 {
   MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing screens"));
 
   AutoTArray<RefPtr<Screen>, 4> screens;
-  HDC hdc = ::CreateDC(L"DISPLAY", nullptr, nullptr, nullptr);
-  NS_ASSERTION(hdc,"CreateDC Failure");
-  BOOL result = ::EnumDisplayMonitors(hdc, nullptr,
+  BOOL result = ::EnumDisplayMonitors(nullptr, nullptr,
                                       (MONITORENUMPROC)CollectMonitors,
                                       (LPARAM)&screens);
-  ::DeleteDC(hdc);
   if (!result) {
     NS_WARNING("Unable to EnumDisplayMonitors");
   }
   ScreenManager& screenManager = ScreenManager::GetSingleton();
   screenManager.Refresh(Move(screens));
 }
 
 } // namespace widget
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -65,16 +65,17 @@
 #include "mozilla/MouseEvents.h"
 #include "mozilla/TouchEvents.h"
 
 #include "mozilla/ipc/MessageChannel.h"
 #include <algorithm>
 #include <limits>
 
 #include "nsWindow.h"
+#include "nsAppRunner.h"
 
 #include <shellapi.h>
 #include <windows.h>
 #include <wtsapi32.h>
 #include <process.h>
 #include <commctrl.h>
 #include <unknwn.h>
 #include <psapi.h>
@@ -5025,16 +5026,30 @@ LRESULT CALLBACK nsWindow::WindowProcInt
   }
 
   LRESULT res = ::CallWindowProcW(targetWindow->GetPrevWindowProc(),
                                   hWnd, msg, wParam, lParam);
 
   return res;
 }
 
+const char16_t*
+GetQuitType()
+{
+  if (Preferences::GetBool(PREF_WIN_REGISTER_APPLICATION_RESTART, false)) {
+    DWORD cchCmdLine = 0;
+    HRESULT rc =
+      ::GetApplicationRestartSettings(::GetCurrentProcess(), nullptr, &cchCmdLine, nullptr);
+    if (rc == S_OK) {
+      return u"os-restart";
+    }
+  }
+  return nullptr;
+}
+
 // The main windows message processing method for plugins.
 // The result means whether this method processed the native
 // event for plugin. If false, the native event should be
 // processed by the caller self.
 bool
 nsWindow::ProcessMessageForPlugin(const MSG &aMsg,
                                   MSGResult& aResult)
 {
@@ -5197,17 +5212,19 @@ nsWindow::ProcessMessage(UINT msg, WPARA
       {
         // Ask if it's ok to quit, and store the answer until we
         // get WM_ENDSESSION signaling the round is complete.
         nsCOMPtr<nsIObserverService> obsServ =
           mozilla::services::GetObserverService();
         nsCOMPtr<nsISupportsPRBool> cancelQuit =
           do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID);
         cancelQuit->SetData(false);
-        obsServ->NotifyObservers(cancelQuit, "quit-application-requested", nullptr);
+
+        const char16_t* quitType = GetQuitType();
+        obsServ->NotifyObservers(cancelQuit, "quit-application-requested", quitType);
 
         bool abortQuit;
         cancelQuit->GetData(&abortQuit);
         sCanQuit = abortQuit ? TRI_FALSE : TRI_TRUE;
       }
       *aRetValue = sCanQuit ? TRUE : FALSE;
       result = true;
       break;
@@ -5228,19 +5245,21 @@ nsWindow::ProcessMessage(UINT msg, WPARA
         // Let's fake a shutdown sequence without actually closing windows etc.
         // to avoid Windows killing us in the middle. A proper shutdown would
         // require having a chance to pump some messages. Unfortunately
         // Windows won't let us do that. Bug 212316.
         nsCOMPtr<nsIObserverService> obsServ =
           mozilla::services::GetObserverService();
         const char16_t* context = u"shutdown-persist";
         const char16_t* syncShutdown = u"syncShutdown";
+        const char16_t* quitType = GetQuitType();
+
         obsServ->NotifyObservers(nullptr, "quit-application-granted", syncShutdown);
         obsServ->NotifyObservers(nullptr, "quit-application-forced", nullptr);
-        obsServ->NotifyObservers(nullptr, "quit-application", nullptr);
+        obsServ->NotifyObservers(nullptr, "quit-application", quitType);
         obsServ->NotifyObservers(nullptr, "profile-change-net-teardown", context);
         obsServ->NotifyObservers(nullptr, "profile-change-teardown", context);
         obsServ->NotifyObservers(nullptr, "profile-before-change", context);
         obsServ->NotifyObservers(nullptr, "profile-before-change-qm", context);
         obsServ->NotifyObservers(nullptr, "profile-before-change-telemetry", context);
         ExitThisProcessSafely();
       }
       sCanQuit = TRI_UNKNOWN;