From 8291f7ccc15e81db0e2b8ed3912750e7c29b3a59 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:54:59 +0800 Subject: [PATCH 1/3] fix(web_browser): fall back to scale factor 1 on macOS when AppKit is unavailable `get_screen_scale_factor()` guards the Windows and Linux branches with try/except and defaults to 1.0, but the macOS branch imported AppKit and dereferenced `NSScreen.mainScreen()` unconditionally. Two reachable cases crashed instead of degrading: - `pyobjc-framework-AppKit` is an optional dependency ("required to run headfully on macOS"), so a headful run without it raised ImportError. - `NSScreen.mainScreen()` returns None with no display attached (headless / over SSH), so `.backingScaleFactor()` raised AttributeError. Wrap the import and null-guard the screen so macOS degrades to 1.0 like the other platforms. Adds unit tests for the working, headless, and missing-AppKit paths. --- CHANGELOG.md | 1 + .../_web_browser/scale_factor.py | 11 ++++-- .../_web_browser/test_scale_factor.py | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/test_scale_factor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bf9c469678..7255d473a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Analysis: `samples_df` gains default `turn_count` and `token_limit_usage` columns, and `evals_df` configuration columns gain `token_limit_type`. - Control Channel: `inspect ctl sample list` now shows per-sample turn count and, when a token limit is configured, its computed usage and configured ceiling. - Bugfix: Crash recovery now reconstructs `sample.messages` from the first model role's conversation when a solver runs multiple agents concurrently, instead of returning whichever agent's model call happened to fire last. (#4414) +- Web Browser: headful macOS scale-factor detection falls back to 1 instead of crashing when `pyobjc-framework-AppKit` is missing or no display is attached. ## 0.3.246 (10 July 2026) diff --git a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py index 339c5647c4..e5196d5cfc 100644 --- a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py +++ b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py @@ -21,10 +21,15 @@ def get_screen_scale_factor() -> float: if sys.platform == "darwin": - # `pip install pyobjc-framework-AppKit` is required to run headfully on macOS - from AppKit import NSScreen # type: ignore # noqa: PLC0415 + try: + # `pip install pyobjc-framework-AppKit` is required to run headfully on macOS + from AppKit import NSScreen # type: ignore # noqa: PLC0415 - return NSScreen.mainScreen().backingScaleFactor() + screen = NSScreen.mainScreen() + except Exception: + return 1.0 + # mainScreen() is None when no display is attached (headless / over SSH) + return float(screen.backingScaleFactor()) if screen is not None else 1.0 elif sys.platform == "win32": try: # Using GetDpiForSystem from Windows API diff --git a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/test_scale_factor.py b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/test_scale_factor.py new file mode 100644 index 0000000000..fbcb17a214 --- /dev/null +++ b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/test_scale_factor.py @@ -0,0 +1,37 @@ +import sys +import types + +from scale_factor import get_screen_scale_factor + + +def _install_fake_appkit(monkeypatch, screen: object) -> None: + """Put a fake ``AppKit`` on ``sys.modules`` whose ``mainScreen()`` returns ``screen``.""" + module = types.ModuleType("AppKit") + module.NSScreen = types.SimpleNamespace(mainScreen=lambda: screen) # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "AppKit", module) + + +def test_darwin_returns_backing_scale_factor(monkeypatch): + monkeypatch.setattr(sys, "platform", "darwin") + _install_fake_appkit( + monkeypatch, types.SimpleNamespace(backingScaleFactor=lambda: 2.0) + ) + assert get_screen_scale_factor() == 2.0 + + +def test_darwin_headless_screen_falls_back_to_one(monkeypatch): + # NSScreen.mainScreen() is None when no display is attached (headless / over SSH); + # dereferencing it used to raise AttributeError instead of defaulting to 1.0. + monkeypatch.setattr(sys, "platform", "darwin") + _install_fake_appkit(monkeypatch, None) + assert get_screen_scale_factor() == 1.0 + + +def test_darwin_missing_appkit_falls_back_to_one(monkeypatch): + # pyobjc-framework-AppKit is an optional dependency; a missing import used to crash + # the probe instead of degrading like the Windows and Linux branches do. + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.setitem( + sys.modules, "AppKit", None + ) # `from AppKit import ...` -> ImportError + assert get_screen_scale_factor() == 1.0 From acd139cc75aa0778685618a78f9c470613e53208 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:11:47 +0800 Subject: [PATCH 2/3] fix(web_browser): warn once when AppKit is missing on the macOS scale-factor probe Per review: catch the missing-AppKit ModuleNotFoundError specifically and log a one-time warning (pointing at `pip install pyobjc-framework-AppKit`) before returning the default scale factor of 1, instead of degrading silently. The darwin probe is extracted into a helper and the warning is gated with functools.cache so it fires at most once per process. --- .../_web_browser/scale_factor.py | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py index e5196d5cfc..b1d324d5d7 100644 --- a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py +++ b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py @@ -1,6 +1,10 @@ +import functools +import logging import subprocess import sys +logger = logging.getLogger(__name__) + # Playwright launches Chromium with --force-device-scale-factor=1 by default, which # ensures consistent rendering and measurement behavior using CSS pixels instead of # native device pixels, regardless of the actual display's DPI. On HiDPI displays, @@ -19,17 +23,35 @@ # allows the code to accurately calculate node visibility. +@functools.cache +def _warn_missing_appkit() -> None: + # @cache makes this warn exactly once per process, however many headful macOS + # calls hit the missing optional AppKit dependency. + logger.warning( + "pyobjc-framework-AppKit is not installed; falling back to a screen " + "scale factor of 1. Install it (`pip install pyobjc-framework-AppKit`) " + "to enable HiDPI scale-factor detection when running headfully on macOS." + ) + + +def _darwin_scale_factor() -> float: + try: + # `pip install pyobjc-framework-AppKit` is required to run headfully on macOS + from AppKit import NSScreen # type: ignore # noqa: PLC0415 + + screen = NSScreen.mainScreen() + except ModuleNotFoundError: + _warn_missing_appkit() + return 1.0 + except Exception: + return 1.0 + # mainScreen() is None when no display is attached (headless / over SSH) + return float(screen.backingScaleFactor()) if screen is not None else 1.0 + + def get_screen_scale_factor() -> float: if sys.platform == "darwin": - try: - # `pip install pyobjc-framework-AppKit` is required to run headfully on macOS - from AppKit import NSScreen # type: ignore # noqa: PLC0415 - - screen = NSScreen.mainScreen() - except Exception: - return 1.0 - # mainScreen() is None when no display is attached (headless / over SSH) - return float(screen.backingScaleFactor()) if screen is not None else 1.0 + return _darwin_scale_factor() elif sys.platform == "win32": try: # Using GetDpiForSystem from Windows API From 461d1415d66e0d091b615a02062451d4d2130b96 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:02:50 +0800 Subject: [PATCH 3/3] refactor(web_browser): use a warn_once helper for the missing-AppKit warning --- .../_web_browser/scale_factor.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py index b1d324d5d7..a499a01ee5 100644 --- a/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py +++ b/src/inspect_tool_support/src/inspect_tool_support/_remote_tools/_web_browser/scale_factor.py @@ -1,10 +1,19 @@ -import functools import logging import subprocess import sys logger = logging.getLogger(__name__) + +def warn_once(logger: logging.Logger, message: str) -> None: + """Emit ``message`` as a warning, but only the first time it is seen this process.""" + if message not in _warned: + logger.warning(message) + _warned.add(message) + + +_warned: set[str] = set() + # Playwright launches Chromium with --force-device-scale-factor=1 by default, which # ensures consistent rendering and measurement behavior using CSS pixels instead of # native device pixels, regardless of the actual display's DPI. On HiDPI displays, @@ -23,17 +32,6 @@ # allows the code to accurately calculate node visibility. -@functools.cache -def _warn_missing_appkit() -> None: - # @cache makes this warn exactly once per process, however many headful macOS - # calls hit the missing optional AppKit dependency. - logger.warning( - "pyobjc-framework-AppKit is not installed; falling back to a screen " - "scale factor of 1. Install it (`pip install pyobjc-framework-AppKit`) " - "to enable HiDPI scale-factor detection when running headfully on macOS." - ) - - def _darwin_scale_factor() -> float: try: # `pip install pyobjc-framework-AppKit` is required to run headfully on macOS @@ -41,7 +39,12 @@ def _darwin_scale_factor() -> float: screen = NSScreen.mainScreen() except ModuleNotFoundError: - _warn_missing_appkit() + warn_once( + logger, + "pyobjc-framework-AppKit is not installed; falling back to a screen " + "scale factor of 1. Install it (`pip install pyobjc-framework-AppKit`) " + "to enable HiDPI scale-factor detection when running headfully on macOS.", + ) return 1.0 except Exception: return 1.0