tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

session_store_test_case.py (17970B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 from urllib.parse import quote
      6 
      7 from marionette_driver import Wait, errors
      8 from marionette_driver.keys import Keys
      9 from marionette_harness import MarionetteTestCase, WindowManagerMixin
     10 
     11 
     12 def inline(doc):
     13    return f"data:text/html;charset=utf-8,{quote(doc)}"
     14 
     15 
     16 # Each list element represents a window of tabs loaded at
     17 # some testing URL
     18 DEFAULT_WINDOWS = set([
     19    # Window 1. Note the comma after the inline call -
     20    # this is Python's way of declaring a 1 item tuple.
     21    (inline("""<div">Lorem</div>"""),),
     22    # Window 2
     23    (
     24        inline("""<div">ipsum</div>"""),
     25        inline("""<div">dolor</div>"""),
     26    ),
     27    # Window 3
     28    (
     29        inline("""<div">sit</div>"""),
     30        inline("""<div">amet</div>"""),
     31    ),
     32 ])
     33 
     34 
     35 class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase):
     36    def setUp(
     37        self,
     38        startup_page=1,
     39        include_private=True,
     40        restore_on_demand=False,
     41        no_auto_updates=True,
     42        win_register_restart=False,
     43        test_windows=DEFAULT_WINDOWS,
     44        taskbartabs_enable=False,
     45    ):
     46        super().setUp()
     47        self.marionette.set_context("chrome")
     48 
     49        platform = self.marionette.session_capabilities["platformName"]
     50        self.accelKey = Keys.META if platform == "mac" else Keys.CONTROL
     51 
     52        self.test_windows = test_windows
     53 
     54        self.private_windows = set([
     55            (
     56                inline("""<div">consectetur</div>"""),
     57                inline("""<div">ipsum</div>"""),
     58            ),
     59            (
     60                inline("""<div">adipiscing</div>"""),
     61                inline("""<div">consectetur</div>"""),
     62            ),
     63        ])
     64 
     65        self.marionette.enforce_gecko_prefs({
     66            # Set browser restore previous session pref,
     67            # depending on what the test requires.
     68            "browser.startup.page": startup_page,
     69            # Make the content load right away instead of waiting for
     70            # the user to click on the background tabs
     71            "browser.sessionstore.restore_on_demand": restore_on_demand,
     72            # Avoid race conditions by having the content process never
     73            # send us session updates unless the parent has explicitly asked
     74            # for them via the TabStateFlusher.
     75            "browser.sessionstore.debug.no_auto_updates": no_auto_updates,
     76            # Whether to enable the register application restart mechanism.
     77            "toolkit.winRegisterApplicationRestart": win_register_restart,
     78            # Whether to enable taskbar tabs for this test
     79            "browser.taskbarTabs.enabled": taskbartabs_enable,
     80        })
     81 
     82        self.all_windows = self.test_windows.copy()
     83        self.open_windows(self.test_windows)
     84 
     85        if include_private:
     86            self.all_windows.update(self.private_windows)
     87            self.open_windows(self.private_windows, is_private=True)
     88 
     89    def tearDown(self):
     90        try:
     91            # Create a fresh profile for subsequent tests.
     92            self.marionette.restart(in_app=False, clean=True)
     93        finally:
     94            super().tearDown()
     95 
     96    def open_windows(self, window_sets, is_private=False):
     97        """Open a set of windows with tabs pointing at some URLs.
     98 
     99        @param window_sets (list)
    100               A set of URL tuples. Each tuple within window_sets
    101               represents a window, and each URL in the URL
    102               tuples represents what will be loaded in a tab.
    103 
    104               Note that if is_private is False, then the first
    105               URL tuple will be opened in the current window, and
    106               subequent tuples will be opened in new windows.
    107 
    108               Example:
    109 
    110               set(
    111                   (self.marionette.absolute_url('layout/mozilla_1.html'),
    112                    self.marionette.absolute_url('layout/mozilla_2.html')),
    113 
    114                   (self.marionette.absolute_url('layout/mozilla_3.html'),
    115                    self.marionette.absolute_url('layout/mozilla_4.html')),
    116               )
    117 
    118               This would take the currently open window, and load
    119               mozilla_1.html and mozilla_2.html in new tabs. It would
    120               then open a new, second window, and load tabs at
    121               mozilla_3.html and mozilla_4.html.
    122        @param is_private (boolean, optional)
    123               Whether or not any new windows should be a private browsing
    124               windows.
    125        """
    126        if is_private:
    127            win = self.open_window(private=True)
    128            self.marionette.switch_to_window(win)
    129        else:
    130            win = self.marionette.current_chrome_window_handle
    131 
    132        for index, urls in enumerate(window_sets):
    133            if index > 0:
    134                win = self.open_window(private=is_private)
    135                self.marionette.switch_to_window(win)
    136            self.open_tabs(win, urls)
    137 
    138    # Open a Firefox web app (taskbar tab) window
    139    def open_taskbartab_window(self):
    140        self.marionette.execute_async_script(
    141            """
    142            let [resolve] = arguments;
    143            (async () => {
    144                    let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
    145                        Ci.nsIWritablePropertyBag2
    146                    );
    147                    extraOptions.setPropertyAsBool("taskbartab", true);
    148 
    149                    let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
    150                    args.appendElement(null);
    151                    args.appendElement(extraOptions);
    152                    args.appendElement(null);
    153 
    154                    // Simulate opening a taskbar tab window
    155                    let win = Services.ww.openWindow(
    156                        null,
    157                        AppConstants.BROWSER_CHROME_URL,
    158                        "_blank",
    159                        "chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars",
    160                        args
    161                    );
    162                    await new Promise(resolve => {
    163                        win.addEventListener("load", resolve, { once: true });
    164                    });
    165                    await win.delayedStartupPromise;
    166            })().then(resolve);
    167        """
    168        )
    169 
    170    # Helper function for taskbar tabs tests, opens a taskbar tab window,
    171    # closes the regular window, and reopens another regular window.
    172    # Firefox will then be in a "ready to restore" state
    173    def setup_taskbartab_restore_scenario(self):
    174        self.open_taskbartab_window()
    175        taskbar_tab_window_handle = self.marionette.close_chrome_window()[0]
    176        self.marionette.switch_to_window(taskbar_tab_window_handle)
    177        self.marionette.open(type="window")
    178 
    179    def open_tabs(self, win, urls):
    180        """Open a set of URLs inside a window in new tabs.
    181 
    182        @param win (browser window)
    183               The browser window to load the tabs in.
    184        @param urls (tuple)
    185               A tuple of URLs to load in this window. The
    186               first URL will be loaded in the currently selected
    187               browser tab. Subsequent URLs will be loaded in
    188               new tabs.
    189        """
    190        # If there are any remaining URLs for this window,
    191        # open some new tabs and navigate to them.
    192        with self.marionette.using_context("content"):
    193            if isinstance(urls, str):
    194                self.marionette.navigate(urls)
    195            else:
    196                for index, url in enumerate(urls):
    197                    if index > 0:
    198                        tab = self.open_tab()
    199                        self.marionette.switch_to_window(tab)
    200                    self.marionette.navigate(url)
    201 
    202    def wait_for_windows(self, expected_windows, message, timeout=5):
    203        current_windows = None
    204 
    205        def check(_):
    206            nonlocal current_windows
    207            current_windows = self.convert_open_windows_to_set()
    208            return current_windows == expected_windows
    209 
    210        try:
    211            wait = Wait(self.marionette, timeout=timeout, interval=0.1)
    212            wait.until(check, message=message)
    213        except errors.TimeoutException as e:
    214            # Update the message to include the most recent list of windows
    215            message = (
    216                f"{e.message}. Expected {expected_windows}, got {current_windows}."
    217            )
    218            raise errors.TimeoutException(message)
    219 
    220    def get_urls_for_window(self, win):
    221        orig_handle = self.marionette.current_chrome_window_handle
    222 
    223        try:
    224            with self.marionette.using_context("chrome"):
    225                self.marionette.switch_to_window(win)
    226                return self.marionette.execute_script(
    227                    """
    228                  return gBrowser.tabs.map(tab => {
    229                    return tab.linkedBrowser.currentURI.spec;
    230                  });
    231                """
    232                )
    233        finally:
    234            self.marionette.switch_to_window(orig_handle)
    235 
    236    def convert_open_windows_to_set(self):
    237        # There's no guarantee that Marionette will return us an
    238        # iterator for the opened windows that will match the
    239        # order within our window list. Instead, we'll convert
    240        # the list of URLs within each open window to a set of
    241        # tuples that will allow us to do a direct comparison
    242        # while allowing the windows to be in any order.
    243        opened_windows = set()
    244        for win in self.marionette.chrome_window_handles:
    245            urls = tuple(self.get_urls_for_window(win))
    246            opened_windows.add(urls)
    247 
    248        return opened_windows
    249 
    250    def _close_window(self):
    251        """Use as a callback to `marionette.quit` in order to close the
    252        browser window.
    253 
    254        `marionette.close`/`marionette.close_chrome_window` cannot
    255        be used alone because they don't allow closing the last window.
    256        """
    257 
    258        self.marionette.execute_script("window.close()")
    259 
    260    def close_all_tabs_and_restart(self):
    261        self.close_all_tabs()
    262        self.marionette.quit(callback=self._close_window)
    263        self.marionette.start_session()
    264 
    265    def simulate_os_shutdown(self):
    266        """Simulate an OS shutdown.
    267 
    268        :raises: Exception: if not supported on the current platform
    269        :raises: WindowsError: if a Windows API call failed
    270        """
    271        if self.marionette.session_capabilities["platformName"] != "windows":
    272            raise Exception("Unsupported platform for simulate_os_shutdown")
    273 
    274        self._shutdown_with_windows_restart_manager(self.marionette.process_id)
    275 
    276    def _shutdown_with_windows_restart_manager(self, pid):
    277        """Shut down a process using the Windows Restart Manager.
    278 
    279        When Windows shuts down, it uses a protocol including the
    280        WM_QUERYENDSESSION and WM_ENDSESSION messages to give
    281        applications a chance to shut down safely. The best way to
    282        simulate this is via the Restart Manager, which allows a process
    283        (such as an installer) to use the same mechanism to shut down
    284        any other processes which are using registered resources.
    285 
    286        This function starts a Restart Manager session, registers the
    287        process as a resource, and shuts down the process.
    288 
    289        :param pid: The process id (int) of the process to shutdown
    290 
    291        :raises: WindowsError: if a Windows API call fails
    292        """
    293        import ctypes
    294        from ctypes import POINTER, WINFUNCTYPE, Structure, WinError, pointer, windll
    295        from ctypes.wintypes import BOOL, DWORD, HANDLE, LPCWSTR, UINT, ULONG, WCHAR
    296 
    297        # set up Windows SDK types
    298        OpenProcess = windll.kernel32.OpenProcess
    299        OpenProcess.restype = HANDLE
    300        OpenProcess.argtypes = [
    301            DWORD,  # dwDesiredAccess
    302            BOOL,  # bInheritHandle
    303            DWORD,
    304        ]  # dwProcessId
    305        PROCESS_QUERY_INFORMATION = 0x0400
    306 
    307        class FILETIME(Structure):
    308            _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)]
    309 
    310        LPFILETIME = POINTER(FILETIME)
    311 
    312        GetProcessTimes = windll.kernel32.GetProcessTimes
    313        GetProcessTimes.restype = BOOL
    314        GetProcessTimes.argtypes = [
    315            HANDLE,  # hProcess
    316            LPFILETIME,  # lpCreationTime
    317            LPFILETIME,  # lpExitTime
    318            LPFILETIME,  # lpKernelTime
    319            LPFILETIME,
    320        ]  # lpUserTime
    321 
    322        ERROR_SUCCESS = 0
    323 
    324        class RM_UNIQUE_PROCESS(Structure):
    325            _fields_ = [("dwProcessId", DWORD), ("ProcessStartTime", FILETIME)]
    326 
    327        RmStartSession = windll.rstrtmgr.RmStartSession
    328        RmStartSession.restype = DWORD
    329        RmStartSession.argtypes = [
    330            POINTER(DWORD),  # pSessionHandle
    331            DWORD,  # dwSessionFlags
    332            POINTER(WCHAR),
    333        ]  # strSessionKey
    334 
    335        class GUID(ctypes.Structure):
    336            _fields_ = [
    337                ("Data1", ctypes.c_ulong),
    338                ("Data2", ctypes.c_ushort),
    339                ("Data3", ctypes.c_ushort),
    340                ("Data4", ctypes.c_ubyte * 8),
    341            ]
    342 
    343        CCH_RM_SESSION_KEY = ctypes.sizeof(GUID) * 2
    344 
    345        RmRegisterResources = windll.rstrtmgr.RmRegisterResources
    346        RmRegisterResources.restype = DWORD
    347        RmRegisterResources.argtypes = [
    348            DWORD,  # dwSessionHandle
    349            UINT,  # nFiles
    350            POINTER(LPCWSTR),  # rgsFilenames
    351            UINT,  # nApplications
    352            POINTER(RM_UNIQUE_PROCESS),  # rgApplications
    353            UINT,  # nServices
    354            POINTER(LPCWSTR),
    355        ]  # rgsServiceNames
    356 
    357        RM_WRITE_STATUS_CALLBACK = WINFUNCTYPE(None, UINT)
    358        RmShutdown = windll.rstrtmgr.RmShutdown
    359        RmShutdown.restype = DWORD
    360        RmShutdown.argtypes = [
    361            DWORD,  # dwSessionHandle
    362            ULONG,  # lActionFlags
    363            RM_WRITE_STATUS_CALLBACK,
    364        ]  # fnStatus
    365 
    366        RmEndSession = windll.rstrtmgr.RmEndSession
    367        RmEndSession.restype = DWORD
    368        RmEndSession.argtypes = [DWORD]  # dwSessionHandle
    369 
    370        # Get the info needed to uniquely identify the process
    371        hProc = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)
    372        if not hProc:
    373            raise WinError()
    374 
    375        creationTime = FILETIME()
    376        exitTime = FILETIME()
    377        kernelTime = FILETIME()
    378        userTime = FILETIME()
    379        if not GetProcessTimes(
    380            hProc,
    381            pointer(creationTime),
    382            pointer(exitTime),
    383            pointer(kernelTime),
    384            pointer(userTime),
    385        ):
    386            raise WinError()
    387 
    388        # Start the Restart Manager Session
    389        dwSessionHandle = DWORD()
    390        sessionKeyType = WCHAR * (CCH_RM_SESSION_KEY + 1)
    391        sessionKey = sessionKeyType()
    392        if RmStartSession(pointer(dwSessionHandle), 0, sessionKey) != ERROR_SUCCESS:
    393            raise WinError()
    394 
    395        try:
    396            UProcs_count = 1
    397            UProcsArrayType = RM_UNIQUE_PROCESS * UProcs_count
    398            UProcs = UProcsArrayType(RM_UNIQUE_PROCESS(pid, creationTime))
    399 
    400            # Register the process as a resource
    401            if (
    402                RmRegisterResources(
    403                    dwSessionHandle, 0, None, UProcs_count, UProcs, 0, None
    404                )
    405                != ERROR_SUCCESS
    406            ):
    407                raise WinError()
    408 
    409            # Shut down all processes using registered resources
    410            if (
    411                RmShutdown(
    412                    dwSessionHandle, 0, ctypes.cast(None, RM_WRITE_STATUS_CALLBACK)
    413                )
    414                != ERROR_SUCCESS
    415            ):
    416                raise WinError()
    417 
    418        finally:
    419            RmEndSession(dwSessionHandle)
    420 
    421    def windows_shutdown_with_variety(self, restart_by_os, expect_restore):
    422        """Test restoring windows after Windows shutdown.
    423 
    424        Opens a set of windows, both standard and private, with
    425        some number of tabs in them. Once the tabs have loaded, shuts down
    426        the browser with the Windows Restart Manager and restarts the browser.
    427 
    428        This specifically exercises the Windows synchronous shutdown mechanism,
    429        which terminates the process in response to the Restart Manager's
    430        WM_ENDSESSION message.
    431 
    432        If restart_by_os is True, the -os-restarted arg is passed when restarting,
    433        simulating being automatically restarted by the Restart Manager.
    434 
    435        If expect_restore is True, this ensures that the standard tabs have been
    436        restored, and that the private ones have not. Otherwise it ensures that
    437        no tabs and windows have been restored.
    438        """
    439        current_windows_set = self.convert_open_windows_to_set()
    440        self.assertEqual(
    441            current_windows_set,
    442            self.all_windows,
    443            msg=f"Not all requested windows have been opened. Expected {self.all_windows}, got {current_windows_set}.",
    444        )
    445 
    446        self.marionette.quit(callback=lambda: self.simulate_os_shutdown())
    447 
    448        saved_args = self.marionette.instance.app_args
    449        try:
    450            if restart_by_os:
    451                self.marionette.instance.app_args = ["-os-restarted"]
    452 
    453            self.marionette.start_session()
    454            self.marionette.set_context("chrome")
    455        finally:
    456            self.marionette.instance.app_args = saved_args
    457 
    458        if expect_restore:
    459            self.wait_for_windows(
    460                self.test_windows,
    461                "Non private browsing windows should have been restored",
    462            )
    463        else:
    464            self.assertEqual(
    465                len(self.marionette.chrome_window_handles),
    466                1,
    467                msg="Windows from last session shouldn`t have been restored.",
    468            )
    469            self.assertEqual(
    470                len(self.marionette.window_handles),
    471                1,
    472                msg="Tabs from last session shouldn`t have been restored.",
    473            )