From 1da8973323608181205163a1ca06409558a02b3a Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 12:22:54 +0000 Subject: [PATCH 01/35] WiP - interaction script --- bci/browser/interaction/__init__.py | 0 bci/browser/interaction/browsers/__init__.py | 0 bci/browser/interaction/browsers/browser.py | 82 +++++++++++++++++++ bci/browser/interaction/browsers/chromium.py | 51 ++++++++++++ bci/browser/interaction/browsers/firefox.py | 74 +++++++++++++++++ bci/browser/interaction/interaction.py | 13 +++ bci/evaluations/custom/custom_evaluation.py | 51 ++++++++---- .../UserInteraction/a.test/main/headers.json | 1 + .../UserInteraction/a.test/main/index.html | 6 ++ .../UserInteraction/interaction_script.cmd | 1 + requirements.in | 1 + requirements.txt | 4 +- 12 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 bci/browser/interaction/__init__.py create mode 100644 bci/browser/interaction/browsers/__init__.py create mode 100644 bci/browser/interaction/browsers/browser.py create mode 100644 bci/browser/interaction/browsers/chromium.py create mode 100644 bci/browser/interaction/browsers/firefox.py create mode 100644 bci/browser/interaction/interaction.py create mode 100644 experiments/pages/Support/UserInteraction/a.test/main/headers.json create mode 100644 experiments/pages/Support/UserInteraction/a.test/main/index.html create mode 100644 experiments/pages/Support/UserInteraction/interaction_script.cmd diff --git a/bci/browser/interaction/__init__.py b/bci/browser/interaction/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bci/browser/interaction/browsers/__init__.py b/bci/browser/interaction/browsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py new file mode 100644 index 00000000..a969fcce --- /dev/null +++ b/bci/browser/interaction/browsers/browser.py @@ -0,0 +1,82 @@ +import json +from abc import ABC, abstractmethod + +from websockets.sync.client import ClientConnection, connect + + +# Returns true if: +# - required == True -> all fields from `pattern` are present in `data` with the same value +# - required == False -> all fields from `pattern` which are present in `data` have the same value +def dictionaries_match(pattern: dict, data: dict, required: bool) -> bool: + for key in pattern: + if required and key not in data: + return False + + if key in data: + # Equal values, up to slashes + if ( + not dictionaries_match(pattern[key], data[key], required) + if isinstance(pattern[key], dict) + else str(data[key]).replace('/', '') != str(pattern[key]).replace('/', '') + ): + return False + return True + + +class Browser(ABC): + request_id: int = 0 + ws_timeout: float + ws: ClientConnection + + def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 0.5): + self.ws_timeout = autoclose_timeout + self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) + self.initialize_connection(browser_id, port, host) + + def req_id(self) -> int: + self.request_id += 1 + return self.request_id + + def send(self, data: dict) -> dict: + data['id'] = self.req_id() + + self.ws.send(json.dumps(data)) + + return self.receive({'method': data['method'], 'id': data['id']}, False) + + def listen(self, event: str, params: dict) -> dict: + return self.receive({'type': 'event', 'method': event, 'params': params}, True) + + def receive(self, data: dict, required: bool) -> dict: + result = None + + while result == None or (not dictionaries_match(data, result, required)): + result = json.loads(self.ws.recv(self.ws_timeout)) + + if 'type' in result and result['type'] == 'error': + raise Exception(f'Received browser error: {result}') + + return result + + # --- BROWSER-SPECIFIC METHODS --- + @abstractmethod + def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: + pass + + @abstractmethod + def initialize_connection(self, _browserId, _port, _host): + pass + + @abstractmethod + def close_connection( + self, + ): + pass + + @abstractmethod + def navigate(self, _url): + pass + + @abstractmethod + def click(self, _x, _y): + pass diff --git a/bci/browser/interaction/browsers/chromium.py b/bci/browser/interaction/browsers/chromium.py new file mode 100644 index 00000000..b82c7014 --- /dev/null +++ b/bci/browser/interaction/browsers/chromium.py @@ -0,0 +1,51 @@ +from time import sleep + +from .browser import Browser + + +class Chromium(Browser): + session_id: str + + def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: + return f'ws://{host}:{port}/devtools/browser/{browser_id}' + + def initialize_connection(self, browserId, port, host): + # Get list of all targets and find a "page" target. + target_response = self.send( + { + 'method': 'Target.getTargets', + } + ) + + page_target = list(filter(lambda info: (info['type'] == 'page'), target_response['result']['targetInfos']))[0][ + 'targetId' + ] + + # Attach to the page target. + session = self.send({'method': 'Target.attachToTarget', 'params': {'targetId': page_target, 'flatten': True}}) + + self.session_id = session['result']['sessionId'] + + def close_connection(self): + pass + + def navigate(self, url): + self.send({'sessionId': self.session_id, 'method': 'Page.navigate', 'params': {'url': url}}) + sleep(0.01) + + def click(self, x, y): + self.send( + { + 'sessionId': self.session_id, + 'method': 'Input.dispatchMouseEvent', + 'params': {'x': x, 'y': y, 'type': 'mousePressed', 'clickCount': 1, 'button': 'left'}, + } + ) + + self.send( + { + 'sessionId': self.session_id, + 'method': 'Input.dispatchMouseEvent', + 'params': {'x': x, 'y': y, 'type': 'mouseReleased', 'button': 'left'}, + } + ) diff --git a/bci/browser/interaction/browsers/firefox.py b/bci/browser/interaction/browsers/firefox.py new file mode 100644 index 00000000..97dd7202 --- /dev/null +++ b/bci/browser/interaction/browsers/firefox.py @@ -0,0 +1,74 @@ +from .browser import Browser + + +class Firefox(Browser): + browsing_context: str + + def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: + return f'ws://{host}:{port}/session' + + def initialize_connection(self, browserId, port, host): + # Initiate the session + session_id = self.send({'method': 'session.new', 'params': {'capabilities': {}}})['result']['sessionId'] + + # Subscribe to browser events + self.send( + { + 'method': 'session.subscribe', + 'params': { + 'events': [ + 'browsingContext.domContentLoaded', + ] + }, + } + ) + + # Create the browsing context + user_context = self.send({'method': 'browser.createUserContext', 'params': {}})['result']['userContext'] + + self.browsing_context = self.send( + {'method': 'browsingContext.create', 'params': {'type': 'tab', 'userContext': user_context}} + )['result']['context'] + + def close_connection(self): + self.send( + { + 'method': 'session.end', + 'params': {}, + } + ) + + def navigate(self, url): + navigation = self.send( + {'method': 'browsingContext.navigate', 'params': {'url': url, 'context': self.browsing_context}} + )['result']['navigation'] + + # Wait for the DOM to load + self.listen( + 'browsingContext.domContentLoaded', + { + 'url': url, + 'context': self.browsing_context, + 'navigation': navigation, + }, + ) + + def click(self, x, y): + self.send( + { + 'method': 'input.performActions', + 'params': { + 'context': self.browsing_context, + 'actions': [ + { + 'type': 'pointer', + 'id': str(self.req_id()), + 'actions': [ + {'type': 'pointerDown', 'button': 1, 'width': x, 'height': y}, + {'type': 'pointerUp', 'button': 1, 'width': x, 'height': y}, + ], + } + ], + }, + } + ) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py new file mode 100644 index 00000000..8a014ac1 --- /dev/null +++ b/bci/browser/interaction/interaction.py @@ -0,0 +1,13 @@ +from bci.browser.configuration.browser import Browser as BrowserConfig + + +class Interaction: + browser_config: BrowserConfig + script: list[str] + + def __init__(self, browser: BrowserConfig, script: list[str]) -> None: + self.browser_config = browser + self.script = script + + def execute(self) -> None: + print(f'TODO - execute {self.browser_config._get_terminal_args()} with script {", ".join(self.script)}') diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 279065e3..3f235378 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -3,6 +3,7 @@ import textwrap from bci.browser.configuration.browser import Browser +from bci.browser.interaction.interaction import Interaction from bci.configuration import Global from bci.evaluations.collectors.collector import Collector, Type from bci.evaluations.evaluation_framework import EvaluationFramework @@ -16,7 +17,7 @@ class CustomEvaluationFramework(EvaluationFramework): def __init__(self): super().__init__() self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree) + self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) @staticmethod def initialize_dir_tree() -> dict: @@ -35,7 +36,7 @@ def path_to_dict(path): return path_to_dict(path) @staticmethod - def initialize_tests_and_url_queues(dir_tree: dict) -> dict: + def initialize_tests_and_interactions(dir_tree: dict) -> dict: experiments_per_project = {} page_folder_path = Global.custom_page_folder for project, experiments in dir_tree.items(): @@ -43,13 +44,29 @@ def initialize_tests_and_url_queues(dir_tree: dict) -> dict: project_path = os.path.join(page_folder_path, project) experiments_per_project[project] = {} for experiment in experiments: - url_queue = CustomEvaluationFramework.__get_url_queue(project, project_path, experiment) - experiments_per_project[project][experiment] = { - 'url_queue': url_queue, - 'runnable': CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree), - } + interaction_script = CustomEvaluationFramework.__get_interaction_script(project_path, experiment) + data = {} + + if interaction_script is not None: + data['interaction_script'] = interaction_script + else: + url_queue = CustomEvaluationFramework.__get_url_queue(project, project_path, experiment) + data['url_queue'] = url_queue + + data['runnable'] = CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree) + + experiments_per_project[project][experiment] = data return experiments_per_project + @staticmethod + def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: + interaction_file_path = os.path.join(project_path, experiment, 'interaction_script.cmd') + if os.path.isfile(interaction_file_path): + # If an interaction script is specified, it is parsed and used + with open(interaction_file_path) as file: + return file.readlines() + return None + @staticmethod def __get_url_queue(project: str, project_path: str, experiment: str) -> list[str]: url_queue_file_path = os.path.join(project_path, experiment, 'url_queue.txt') @@ -88,12 +105,18 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) is_dirty = False try: - url_queue = self.tests_per_project[params.evaluation_configuration.project][params.mech_group]['url_queue'] - for url in url_queue: - tries = 0 - while tries < 3: - tries += 1 - browser.visit(url) + experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] + + if 'interaction_script' in experiment: + interaction = Interaction(browser, experiment['interaction_script']) + interaction.execute() + else: + url_queue = experiment['url_queue'] + for url in url_queue: + tries = 0 + while tries < 3: + tries += 1 + browser.visit(url) except Exception as e: logger.error(f'Error during test: {e}', exc_info=True) is_dirty = True @@ -205,5 +228,5 @@ def include_file_headers(file_type: str) -> bool: def sync_with_folders(self): self.dir_tree = self.initialize_dir_tree() - self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree) + self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree) logger.info('Framework is synced with folders') diff --git a/experiments/pages/Support/UserInteraction/a.test/main/headers.json b/experiments/pages/Support/UserInteraction/a.test/main/headers.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/experiments/pages/Support/UserInteraction/a.test/main/headers.json @@ -0,0 +1 @@ +[] diff --git a/experiments/pages/Support/UserInteraction/a.test/main/index.html b/experiments/pages/Support/UserInteraction/a.test/main/index.html new file mode 100644 index 00000000..ad482c63 --- /dev/null +++ b/experiments/pages/Support/UserInteraction/a.test/main/index.html @@ -0,0 +1,6 @@ + + CLICK + + \ No newline at end of file diff --git a/experiments/pages/Support/UserInteraction/interaction_script.cmd b/experiments/pages/Support/UserInteraction/interaction_script.cmd new file mode 100644 index 00000000..5daa56cf --- /dev/null +++ b/experiments/pages/Support/UserInteraction/interaction_script.cmd @@ -0,0 +1 @@ +NAVIGATE https://a.test/Support/UserInteraction/main diff --git a/requirements.in b/requirements.in index b7055ad5..385a39ca 100644 --- a/requirements.in +++ b/requirements.in @@ -5,3 +5,4 @@ flatten-dict gunicorn pymongo requests +websockets \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d2968cc8..a5f13430 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile requirements.in @@ -54,6 +54,8 @@ urllib3==2.2.3 # via # docker # requests +websockets==13.1 + # via -r requirements.in werkzeug==3.0.4 # via flask wsproto==1.2.0 From 5a516ba2765d2dc8bab09293163390ce1c3f8e93 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:10:09 +0000 Subject: [PATCH 02/35] Connect browser interaction to terminal automation --- bci/browser/automation/terminal.py | 35 +++++++++++++-------- bci/browser/configuration/browser.py | 21 ++++++++++--- bci/browser/configuration/chromium.py | 2 ++ bci/browser/interaction/interaction.py | 17 ++++++++-- bci/evaluations/custom/custom_evaluation.py | 8 +++-- 5 files changed, 61 insertions(+), 22 deletions(-) diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index d1fe6f70..ed2235e1 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -7,22 +7,31 @@ class TerminalAutomation: - @staticmethod - def run(url: str, args: list[str], seconds_per_visit: int): - logger.debug("Starting browser process...") + def visit_url(url: str, args: list[str], seconds_per_visit: int): args.append(url) + proc, _ = TerminalAutomation.open_browser(args) + logger.debug(f'Visiting the page for {seconds_per_visit}s') + time.sleep(seconds_per_visit) + TerminalAutomation.terminate_browser(proc, args) + + @staticmethod + def open_browser(args: list[str]) -> tuple[subprocess.Popen, str]: + logger.debug('Starting browser process...') logger.debug(f'Command string: \'{" ".join(args)}\'') - with open('/tmp/browser.log', 'a') as file: - proc = subprocess.Popen( - args, - stdout=file, - stderr=file - ) + with open('/tmp/browser.log', 'a+') as file: + proc = subprocess.Popen(args, stdout=file, stderr=file) + time.sleep(0.5) + position = file.tell() + file.seek(0) + output = file.read() + file.seek(position) + return proc, output - time.sleep(seconds_per_visit) + @staticmethod + def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None: + logger.debug('Terminating browser process using SIGINT...') - logger.debug(f'Terminating browser process after {seconds_per_visit}s using SIGINT...') # Use SIGINT and SIGTERM to end process such that cookies remain saved. proc.send_signal(signal.SIGINT) proc.send_signal(signal.SIGTERM) @@ -30,8 +39,8 @@ def run(url: str, args: list[str], seconds_per_visit: int): try: stdout, stderr = proc.communicate(timeout=5) except subprocess.TimeoutExpired: - logger.info("Browser process did not terminate after 5s. Killing process through pkill...") + logger.info('Browser process did not terminate after 5s. Killing process through pkill...') subprocess.run(['pkill', '-2', args[0].split('/')[-1]]) proc.wait() - logger.debug("Browser process terminated.") + logger.debug('Browser process terminated.') diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 1178ece1..67419cd4 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import subprocess from abc import abstractmethod import bci.browser.binary.factory as binary_factory @@ -15,8 +16,11 @@ class Browser: + process: subprocess.Popen - def __init__(self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary) -> None: + def __init__( + self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary + ) -> None: self.browser_config = browser_config self.eval_config = eval_config self.binary = binary @@ -34,10 +38,17 @@ def visit(self, url: str): match self.eval_config.automation: case 'terminal': args = self._get_terminal_args() - TerminalAutomation.run(url, args, self.eval_config.seconds_per_visit) + TerminalAutomation.visit_url(url, args, self.eval_config.seconds_per_visit) case _: raise AttributeError('Not implemented') + def open(self) -> str: + self.process, output = TerminalAutomation.open_browser(self._get_terminal_args()) + return output + + def terminate(self): + TerminalAutomation.terminate_browser(self.process, self._get_terminal_args()) + def pre_evaluation_setup(self): self.__fetch_binary() @@ -80,11 +91,13 @@ def _get_executable_file_path(self) -> str: return os.path.join(self.__get_execution_folder_path(), self.binary.executable_name) @abstractmethod - def _get_terminal_args(self): + def _get_terminal_args(self) -> list[str]: pass @staticmethod - def get_browser(browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State) -> Browser: + def get_browser( + browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State + ) -> Browser: from bci.browser.configuration.chromium import Chromium from bci.browser.configuration.firefox import Firefox diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index 6108328d..3aa0f8e3 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -1,6 +1,7 @@ from bci.browser.configuration.browser import Browser from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing from bci.browser.configuration.profile import prepare_chromium_profile +from bci.browser.interaction.interaction import Interaction SUPPORTED_OPTIONS = [ Default(), @@ -41,6 +42,7 @@ def _get_terminal_args(self) -> list[str]: args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') + args.append(f'--remote-debugging-port={Interaction.port}') # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless # Using the `--headless` flag will crash the browser for these later versions. # Also see: https://github.com/DistriNet/BugHog/issues/12 diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 8a014ac1..1f867a0a 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -1,13 +1,24 @@ +from bci.browser.automation.terminal import TerminalAutomation from bci.browser.configuration.browser import Browser as BrowserConfig class Interaction: - browser_config: BrowserConfig + port = 9222 + + browser: BrowserConfig script: list[str] def __init__(self, browser: BrowserConfig, script: list[str]) -> None: - self.browser_config = browser + self.browser = browser self.script = script def execute(self) -> None: - print(f'TODO - execute {self.browser_config._get_terminal_args()} with script {", ".join(self.script)}') + output = self.browser.open() + + # TODO - identify browser from the output + # TODO - initialize the browser + # TODO - parse the script and run the commands + # TODO - visit the sanity check URL + # TODO - destroy the browser + + self.browser.terminate() diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 3f235378..b2d97ef3 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -107,14 +107,18 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) try: experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] + max_tries = 3 if 'interaction_script' in experiment: interaction = Interaction(browser, experiment['interaction_script']) - interaction.execute() + tries = 0 + while tries < max_tries: + tries += 1 + interaction.execute() else: url_queue = experiment['url_queue'] for url in url_queue: tries = 0 - while tries < 3: + while tries < max_tries: tries += 1 browser.visit(url) except Exception as e: From 6cef7597894fef52a5236a8e5521440453b855ff Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:26:05 +0000 Subject: [PATCH 03/35] Install pip requirements in dev container --- .devcontainer/devcontainer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8687890c..16cc9c75 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,7 +24,10 @@ "Vue.volar" ] } - } + }, + + // Install pip requirements + "postCreateCommand": "pip install -r requirements.txt" // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -35,9 +38,6 @@ // Uncomment the next line if you want to keep your containers running after VS Code shuts down. // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "cat /etc/os-release", - // Configure tool-specific properties. // "customizations": {}, } From 688948fcb7748dd5011c355b1c6e716043a0f56d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:43:36 +0000 Subject: [PATCH 04/35] Simple interaction browser identification --- bci/browser/interaction/browsers/chromium.py | 2 +- bci/browser/interaction/interaction.py | 25 +++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/bci/browser/interaction/browsers/chromium.py b/bci/browser/interaction/browsers/chromium.py index b82c7014..7db878af 100644 --- a/bci/browser/interaction/browsers/chromium.py +++ b/bci/browser/interaction/browsers/chromium.py @@ -31,7 +31,7 @@ def close_connection(self): def navigate(self, url): self.send({'sessionId': self.session_id, 'method': 'Page.navigate', 'params': {'url': url}}) - sleep(0.01) + sleep(0.5) def click(self, x, y): self.send( diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 1f867a0a..518d5ee4 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -1,5 +1,8 @@ -from bci.browser.automation.terminal import TerminalAutomation +import re + from bci.browser.configuration.browser import Browser as BrowserConfig +from bci.browser.interaction.browsers.browser import Browser +from bci.browser.interaction.browsers.chromium import Chromium class Interaction: @@ -15,10 +18,20 @@ def __init__(self, browser: BrowserConfig, script: list[str]) -> None: def execute(self) -> None: output = self.browser.open() - # TODO - identify browser from the output - # TODO - initialize the browser - # TODO - parse the script and run the commands - # TODO - visit the sanity check URL - # TODO - destroy the browser + interaction_browser = self._initiate_browser(output) + + # TODO - parse the script and run the commands instead + interaction_browser.navigate('https://a.test/Support/UserInteraction/main') + + interaction_browser.navigate('https://a.test/report/?bughog_sanity_check=OK') + interaction_browser.close_connection() self.browser.terminate() + + def _initiate_browser(self, init_output: str) -> Browser: + cdp = re.search(r'DevTools listening on ws:\/\/127\.0\.0\.1:9222\/devtools\/browser\/(.+)\n', init_output) + + if cdp: + return Chromium(browser_id=cdp.group(1), port=Interaction.port) + + raise Exception('Unrecognized browser') From b2f1f828e1bbb3b9615b659787f10f8e7bd35661 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 14:48:26 +0000 Subject: [PATCH 05/35] Fix browser log reading --- bci/browser/automation/terminal.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index ed2235e1..77869206 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -20,12 +20,13 @@ def open_browser(args: list[str]) -> tuple[subprocess.Popen, str]: logger.debug('Starting browser process...') logger.debug(f'Command string: \'{" ".join(args)}\'') with open('/tmp/browser.log', 'a+') as file: + initial_position = file.tell() proc = subprocess.Popen(args, stdout=file, stderr=file) time.sleep(0.5) - position = file.tell() - file.seek(0) + last_position = file.tell() + file.seek(initial_position) output = file.read() - file.seek(position) + file.seek(last_position) return proc, output @staticmethod From e8f56a2903c5868ae76ce12ed82b28476ed91743 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 15:19:17 +0000 Subject: [PATCH 06/35] Browser interaction commands interpreter --- bci/browser/interaction/browsers/browser.py | 2 ++ bci/browser/interaction/interaction.py | 28 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index a969fcce..fcee439b 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -28,6 +28,8 @@ class Browser(ABC): ws_timeout: float ws: ClientConnection + public_methods: list[str] = ['navigate', 'click'] + def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 0.5): self.ws_timeout = autoclose_timeout self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 518d5ee4..65b582b0 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -1,9 +1,13 @@ +import logging import re +from inspect import signature from bci.browser.configuration.browser import Browser as BrowserConfig from bci.browser.interaction.browsers.browser import Browser from bci.browser.interaction.browsers.chromium import Chromium +logger = logging.getLogger(__name__) + class Interaction: port = 9222 @@ -20,8 +24,7 @@ def execute(self) -> None: interaction_browser = self._initiate_browser(output) - # TODO - parse the script and run the commands instead - interaction_browser.navigate('https://a.test/Support/UserInteraction/main') + self._interpret(interaction_browser) interaction_browser.navigate('https://a.test/report/?bughog_sanity_check=OK') interaction_browser.close_connection() @@ -35,3 +38,24 @@ def _initiate_browser(self, init_output: str) -> Browser: return Chromium(browser_id=cdp.group(1), port=Interaction.port) raise Exception('Unrecognized browser') + + def _interpret(self, browser: Browser) -> None: + for statement in self.script: + cmd, *args = statement.split() + method_name = cmd.lower() + + if method_name not in Browser.public_methods: + raise Exception( + f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), Browser.public_methods))}.' + ) + + method = getattr(browser, method_name) + method_params_len = len(signature(method).parameters) + + if method_params_len != len(args): + raise Exception( + f'Invalid number of arguments for command `{cmd}`. Expected {method_params_len}, got {len(args)}.' + ) + + logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') + method(*args) From c0e47903e97a42eded3617864d2166275d7af134 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 16:55:44 +0000 Subject: [PATCH 07/35] Basic BiDi implementation --- bci/browser/configuration/chromium.py | 3 +-- bci/browser/configuration/firefox.py | 4 ++-- bci/browser/interaction/browsers/browser.py | 2 +- bci/browser/interaction/browsers/firefox.py | 11 ++++++----- bci/browser/interaction/interaction.py | 20 ++++++++++++++++---- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index 3aa0f8e3..c2d70ea4 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -1,7 +1,6 @@ from bci.browser.configuration.browser import Browser from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing from bci.browser.configuration.profile import prepare_chromium_profile -from bci.browser.interaction.interaction import Interaction SUPPORTED_OPTIONS = [ Default(), @@ -42,7 +41,7 @@ def _get_terminal_args(self) -> list[str]: args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') - args.append(f'--remote-debugging-port={Interaction.port}') + args.append('--remote-debugging-port=0') # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless # Using the `--headless` flag will crash the browser for these later versions. # Also see: https://github.com/DistriNet/BugHog/issues/12 diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index 643ae57c..c6918d70 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -2,10 +2,9 @@ from bci import cli from bci.browser.configuration.browser import Browser -from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing, TrackingProtection +from bci.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection from bci.browser.configuration.profile import prepare_firefox_profile - SUPPORTED_OPTIONS = [ Default(), BlockThirdPartyCookies(), @@ -26,6 +25,7 @@ def _get_terminal_args(self) -> list[str]: args = [self._get_executable_file_path()] args.extend(['-profile', self._profile_path]) + args.append('--remote-debugging-port=0') user_prefs = [] def add_user_pref(key: str, value: str | int | bool): diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index fcee439b..94dcc5ff 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -30,7 +30,7 @@ class Browser(ABC): public_methods: list[str] = ['navigate', 'click'] - def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 0.5): + def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 2): self.ws_timeout = autoclose_timeout self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) self.initialize_connection(browser_id, port, host) diff --git a/bci/browser/interaction/browsers/firefox.py b/bci/browser/interaction/browsers/firefox.py index 97dd7202..9f5d6f02 100644 --- a/bci/browser/interaction/browsers/firefox.py +++ b/bci/browser/interaction/browsers/firefox.py @@ -46,11 +46,12 @@ def navigate(self, url): # Wait for the DOM to load self.listen( 'browsingContext.domContentLoaded', - { - 'url': url, - 'context': self.browsing_context, - 'navigation': navigation, - }, + {} + #{ + # 'url': url, + # 'context': self.browsing_context, + # 'navigation': navigation, + #}, ) def click(self, x, y): diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 65b582b0..a3c98488 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -5,13 +5,12 @@ from bci.browser.configuration.browser import Browser as BrowserConfig from bci.browser.interaction.browsers.browser import Browser from bci.browser.interaction.browsers.chromium import Chromium +from bci.browser.interaction.browsers.firefox import Firefox logger = logging.getLogger(__name__) class Interaction: - port = 9222 - browser: BrowserConfig script: list[str] @@ -32,10 +31,23 @@ def execute(self) -> None: self.browser.terminate() def _initiate_browser(self, init_output: str) -> Browser: - cdp = re.search(r'DevTools listening on ws:\/\/127\.0\.0\.1:9222\/devtools\/browser\/(.+)\n', init_output) + print(init_output) + + cdp = re.search( + r'DevTools listening on ws:\/\/(.+):(.+)\/devtools\/browser\/(.+)\n', + init_output, + ) if cdp: - return Chromium(browser_id=cdp.group(1), port=Interaction.port) + return Chromium(browser_id=cdp.group(3), port=int(cdp.group(2)), host=cdp.group(1)) + + bidi = re.search( + r'WebDriver BiDi listening on ws:\/\/(.+):(.+)', + init_output, + ) + + if bidi: + return Firefox(port=int(bidi.group(2)), host=bidi.group(1)) raise Exception('Unrecognized browser') From 796bf93e22007beaeeae4b518270f19f6e418a22 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Wed, 30 Oct 2024 16:57:12 +0000 Subject: [PATCH 08/35] Remove print --- bci/browser/interaction/interaction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index a3c98488..f0186382 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -31,8 +31,6 @@ def execute(self) -> None: self.browser.terminate() def _initiate_browser(self, init_output: str) -> Browser: - print(init_output) - cdp = re.search( r'DevTools listening on ws:\/\/(.+):(.+)\/devtools\/browser\/(.+)\n', init_output, From c2c5580561c09294f6082897c3984f74a66c9e6e Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 08:55:56 +0000 Subject: [PATCH 09/35] Simple AutoGUI test experiment --- experiments/pages/Support/AutoGUI/a.test/main/headers.json | 1 + experiments/pages/Support/AutoGUI/a.test/main/index.html | 1 + experiments/pages/Support/AutoGUI/interaction_script.cmd | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 experiments/pages/Support/AutoGUI/a.test/main/headers.json create mode 100644 experiments/pages/Support/AutoGUI/a.test/main/index.html create mode 100644 experiments/pages/Support/AutoGUI/interaction_script.cmd diff --git a/experiments/pages/Support/AutoGUI/a.test/main/headers.json b/experiments/pages/Support/AutoGUI/a.test/main/headers.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/experiments/pages/Support/AutoGUI/a.test/main/headers.json @@ -0,0 +1 @@ +[] diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/experiments/pages/Support/AutoGUI/a.test/main/index.html new file mode 100644 index 00000000..4c9318e8 --- /dev/null +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -0,0 +1 @@ +CLICK \ No newline at end of file diff --git a/experiments/pages/Support/AutoGUI/interaction_script.cmd b/experiments/pages/Support/AutoGUI/interaction_script.cmd new file mode 100644 index 00000000..92690174 --- /dev/null +++ b/experiments/pages/Support/AutoGUI/interaction_script.cmd @@ -0,0 +1,2 @@ +NAVIGATE https://a.test/Support/AutoGUI/main +CLICK 100 100 \ No newline at end of file From b8c02dd5be326749c7cadd03504887dd61f28181 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 09:01:13 +0000 Subject: [PATCH 10/35] Install AutoGUI --- requirements.in | 3 ++- requirements.txt | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/requirements.in b/requirements.in index 385a39ca..814ea5bf 100644 --- a/requirements.in +++ b/requirements.in @@ -5,4 +5,5 @@ flatten-dict gunicorn pymongo requests -websockets \ No newline at end of file +websockets +pyautogui \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a5f13430..c03e9c00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,10 +38,30 @@ markupsafe==3.0.2 # via # jinja2 # werkzeug +mouseinfo==0.1.3 + # via pyautogui packaging==24.1 # via gunicorn +pyautogui==0.9.54 + # via -r requirements.in +pygetwindow==0.0.9 + # via pyautogui pymongo==4.10.1 # via -r requirements.in +pymsgbox==1.0.9 + # via pyautogui +pyperclip==1.9.0 + # via mouseinfo +pyrect==0.2.0 + # via pygetwindow +pyscreeze==1.0.1 + # via pyautogui +python3-xlib==0.15 + # via + # mouseinfo + # pyautogui +pytweening==1.2.0 + # via pyautogui requests==2.32.3 # via # -r requirements.in From 1444ff2de22fbede606e9c9f3ea64b00c924c1b0 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 10:07:16 +0000 Subject: [PATCH 11/35] Simple AutoGUI usage --- Dockerfile | 4 + bci/browser/configuration/browser.py | 13 ++- bci/browser/interaction/browsers/browser.py | 106 +++++++------------- bci/browser/interaction/interaction.py | 13 ++- requirements.in | 6 +- requirements.txt | 12 ++- 6 files changed, 67 insertions(+), 87 deletions(-) diff --git a/Dockerfile b/Dockerfile index b7bd1020..9439b3d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,11 @@ RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb # Install python packages COPY requirements.txt /app/requirements.txt RUN pip install --user -r /app/requirements.txt +RUN apt-get install python3-tk python3-xlib gnome-screenshot -y +# Initiate PyAutoGUI +RUN touch /root/.Xauthority && \ + xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom) FROM base AS core # Copy rest of source code diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 67419cd4..707f53d3 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -16,12 +16,13 @@ class Browser: - process: subprocess.Popen + process: subprocess.Popen | None def __init__( self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary ) -> None: self.browser_config = browser_config + self.process = None self.eval_config = eval_config self.binary = binary self.state = binary.state @@ -42,12 +43,18 @@ def visit(self, url: str): case _: raise AttributeError('Not implemented') - def open(self) -> str: - self.process, output = TerminalAutomation.open_browser(self._get_terminal_args()) + def open(self, url: str) -> str: + args = self._get_terminal_args() + args.append(url) + self.process, output = TerminalAutomation.open_browser(args) return output def terminate(self): + if self.process is None: + return + TerminalAutomation.terminate_browser(self.process, self._get_terminal_args()) + self.process = None def pre_evaluation_setup(self): self.__fetch_binary() diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index 94dcc5ff..cdfec4ae 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -1,84 +1,46 @@ -import json -from abc import ABC, abstractmethod +import base64 +import os +from io import BytesIO +from time import sleep -from websockets.sync.client import ClientConnection, connect +import pyautogui as gui +import Xlib.display +from pyvirtualdisplay.display import Display +from bci.browser.configuration.browser import Browser as BrowserConfig -# Returns true if: -# - required == True -> all fields from `pattern` are present in `data` with the same value -# - required == False -> all fields from `pattern` which are present in `data` have the same value -def dictionaries_match(pattern: dict, data: dict, required: bool) -> bool: - for key in pattern: - if required and key not in data: - return False - - if key in data: - # Equal values, up to slashes - if ( - not dictionaries_match(pattern[key], data[key], required) - if isinstance(pattern[key], dict) - else str(data[key]).replace('/', '') != str(pattern[key]).replace('/', '') - ): - return False - return True - - -class Browser(ABC): - request_id: int = 0 - ws_timeout: float - ws: ClientConnection +class Browser: + browser_config: BrowserConfig public_methods: list[str] = ['navigate', 'click'] - def __init__(self, browser_id: str = '', port: int = 9222, host: str = '127.0.0.1', autoclose_timeout: float = 2): - self.ws_timeout = autoclose_timeout - self.ws = connect(self.get_ws_endpoint(host, port, browser_id), close_timeout=autoclose_timeout) - self.initialize_connection(browser_id, port, host) - - def req_id(self) -> int: - self.request_id += 1 - return self.request_id - - def send(self, data: dict) -> dict: - data['id'] = self.req_id() - - self.ws.send(json.dumps(data)) - - return self.receive({'method': data['method'], 'id': data['id']}, False) - - def listen(self, event: str, params: dict) -> dict: - return self.receive({'type': 'event', 'method': event, 'params': params}, True) - - def receive(self, data: dict, required: bool) -> dict: - result = None - - while result == None or (not dictionaries_match(data, result, required)): - result = json.loads(self.ws.recv(self.ws_timeout)) - - if 'type' in result and result['type'] == 'error': - raise Exception(f'Received browser error: {result}') + def __init__(self, browser_config: BrowserConfig): + self.browser_config = browser_config + disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True) + disp.start() + gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY']) - return result + def __del__(self): + self.browser_config.terminate() - # --- BROWSER-SPECIFIC METHODS --- - @abstractmethod - def get_ws_endpoint(self, host: str, port: int, browser_id: str) -> str: - pass + # --- PUBLIC METHODS --- + def navigate(self, url: str): + self.browser_config.terminate() + self.browser_config.open(url) - @abstractmethod - def initialize_connection(self, _browserId, _port, _host): - pass + # TODO - convert this into an argument or a separate command + sleep(0.5) - @abstractmethod - def close_connection( - self, - ): - pass + def click(self, x: str, y: str): + # print(gui.size()) + # print(gui.position()) + # gui.moveTo(int(x), int(y)) + gui.moveTo(100, 540) + gui.click() - @abstractmethod - def navigate(self, _url): - pass + # buffered = BytesIO() + # print(gui.screenshot().save(buffered, format='JPEG')) + # img_str = base64.b64encode(buffered.getvalue()) + # print(img_str.decode('utf-8')) - @abstractmethod - def click(self, _x, _y): - pass + sleep(0.5) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index f0186382..e84c4328 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -19,18 +19,16 @@ def __init__(self, browser: BrowserConfig, script: list[str]) -> None: self.script = script def execute(self) -> None: - output = self.browser.open() - - interaction_browser = self._initiate_browser(output) + interaction_browser = self._initiate_browser() self._interpret(interaction_browser) interaction_browser.navigate('https://a.test/report/?bughog_sanity_check=OK') - interaction_browser.close_connection() - - self.browser.terminate() - def _initiate_browser(self, init_output: str) -> Browser: + def _initiate_browser(self) -> Browser: + # TODO - possibly return different browser instances + return Browser(self.browser) + """ cdp = re.search( r'DevTools listening on ws:\/\/(.+):(.+)\/devtools\/browser\/(.+)\n', init_output, @@ -48,6 +46,7 @@ def _initiate_browser(self, init_output: str) -> Browser: return Firefox(port=int(bidi.group(2)), host=bidi.group(1)) raise Exception('Unrecognized browser') + """ def _interpret(self, browser: Browser) -> None: for statement in self.script: diff --git a/requirements.in b/requirements.in index 814ea5bf..f0993275 100644 --- a/requirements.in +++ b/requirements.in @@ -5,5 +5,7 @@ flatten-dict gunicorn pymongo requests -websockets -pyautogui \ No newline at end of file +pyautogui +pyvirtualdisplay +Pillow +Xlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c03e9c00..2a367b96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,6 +42,8 @@ mouseinfo==0.1.3 # via pyautogui packaging==24.1 # via gunicorn +pillow==11.0.0 + # via -r requirements.in pyautogui==0.9.54 # via -r requirements.in pygetwindow==0.0.9 @@ -62,6 +64,8 @@ python3-xlib==0.15 # pyautogui pytweening==1.2.0 # via pyautogui +pyvirtualdisplay==3.0 + # via -r requirements.in requests==2.32.3 # via # -r requirements.in @@ -69,14 +73,16 @@ requests==2.32.3 simple-websocket==1.1.0 # via flask-sock six==1.16.0 - # via flatten-dict + # via + # flatten-dict + # xlib urllib3==2.2.3 # via # docker # requests -websockets==13.1 - # via -r requirements.in werkzeug==3.0.4 # via flask wsproto==1.2.0 # via simple-websocket +xlib==0.21 + # via -r requirements.in From 0acc9c204d9cba8d7862167f7139b9a63be516a4 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 10:17:02 +0000 Subject: [PATCH 12/35] Remove debugging ports --- bci/browser/configuration/chromium.py | 1 - bci/browser/configuration/firefox.py | 1 - 2 files changed, 2 deletions(-) diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index c2d70ea4..6108328d 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -41,7 +41,6 @@ def _get_terminal_args(self) -> list[str]: args.append('--enable-logging') args.append('--v=1') args.append('--log-level=0') - args.append('--remote-debugging-port=0') # Headless changed from version +/- 110 onwards: https://developer.chrome.com/docs/chromium/new-headless # Using the `--headless` flag will crash the browser for these later versions. # Also see: https://github.com/DistriNet/BugHog/issues/12 diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index c6918d70..c336190b 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -25,7 +25,6 @@ def _get_terminal_args(self) -> list[str]: args = [self._get_executable_file_path()] args.extend(['-profile', self._profile_path]) - args.append('--remote-debugging-port=0') user_prefs = [] def add_user_pref(key: str, value: str | int | bool): From 840190a8cdd5447039e4c732e32548cae0ffb83d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 11:14:46 +0000 Subject: [PATCH 13/35] Fix opening Firefox --- bci/browser/automation/terminal.py | 12 +++--------- bci/browser/configuration/browser.py | 5 ++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index 77869206..ddc08b8e 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -10,24 +10,18 @@ class TerminalAutomation: @staticmethod def visit_url(url: str, args: list[str], seconds_per_visit: int): args.append(url) - proc, _ = TerminalAutomation.open_browser(args) + proc = TerminalAutomation.open_browser(args) logger.debug(f'Visiting the page for {seconds_per_visit}s') time.sleep(seconds_per_visit) TerminalAutomation.terminate_browser(proc, args) @staticmethod - def open_browser(args: list[str]) -> tuple[subprocess.Popen, str]: + def open_browser(args: list[str]) -> subprocess.Popen: logger.debug('Starting browser process...') logger.debug(f'Command string: \'{" ".join(args)}\'') with open('/tmp/browser.log', 'a+') as file: - initial_position = file.tell() proc = subprocess.Popen(args, stdout=file, stderr=file) - time.sleep(0.5) - last_position = file.tell() - file.seek(initial_position) - output = file.read() - file.seek(last_position) - return proc, output + return proc @staticmethod def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None: diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 707f53d3..0247f28e 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -43,11 +43,10 @@ def visit(self, url: str): case _: raise AttributeError('Not implemented') - def open(self, url: str) -> str: + def open(self, url: str) -> None: args = self._get_terminal_args() args.append(url) - self.process, output = TerminalAutomation.open_browser(args) - return output + self.process = TerminalAutomation.open_browser(args) def terminate(self): if self.process is None: From c85fa2a198af6a45748fbad00606ed692da4e59d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 11:15:15 +0000 Subject: [PATCH 14/35] Fix opening Firefox --- bci/browser/configuration/firefox.py | 2 ++ bci/browser/interaction/browsers/browser.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index c336190b..4310bf20 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -25,6 +25,7 @@ def _get_terminal_args(self) -> list[str]: args = [self._get_executable_file_path()] args.extend(['-profile', self._profile_path]) + args.append('-setDefaultBrowser') user_prefs = [] def add_user_pref(key: str, value: str | int | bool): @@ -44,6 +45,7 @@ def add_user_pref(key: str, value: str | int | bool): # add_user_pref('network.proxy.type', 1) add_user_pref('app.update.enabled', False) + add_user_pref('browser.shell.checkDefaultBrowser', False) if 'default' in self.browser_config.browser_setting: pass elif 'btpc' in self.browser_config.browser_setting: diff --git a/bci/browser/interaction/browsers/browser.py b/bci/browser/interaction/browsers/browser.py index cdfec4ae..2d45dafd 100644 --- a/bci/browser/interaction/browsers/browser.py +++ b/bci/browser/interaction/browsers/browser.py @@ -29,12 +29,13 @@ def navigate(self, url: str): self.browser_config.open(url) # TODO - convert this into an argument or a separate command - sleep(0.5) + sleep(2) def click(self, x: str, y: str): # print(gui.size()) # print(gui.position()) # gui.moveTo(int(x), int(y)) + gui.moveTo(100, 540) gui.click() From 44e5dac47981a39b41e81bf5354538c7255781ac Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Thu, 31 Oct 2024 12:15:44 +0000 Subject: [PATCH 15/35] Editing config files through the web UI --- bci/evaluations/custom/custom_evaluation.py | 17 ++++--- bci/web/vue/src/components/poc-editor.vue | 51 ++++++++++++++++++--- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index b2d97ef3..8e81ed90 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -25,11 +25,7 @@ def initialize_dir_tree() -> dict: def path_to_dict(path): if os.path.isdir(path): - return { - sub_folder: path_to_dict(os.path.join(path, sub_folder)) - for sub_folder in os.listdir(path) - if sub_folder != 'url_queue.txt' - } + return {sub_folder: path_to_dict(os.path.join(path, sub_folder)) for sub_folder in os.listdir(path)} else: return os.path.basename(path) @@ -154,15 +150,22 @@ def get_projects(self) -> list[str]: def get_poc_structure(self, project: str, poc: str) -> dict: return self.dir_tree[project][poc] + def _get_poc_file_path(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: + # Top-level config file + if domain == 'Config' and path == '_': + return os.path.join(Global.custom_page_folder, project, poc, file_name) + + return os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) + def get_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str: - file_path = os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) + file_path = self._get_poc_file_path(project, poc, domain, path, file_name) if os.path.isfile(file_path): with open(file_path) as file: return file.read() raise AttributeError(f"Could not find PoC file at expected path '{file_path}'") def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool: - file_path = os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name) + file_path = self._get_poc_file_path(project, poc, domain, path, file_name) if os.path.isfile(file_path): if content == '': logger.warning('Attempt to save empty file ignored') diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index 59e894fb..53a2eaac 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -37,6 +37,7 @@ // "path1": ["file1", "file2"], // "path2": ["file1"], // }, + // 'interaction_script.cmd': 'interaction_script.cmd', // }, }, available_file_types: [ @@ -44,6 +45,10 @@ 'js', 'py', ], + poc_tree_config: { + domain: 'Config', + page: '_', + }, dialog: { domain: { name: null, @@ -59,6 +64,33 @@ timeout: null, } }, + computed: { + active_poc_tree() { + return Object.entries(this.active_poc.tree).reduce((acc, [domain, pages]) => { + if (domain !== pages) { + return [ + ...acc, + [domain, pages] + ] + } + + const configDomain = this.poc_tree_config.domain; + const configPage = this.poc_tree_config.page; + + // A single top-level file -> create a virtual config folder + const config_folder = acc.find(([domain]) => domain === configDomain) ?? [configDomain, {[configPage]: {}}]; + return [ + ...acc.filter(([domain]) => domain !== configDomain), + [configDomain, { + [configPage]: { + ...config_folder[1].subfolder, + [domain]: pages, + } + }], + ] + }, []); + } + }, methods: { set_active_file(domain, file_path, file_name) { this.active_poc.active_domain = domain; @@ -199,16 +231,21 @@
    @@ -304,4 +327,24 @@ + + + +
    +

    +

    Choose config type:
    + + +

    +
    + + +
    +
    +
    diff --git a/bci/web/vue/src/main.js b/bci/web/vue/src/main.js index 70ff98b0..121570e3 100644 --- a/bci/web/vue/src/main.js +++ b/bci/web/vue/src/main.js @@ -5,8 +5,8 @@ import 'flowbite' import 'axios' import { OhVueIcon, addIcons } from "oh-vue-icons"; -import { MdInfooutline, FaRegularEdit, FaLink } from "oh-vue-icons/icons"; +import { MdInfooutline, FaRegularEdit, FaLink, FaPlus } from "oh-vue-icons/icons"; -addIcons(MdInfooutline, FaRegularEdit, FaLink); +addIcons(MdInfooutline, FaRegularEdit, FaLink, FaPlus); const app = createApp(App); app.component("v-icon", OhVueIcon).mount('#app') diff --git a/bci/web/vue/src/style.css b/bci/web/vue/src/style.css index 713de6c3..12c7eb4c 100644 --- a/bci/web/vue/src/style.css +++ b/bci/web/vue/src/style.css @@ -3,7 +3,7 @@ @tailwind utilities; .button { - @apply text-white bg-blue-700 hover:bg-blue-800 active:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2.5 text-center items-center dark:hover:bg-blue-700 dark:focus:ring-blue-800 + @apply text-white bg-blue-700 hover:bg-blue-800 active:ring-4 focus:outline-none cursor-pointer font-medium rounded-lg text-sm px-4 py-2.5 text-center items-center dark:hover:bg-blue-700 dark:focus:ring-blue-800 } .dropdown-head { @@ -192,7 +192,7 @@ h1 { line-height: 1.1; } -button { +button:not(.no-style) { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; @@ -203,8 +203,8 @@ button { cursor: pointer; transition: border-color 0.25s; } -button:focus, -button:focus-visible { +button:not(.no-style):focus, +button:not(.no-style):focus-visible { outline: 4px auto -webkit-focus-ring-color; } From acdec3cf51e5c1e4b5f5e2dfc2993b924767789e Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 10:42:29 +0000 Subject: [PATCH 29/35] Adding config files through the frontend - Python part --- bci/evaluations/custom/custom_evaluation.py | 16 ++++++++++++++++ bci/main.py | 4 ++++ bci/web/blueprints/api.py | 14 ++++++++++++++ bci/web/vue/src/components/poc-editor.vue | 4 ++-- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 84517c9c..baa464a3 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -207,6 +207,22 @@ def add_page(self, project: str, poc: str, domain: str, path: str, file_type: st # Notify clients of change (an experiment might now be runnable) Clients.push_experiments_to_all() return True + + def add_config(self, project: str, poc: str, type: str) -> bool: + content = self.get_default_file_content(type) + + if (content == ''): + return False + + file_path = os.path.join(Global.custom_page_folder, project, poc, type) + with open(file_path, 'w') as file: + file.write(content) + + self.sync_with_folders() + # Notify clients of change (an experiment might now be runnable) + Clients.push_experiments_to_all() + + return True @staticmethod def get_default_file_content(file_type: str) -> str: diff --git a/bci/main.py b/bci/main.py index 127c1c29..fddb53ae 100644 --- a/bci/main.py +++ b/bci/main.py @@ -155,6 +155,10 @@ def get_available_domains() -> list[str]: @staticmethod def add_page(project: str, poc: str, domain: str, path: str, file_type: str) -> bool: return Main.master.evaluation_framework.add_page(project, poc, domain, path, file_type) + + @staticmethod + def add_config(project: str, poc: str, type: str) -> bool: + return Main.master.evaluation_framework.add_config(project, poc, type) @staticmethod def sigint_handler(signum, frame): diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py index 592fbcab..8c1094b8 100644 --- a/bci/web/blueprints/api.py +++ b/bci/web/blueprints/api.py @@ -208,6 +208,20 @@ def add_page(project: str, poc: str): } +@api.route('/poc///config', methods=['POST']) +def add_config(project: str, poc: str): + data = request.json.copy() + success = bci_api.add_config(project, poc, data['type']) + if success: + return { + 'status': 'OK' + } + else: + return { + 'status': 'NOK' + } + + @api.route('/poc/domain/', methods=['GET']) def get_available_domains(): return { diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index 78d2f526..45cffbed 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -47,8 +47,8 @@ 'py', ], available_config_types: { - 'interaction_script': 'Interaction script', - 'url_queue': 'URL queue' + 'interaction_script.cmd': 'Interaction script', + 'url_queue.txt': 'URL queue' }, poc_tree_config: { domain: 'Config', From 3ee4d20e19d748bcd2210b846966e8ea1deb6516 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 12:06:47 +0000 Subject: [PATCH 30/35] More interaction commands --- bci/browser/interaction/interaction.py | 14 ++++---- bci/browser/interaction/simulation.py | 35 ++++++++++++++++--- .../default_files/interaction_script.cmd | 17 +++++---- bci/web/vue/src/interaction_script_mode.js | 2 +- .../Support/AutoGUI/a.test/main/index.html | 6 +++- .../Support/AutoGUI/interaction_script.cmd | 19 ++++++++-- experiments/res/bughog.css | 2 +- 7 files changed, 71 insertions(+), 24 deletions(-) diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 5da2716d..4561fff5 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -22,32 +22,30 @@ def execute(self) -> None: simulation = Simulation(self.browser, self.params) self._interpret(simulation) + simulation.sleep('0.5') simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - simulation.sleep('0.5') def _interpret(self, simulation: Simulation) -> None: for statement in self.script: - if statement.strip() == '': + if statement.strip() == '' or statement[0] == '#': continue cmd, *args = statement.split() method_name = cmd.lower() - if cmd == '#': - continue - if method_name not in Simulation.public_methods: raise Exception( f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), Simulation.public_methods))}.' ) method = getattr(simulation, method_name) - method_params_len = len(signature(method).parameters) + method_params = list(signature(method).parameters.values()) - if method_params_len != len(args): + # Allow different number of arguments only for variable argument number (*) + if len(method_params) != len(args) and (len(method_params) < 1 or str(method_params[0])[0] != '*'): raise Exception( - f'Invalid number of arguments for command `{cmd}`. Expected {method_params_len}, got {len(args)}.' + f'Invalid number of arguments for command `{cmd}`. Expected {len(method_params)}, got {len(args)}.' ) logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py index da22069f..68da367e 100644 --- a/bci/browser/interaction/simulation.py +++ b/bci/browser/interaction/simulation.py @@ -13,7 +13,18 @@ class Simulation: browser_config: BrowserConfig params: TestParameters - public_methods: list[str] = ['navigate', 'click', 'click_el', 'sleep', 'screenshot'] + public_methods: list[str] = [ + 'navigate', + 'click_position', + 'click', + 'write', + 'press', + 'hold', + 'release', + 'hotkey', + 'sleep', + 'screenshot', + ] def __init__(self, browser_config: BrowserConfig, params: TestParameters): self.browser_config = browser_config @@ -37,17 +48,33 @@ def parse_position(self, position: str, max_value: int) -> int: def navigate(self, url: str): self.browser_config.terminate() self.browser_config.open(url) + self.sleep('0.5') - def click(self, x: str, y: str): + def click_position(self, x: str, y: str): max_x, max_y = gui.size() gui.moveTo(self.parse_position(x, max_x), self.parse_position(y, max_y)) gui.click() - def click_el(self, el_id: str): + def click(self, el_id: str): el_image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'elements/{el_id}.png') x, y = gui.locateCenterOnScreen(el_image_path) - self.click(str(x), str(y)) + self.click_position(str(x), str(y)) + + def write(self, text: str): + gui.write(text, interval=0.1) + + def press(self, key: str): + gui.press(key) + + def hold(self, key: str): + gui.keyDown(key) + + def release(self, key: str): + gui.keyUp(key) + + def hotkey(self, *keys: str): + gui.hotkey(*keys) def sleep(self, duration: str): sleep(float(duration)) diff --git a/bci/evaluations/custom/default_files/interaction_script.cmd b/bci/evaluations/custom/default_files/interaction_script.cmd index 59f6d22b..63ad6d9b 100644 --- a/bci/evaluations/custom/default_files/interaction_script.cmd +++ b/bci/evaluations/custom/default_files/interaction_script.cmd @@ -1,7 +1,12 @@ -# TODO - add your interaction script +# TODO - add your interaction script using the commands -# NAVIGATE https://a.test/Project/Experiment/main -# SLEEP 1 -# SCREENSHOT file_name -# CLICK_EL one -# SLEEP 1 \ No newline at end of file +# NAVIGATE url +# CLICK_POSITION x y where x and y are absolute numbers or screen percentages +# CLICK element_id where element_id is one of one, two, three, four, five, six +# WRITE text +# PRESS key +# HOLD key +# RELEASE key +# HOTKEY key1 key2 ... +# SLEEP seconds where seconds is a float or an int +# SCREENSHOT file_name \ No newline at end of file diff --git a/bci/web/vue/src/interaction_script_mode.js b/bci/web/vue/src/interaction_script_mode.js index 35785b44..4413d521 100644 --- a/bci/web/vue/src/interaction_script_mode.js +++ b/bci/web/vue/src/interaction_script_mode.js @@ -1,4 +1,4 @@ -const KEYWORDS = "NAVIGATE|CLICK|CLICK_EL|SLEEP|SCREENSHOT"; +const KEYWORDS = "NAVIGATE|CLICK_POSITION|CLICK|WRITE|PRESS|HOLD|RELEASE|HOTKEY|SLEEP|SCREENSHOT"; ace.define("ace/mode/interaction_script_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module){"use strict"; const oop = require("../lib/oop"); diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/experiments/pages/Support/AutoGUI/a.test/main/index.html index 70d626e3..e2b6d3c4 100644 --- a/experiments/pages/Support/AutoGUI/a.test/main/index.html +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -5,6 +5,10 @@ - CLICK +
    + + + +
    \ No newline at end of file diff --git a/experiments/pages/Support/AutoGUI/interaction_script.cmd b/experiments/pages/Support/AutoGUI/interaction_script.cmd index 895572c5..a9ed5d45 100644 --- a/experiments/pages/Support/AutoGUI/interaction_script.cmd +++ b/experiments/pages/Support/AutoGUI/interaction_script.cmd @@ -1,5 +1,18 @@ NAVIGATE https://a.test/Support/AutoGUI/main SLEEP 1 -SCREENSHOT testtest -CLICK_EL three -SLEEP 1 \ No newline at end of file +SCREENSHOT test + +CLICK one +WRITE AutoGUI +HOTKEY ctrl a +HOTKEY ctrl c + +CLICK two + +# Equivalent to HOTKEY ctrl v +HOLD ctrl +HOLD v +RELEASE v +RELEASE ctrl + +PRESS Enter \ No newline at end of file diff --git a/experiments/res/bughog.css b/experiments/res/bughog.css index 7cdbfa7d..fd01d4cd 100644 --- a/experiments/res/bughog.css +++ b/experiments/res/bughog.css @@ -1,7 +1,7 @@ #fullscreen, #one, #two, #three, #four, #five, #six { position: fixed; z-index: 10; - font-size: 0; + font-size: 1px; border: none; outline: none; resize: none; From 52211d03ee147b2f08f5a2f9a1ef7f0aae177fff Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 12:11:14 +0000 Subject: [PATCH 31/35] Styling fix --- bci/web/vue/src/style.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bci/web/vue/src/style.css b/bci/web/vue/src/style.css index 12c7eb4c..c52649c6 100644 --- a/bci/web/vue/src/style.css +++ b/bci/web/vue/src/style.css @@ -192,7 +192,7 @@ h1 { line-height: 1.1; } -button:not(.no-style) { +button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; @@ -203,11 +203,17 @@ button:not(.no-style) { cursor: pointer; transition: border-color 0.25s; } -button:not(.no-style):focus, -button:not(.no-style):focus-visible { +button:focus, +button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +button.no-style { + padding: 0; + background: transparent; + border: none; +} + .card { padding: 2em; } From 639441c6c4d7144609a2568ed759751f25d3ccb8 Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 13:25:22 +0000 Subject: [PATCH 32/35] Final interaction improvements --- bci/browser/configuration/browser.py | 4 ++++ bci/browser/configuration/chromium.py | 3 +++ bci/browser/configuration/firefox.py | 3 +++ bci/browser/interaction/interaction.py | 17 +++++++++++------ bci/browser/interaction/simulation.py | 2 +- .../Support/AutoGUI/a.test/main/index.html | 2 +- .../Support/AutoGUI/interaction_script.cmd | 4 ++-- experiments/res/bughog.css | 14 +++++++------- 8 files changed, 32 insertions(+), 17 deletions(-) diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py index 0247f28e..f6a9f2e7 100644 --- a/bci/browser/configuration/browser.py +++ b/bci/browser/configuration/browser.py @@ -100,6 +100,10 @@ def _get_executable_file_path(self) -> str: def _get_terminal_args(self) -> list[str]: pass + @abstractmethod + def get_navigation_sleep_duration(self) -> int: + pass + @staticmethod def get_browser( browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py index 6108328d..f2cd16cb 100644 --- a/bci/browser/configuration/chromium.py +++ b/bci/browser/configuration/chromium.py @@ -32,6 +32,9 @@ class Chromium(Browser): + def get_navigation_sleep_duration(self) -> int: + return 1 + def _get_terminal_args(self) -> list[str]: assert self._profile_path is not None diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py index 4310bf20..cff1d7f1 100644 --- a/bci/browser/configuration/firefox.py +++ b/bci/browser/configuration/firefox.py @@ -20,6 +20,9 @@ class Firefox(Browser): + def get_navigation_sleep_duration(self) -> int: + return 2 + def _get_terminal_args(self) -> list[str]: assert self._profile_path is not None diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py index 4561fff5..14fb8d1b 100644 --- a/bci/browser/interaction/interaction.py +++ b/bci/browser/interaction/interaction.py @@ -21,12 +21,11 @@ def __init__(self, browser: BrowserConfig, script: list[str], params: TestParame def execute(self) -> None: simulation = Simulation(self.browser, self.params) - self._interpret(simulation) - simulation.sleep('0.5') + if self._interpret(simulation): + simulation.sleep(str(self.browser.get_navigation_sleep_duration())) + simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') - - def _interpret(self, simulation: Simulation) -> None: + def _interpret(self, simulation: Simulation) -> bool: for statement in self.script: if statement.strip() == '' or statement[0] == '#': continue @@ -49,4 +48,10 @@ def _interpret(self, simulation: Simulation) -> None: ) logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') - method(*args) + + try: + method(*args) + except: + return False + + return True diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py index 68da367e..d4ba899c 100644 --- a/bci/browser/interaction/simulation.py +++ b/bci/browser/interaction/simulation.py @@ -48,7 +48,7 @@ def parse_position(self, position: str, max_value: int) -> int: def navigate(self, url: str): self.browser_config.terminate() self.browser_config.open(url) - self.sleep('0.5') + self.sleep(str(self.browser_config.get_navigation_sleep_duration())) def click_position(self, x: str, y: str): max_x, max_y = gui.size() diff --git a/experiments/pages/Support/AutoGUI/a.test/main/index.html b/experiments/pages/Support/AutoGUI/a.test/main/index.html index e2b6d3c4..93eb4fc7 100644 --- a/experiments/pages/Support/AutoGUI/a.test/main/index.html +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -5,7 +5,7 @@ -
    + diff --git a/experiments/pages/Support/AutoGUI/interaction_script.cmd b/experiments/pages/Support/AutoGUI/interaction_script.cmd index a9ed5d45..91d6f21f 100644 --- a/experiments/pages/Support/AutoGUI/interaction_script.cmd +++ b/experiments/pages/Support/AutoGUI/interaction_script.cmd @@ -1,12 +1,12 @@ NAVIGATE https://a.test/Support/AutoGUI/main -SLEEP 1 -SCREENSHOT test +SCREENSHOT click1 CLICK one WRITE AutoGUI HOTKEY ctrl a HOTKEY ctrl c +SCREENSHOT click2 CLICK two # Equivalent to HOTKEY ctrl v diff --git a/experiments/res/bughog.css b/experiments/res/bughog.css index fd01d4cd..8fe70435 100644 --- a/experiments/res/bughog.css +++ b/experiments/res/bughog.css @@ -30,14 +30,14 @@ #two { background-color: #dcbeff; color: #dcbeff; - top: 45px; + top: 70px; left: 10px; } #three { background-color: #ffe119; color: #ffe119; - top: 80px; + top: 130px; left: 10px; } @@ -45,19 +45,19 @@ background-color: #4363d8; color: #4363d8; top: 10px; - left: 45px; + left: 85px; } #five { background-color: #f58231; color: #f58231; - top: 45px; - left: 45px; + top: 70px; + left: 85px; } #six { background-color: #000075; color: #000075; - top: 80px; - left: 45px; + top: 130px; + left: 85px; } \ No newline at end of file From 8e4af952e4915460c239a95e450b4060087f255d Mon Sep 17 00:00:00 2001 From: Jakub Szymsza Date: Fri, 1 Nov 2024 13:29:05 +0000 Subject: [PATCH 33/35] Remove old experiment --- .../pages/Support/UserInteraction/a.test/main/headers.json | 1 - .../pages/Support/UserInteraction/a.test/main/index.html | 6 ------ .../pages/Support/UserInteraction/interaction_script.cmd | 1 - 3 files changed, 8 deletions(-) delete mode 100644 experiments/pages/Support/UserInteraction/a.test/main/headers.json delete mode 100644 experiments/pages/Support/UserInteraction/a.test/main/index.html delete mode 100644 experiments/pages/Support/UserInteraction/interaction_script.cmd diff --git a/experiments/pages/Support/UserInteraction/a.test/main/headers.json b/experiments/pages/Support/UserInteraction/a.test/main/headers.json deleted file mode 100644 index fe51488c..00000000 --- a/experiments/pages/Support/UserInteraction/a.test/main/headers.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/experiments/pages/Support/UserInteraction/a.test/main/index.html b/experiments/pages/Support/UserInteraction/a.test/main/index.html deleted file mode 100644 index ad482c63..00000000 --- a/experiments/pages/Support/UserInteraction/a.test/main/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - CLICK - - \ No newline at end of file diff --git a/experiments/pages/Support/UserInteraction/interaction_script.cmd b/experiments/pages/Support/UserInteraction/interaction_script.cmd deleted file mode 100644 index 5daa56cf..00000000 --- a/experiments/pages/Support/UserInteraction/interaction_script.cmd +++ /dev/null @@ -1 +0,0 @@ -NAVIGATE https://a.test/Support/UserInteraction/main From a2e61c2b4b1fd2e5de940159b2762fff2929278d Mon Sep 17 00:00:00 2001 From: Gertjan Date: Wed, 13 Nov 2024 11:17:54 +0000 Subject: [PATCH 34/35] Fix button to add script --- bci/web/vue/src/components/poc-editor.vue | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index ff9afb5b..a750fb01 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -232,6 +232,14 @@ .catch(() => { }); + }, + has_config() { + if (this.active_poc.tree === undefined) { + console.error("Could not check config in undefined poc tree"); + return false; + } + const arr = Object.keys(this.active_poc.tree).filter((domain) => ['url_queue.txt', 'interaction_script.cmd'].includes(domain)); + return arr.length > 0; } }, mounted() { @@ -292,9 +300,6 @@ -
    -
  • - Add page -
  • +
    +
  • + Add page +
  • +
  • + Add script +
  • +
From 792447e4384be3164169f85bc12f35c9b75efe14 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Wed, 13 Nov 2024 12:10:55 +0000 Subject: [PATCH 35/35] Rename interaction_script.cmd to script.md and small button fix --- bci/evaluations/custom/custom_evaluation.py | 10 ++--- .../custom/default_files/script.cmd | 12 ++++++ bci/web/vue/src/components/poc-editor.vue | 41 +++++++++++-------- .../{interaction_script.cmd => script.cmd} | 2 +- .../Support/test}/interaction_script.cmd | 4 +- 5 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 bci/evaluations/custom/default_files/script.cmd rename experiments/pages/Support/AutoGUI/{interaction_script.cmd => script.cmd} (94%) rename {bci/evaluations/custom/default_files => experiments/pages/Support/test}/interaction_script.cmd (79%) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index 085abf41..e4e9ff5c 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -57,7 +57,7 @@ def initialize_tests_and_interactions(dir_tree: dict) -> dict: data = {} if interaction_script := CustomEvaluationFramework.__get_interaction_script(project_path, experiment): - data['interaction_script'] = interaction_script + data['script'] = interaction_script elif url_queue := CustomEvaluationFramework.__get_url_queue(project, project_path, experiment): data['url_queue'] = url_queue @@ -68,7 +68,7 @@ def initialize_tests_and_interactions(dir_tree: dict) -> dict: @staticmethod def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: - interaction_file_path = os.path.join(project_path, experiment, 'interaction_script.cmd') + interaction_file_path = os.path.join(project_path, experiment, 'script.cmd') if os.path.isfile(interaction_file_path): # If an interaction script is specified, it is parsed and used with open(interaction_file_path) as file: @@ -97,7 +97,7 @@ def __get_url_queue(project: str, project_path: str, experiment: str) -> Optiona @staticmethod def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str,dict], data: dict[str,str]) -> bool: # Always runnable if there is either an interaction script or url_queue present - if 'interaction_script' in data or 'url_queue' in data: + if 'script' in data or 'url_queue' in data: return True # Should have exactly one main folder otherwise @@ -123,8 +123,8 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters) experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group] max_tries = 3 - if 'interaction_script' in experiment: - interaction = Interaction(browser, experiment['interaction_script'], params) + if 'script' in experiment: + interaction = Interaction(browser, experiment['script'], params) tries = 0 while tries < max_tries: tries += 1 diff --git a/bci/evaluations/custom/default_files/script.cmd b/bci/evaluations/custom/default_files/script.cmd new file mode 100644 index 00000000..749e7a21 --- /dev/null +++ b/bci/evaluations/custom/default_files/script.cmd @@ -0,0 +1,12 @@ +# TODO - add your interaction script using the commands + +# NAVIGATE url +# CLICK_POSITION x y where x and y are absolute numbers or screen percentages +# CLICK element_id where element_id is one of one, two, three, four, five, six +# WRITE text +# PRESS key +# HOLD key +# RELEASE key +# HOTKEY key1 key2 ... +# SLEEP seconds where seconds is a float or an int +# SCREENSHOT file_name diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index a750fb01..167e0175 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -31,14 +31,24 @@ // Example tree: // { // 'test.com': { - // "path1": ["file1", "file2"], - // "path2": ["file1"], + // "path1": { + // "file1": null, + // "file2": null + // }, + // "path2": { + // "file1": null + // }, // }, // 'not.test': { - // "path1": ["file1", "file2"], - // "path2": ["file1"], + // "path1": { + // "file1": null, + // "file2": null + // }, + // "path2": { + // "file1": null + // }, // }, - // 'interaction_script.cmd': 'interaction_script.cmd', + // 'script.cmd': null, // }, }, available_file_types: [ @@ -47,7 +57,7 @@ 'py', ], available_config_types: { - 'interaction_script.cmd': 'Interaction script', + 'script.cmd': 'Interaction script', 'url_queue.txt': 'URL queue' }, poc_tree_config: { @@ -238,7 +248,7 @@ console.error("Could not check config in undefined poc tree"); return false; } - const arr = Object.keys(this.active_poc.tree).filter((domain) => ['url_queue.txt', 'interaction_script.cmd'].includes(domain)); + const arr = Object.keys(this.active_poc.tree).filter((domain) => ['url_queue.txt', 'script.cmd'].includes(domain)); return arr.length > 0; } }, @@ -277,17 +287,12 @@