• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import abc
8import json
9import logging
10import os
11import subprocess
12from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
13
14import psutil
15
16from crossbench import helper, plt
17from crossbench.browsers.browser import Browser
18from crossbench.env import HostEnvironment, ValidationError
19
20if TYPE_CHECKING:
21  import datetime as dt
22
23  from crossbench.path import AnyPath
24  from crossbench.runner.groups.session import BrowserSessionRunGroup
25
26
27class AppleScript:
28
29  @classmethod
30  def with_args(cls, app_path: AnyPath, apple_script: str,
31                **kwargs) -> Tuple[str, List[str]]:
32    variables = []
33    replacements = {}
34    args: List[str] = []
35    for variable, value in kwargs.items():
36      args.append(value)
37      unique_variable = f"cb_input_{variable}"
38      replacements[variable] = unique_variable
39      variables.append(f"set {unique_variable} to (item {len(args)} of argv)")
40    variables_str = "\n".join(variables)
41    formatted_script = apple_script.strip() % replacements
42    wrapper = f"""
43      {variables_str}
44      tell application "{app_path}"
45        {formatted_script}
46      end tell
47    """
48    return wrapper.strip(), args
49
50  @classmethod
51  def js_script_with_args(cls, script: str, args: Sequence[object]) -> str:
52    """Create a script that returns [JSON.stringify(result), true] on success,
53    and [exception.toString(), false] when failing."""
54    args_str: str = json.dumps(args)
55    script = """JSON.stringify((function exceptionWrapper(){
56        try {
57          return [(function(...arguments){%(script)s}).apply(window, %(args_str)s), true]
58        } catch(e) {
59          return [e + "", false]
60        }
61      })())""" % {
62        "script": script,
63        "args_str": args_str
64    }
65    return script.strip()
66
67  class JavaScriptFromAppleScriptException(ValueError):
68    pass
69
70
71def try_get_parent_app_name(platform: plt.Platform) -> str:
72  if platform.is_remote:
73    return ""
74  launched_apps: Dict[str, str] = {}
75  try:
76    for line in platform.sh_stdout("launchctl", "list").splitlines():
77      parts = line.split()
78      if len(parts) == 3:
79        pid, _, label = parts
80        # Input:  "application.com.google.Chrome.46262139.72133274"
81        # Output: "Chrome"
82        label_parts = label.split(".")
83        if len(label_parts) <= 3:
84          continue
85        launched_apps[pid] = label_parts[3]
86  except Exception as e:  # pylint: disable=broad-except
87    logging.debug("Could not list all parents: %s", e)
88    return ""
89  if not launched_apps:
90    logging.debug("Could not find any apps")
91    return ""
92  try:
93    for parent in psutil.Process(os.getpid()).parents():
94      if label := launched_apps.get(str(parent.pid), ""):
95        return label
96  except Exception as e:  # pylint: disable=broad-except
97    logging.debug("Could not find parent parent app process: %s", e)
98  return ""
99
100
101SYSTEM_EVENTS_CHECK = (
102    'tell application "System Events" to log (count of windows)')
103
104class AppleScriptBrowser(Browser, metaclass=abc.ABCMeta):
105  APPLE_SCRIPT_ALLOW_JS_MENU: str = ""
106  APPLE_SCRIPT_JS_COMMAND: str = ""
107  APPLE_SCRIPT_SET_URL: str = ""
108
109  _browser_process: subprocess.Popen
110
111  def _exec_apple_script(self, apple_script: str, **kwargs) -> Any:
112    assert self.platform.is_macos, (
113        f"Sorry, f{self.__class__} is only supported on MacOS for now")
114    wrapper_script, args = AppleScript.with_args(self.app_path, apple_script,
115                                                 **kwargs)
116    return self.platform.exec_apple_script(wrapper_script, *args)
117
118  def validate_env(self, env: HostEnvironment) -> None:
119    super().validate_env(env)
120    self._check_system_events_allowed(env)
121
122  def start(self, session: BrowserSessionRunGroup) -> None:
123    assert not self._is_running
124    # Start process directly
125    startup_flags = self._get_browser_flags_for_session(session)
126    self._log_browser_start(startup_flags)
127    self._browser_process = self.platform.popen(
128        self.path, *startup_flags, shell=False)
129    if self._browser_process.poll():
130      raise ValueError("Could not start browser process.")
131    self._pid = self._browser_process.pid
132    self.platform.sleep(3)
133    self._exec_apple_script("activate")
134    self._setup_window()
135    self._check_js_from_apple_script_allowed(session.env)
136
137  def _check_system_events_allowed(self, env: HostEnvironment) -> None:
138    try:
139      self._exec_apple_script(SYSTEM_EVENTS_CHECK)
140    except plt.SubprocessError as e:
141      logging.error("Not allowed to run AppleScript and send System Events!")
142      logging.debug("    SubprocessError: %s", e)
143      app_name = try_get_parent_app_name(self.platform) or "parent"
144      env.handle_warning(
145          f"Enable the 'System Events' permission for the {app_name} App. \n"
146          "  See 'System Settings' > 'Privacy & Security' > 'Automation'.\n")
147    try:
148      self._exec_apple_script(SYSTEM_EVENTS_CHECK)
149    except plt.SubprocessError as e:
150      raise ValidationError(
151          " Not allowed to run AppleScript and send System Events!") from e
152
153  def _check_js_from_apple_script_allowed(self, env: HostEnvironment) -> None:
154    try:
155      self.js("return 1")
156    except plt.SubprocessError as e:
157      logging.error("Browser does not allow JS from AppleScript!")
158      logging.debug("    SubprocessError: %s", e)
159      env.handle_warning("Enable JavaScript from Apple Script Events: "
160                         f"'{self.APPLE_SCRIPT_ALLOW_JS_MENU}'")
161    try:
162      self.js("return 1;")
163    except plt.SubprocessError as e:
164      raise ValidationError(
165          " JavaScript from Apple Script Events was not enabled") from e
166    self._is_running = True
167
168  @abc.abstractmethod
169  def _setup_window(self) -> None:
170    pass
171
172  def js(
173      self,
174      script: str,
175      timeout: Optional[dt.timedelta] = None,
176      arguments: Sequence[object] = ()
177  ) -> Any:
178    del timeout
179    js_script = AppleScript.js_script_with_args(script, arguments)
180    json_result: str = self._exec_apple_script(
181        self.APPLE_SCRIPT_JS_COMMAND.strip(), js_script=js_script).rstrip()
182    result, is_success = json.loads(json_result)
183    if not is_success:
184      raise AppleScript.JavaScriptFromAppleScriptException(result)
185    return result
186
187  def show_url(self, url: str, target: Optional[str] = None) -> None:
188    if target not in (None, "_self"):
189      raise NotImplementedError(
190          f"AppleScriptBrowser show_url does not support target {target}")
191    self._exec_apple_script(self.APPLE_SCRIPT_SET_URL, url=url)
192    self.platform.sleep(0.5)
193
194  def quit(self) -> None:
195    self._exec_apple_script("quit")
196    helper.wait_and_kill(self._browser_process)
197