diff --git a/tbselenium/tbbinary.py b/tbselenium/tbbinary.py index 8f98822..0d4890c 100644 --- a/tbselenium/tbbinary.py +++ b/tbselenium/tbbinary.py @@ -1,16 +1,86 @@ -from selenium.webdriver.firefox.firefox_binary import FirefoxBinary +""" +TBBinary — lightweight wrapper around the Tor Browser (Firefox) process. +In Selenium < 4.40 this module subclassed ``FirefoxBinary``. That class was +deprecated in Selenium 4.x and fully removed in 4.40.0 (see +https://github.com/SeleniumHQ/selenium/blob/trunk/py/CHANGES). -class TBBinary(FirefoxBinary): - ''' - Extend FirefoxBinary to better handle terminated browser processes. - ''' +The modern Selenium API no longer requires a ``FirefoxBinary`` object: +``Options.binary`` accepts a plain filesystem path (str / ``pathlib.Path``), +and the ``Service`` object owns the geckodriver life-cycle. ``TBBinary`` is +therefore re-implemented as a standalone helper that only manages the *browser* +process itself — primarily to provide the ``kill()`` method that some callers +rely on when the browser becomes unresponsive. +""" - def kill(self): - """Kill the browser. +from __future__ import annotations - This is useful when the browser is stuck. +import subprocess +from typing import IO, Optional + + +class TBBinary: + """Manage the Tor Browser (Firefox) process independently of Selenium. + + This class replaces the former ``FirefoxBinary``-based implementation and + is compatible with Selenium >= 4.40.0. + + Parameters + ---------- + firefox_path: + Absolute path to the ``firefox`` binary inside the Tor Browser bundle. + log_file: + Optional writable file-like object that receives the browser's stdout + and stderr output. Pass ``None`` (the default) to discard output. + """ + + def __init__( + self, + firefox_path: str, + log_file: Optional[IO[str]] = None, + ) -> None: + self.firefox_path = firefox_path + self.log_file = log_file + self.process: Optional[subprocess.Popen] = None # type: ignore[type-arg] + + # ------------------------------------------------------------------ + # Process control + # ------------------------------------------------------------------ + + def launch_browser(self, profile: str, timeout: int = 30) -> None: + """Start the Tor Browser process with a given Firefox profile. + + Parameters + ---------- + profile: + Path to the Firefox profile directory that should be used. + timeout: + Unused; kept for API compatibility with callers that previously + passed a timeout to the legacy ``FirefoxBinary.launch_browser()``. + """ + sink = self.log_file or subprocess.DEVNULL + self.process = subprocess.Popen( + [self.firefox_path, "--profile", profile], + stdout=sink, + stderr=sink, + ) + + def kill(self) -> None: + """Forcefully terminate the browser process. + + Safe to call even if the process has already exited or was never + started — in those cases the method is a no-op. """ - if self.process and self.process.poll() is None: + if self.process is not None and self.process.poll() is None: self.process.kill() self.process.wait() + + # ------------------------------------------------------------------ + # Context-manager support + # ------------------------------------------------------------------ + + def __enter__(self) -> "TBBinary": + return self + + def __exit__(self, *args: object) -> None: + self.kill() diff --git a/tbselenium/tbdriver.py b/tbselenium/tbdriver.py index 0b33979..ccde75f 100644 --- a/tbselenium/tbdriver.py +++ b/tbselenium/tbdriver.py @@ -90,10 +90,9 @@ def __init__(self, log_path=tbb_logfile_path, port=geckodriver_port ) - # options.binary is path to the Firefox binary and it can be a string - # or a FirefoxBinary object. If it's a string, it will be converted to - # a FirefoxBinary object. - # https://github.com/SeleniumHQ/selenium/blob/7cfd137085fcde932cd71af78642a15fd56fe1f1/py/selenium/webdriver/firefox/options.py#L54 + # options.binary accepts a filesystem path (str) to the Firefox binary. + # FirefoxBinary was removed in Selenium 4.40.0; a plain string path is + # the correct way to configure the binary in modern Selenium. self.options.binary = self.tbb_fx_binary_path self.options.add_argument('--class') self.options.add_argument('"Tor Browser"') @@ -309,7 +308,12 @@ def export_env_vars(self): prepend_to_env_var("PATH", self.tbb_browser_dir) def get_tb_binary(self, logfile=None): - """Return FirefoxBinary pointing to the TBB's firefox binary.""" + """Return a TBBinary instance pointing to the Tor Browser's Firefox binary. + + Note: this method is not called during normal driver initialisation. + It is provided as a convenience for callers that need to manage the + browser process directly (e.g. to forcefully kill a stuck browser). + """ tbb_logfile = open(logfile, 'a+') if logfile else None return TBBinary(firefox_path=self.tbb_fx_binary_path, log_file=tbb_logfile)