diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8687890..16cc9c7 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": {}, } diff --git a/.gitignore b/.gitignore index e1bddf0..3732e54 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,17 @@ nginx/ssl/keys/* !**/.gitkeep **/node_modules **/junit.xml + +# Screenshots +logs/screenshots/* +!logs/screenshots/.gitkeep + +# Fish shell +$HOME + +# JetBrains IDEs +.idea + # Created by https://www.toptal.com/developers/gitignore/api/intellij,python,flask,macos # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,python,flask,macos diff --git a/Dockerfile b/Dockerfile index b7bd102..9439b3d 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/analysis/plot_factory.py b/bci/analysis/plot_factory.py index 84a0f04..7fa5173 100644 --- a/bci/analysis/plot_factory.py +++ b/bci/analysis/plot_factory.py @@ -5,14 +5,14 @@ class PlotFactory: @staticmethod - def get_plot_revision_data(params: PlotParameters, db: MongoDB) -> dict: - revision_docs = db.get_documents_for_plotting(params) + def get_plot_revision_data(params: PlotParameters) -> dict: + revision_docs = MongoDB().get_documents_for_plotting(params) revision_results = PlotFactory.__add_outcome_info(params, revision_docs) return revision_results @staticmethod - def get_plot_version_data(params: PlotParameters, db: MongoDB) -> dict: - version_docs = db.get_documents_for_plotting(params, releases=True) + def get_plot_version_data(params: PlotParameters) -> dict: + version_docs = MongoDB().get_documents_for_plotting(params, releases=True) version_results = PlotFactory.__add_outcome_info(params, version_docs) return version_results diff --git a/bci/app.py b/bci/app.py index 9e504cb..5f037be 100644 --- a/bci/app.py +++ b/bci/app.py @@ -3,12 +3,20 @@ from flask import Flask from flask_sock import Sock -from bci.main import Main as bci_api +from bci.configuration import Global, Loggers +from bci.main import Main sock = Sock() + def create_app(): - bci_api.initialize() + Loggers.configure_loggers() + + if not Global.check_required_env_parameters(): + raise Exception('Not all required environment variables are available') + + # Instantiate main object and add to global flask context + main = Main() # Blueprint modules are only imported after loggers are configured from bci.web.blueprints.api import api @@ -16,6 +24,7 @@ def create_app(): app = Flask(__name__) # We don't store anything sensitive in the session, so we can use a simple secret key + app.config['main'] = main app.secret_key = 'secret_key' app.register_blueprint(api) @@ -23,8 +32,8 @@ def create_app(): sock.init_app(app) # Configure signal handlers - signal.signal(signal.SIGTERM, bci_api.sigint_handler) - signal.signal(signal.SIGINT, bci_api.sigint_handler) + signal.signal(signal.SIGTERM, main.sigint_handler) + signal.signal(signal.SIGINT, main.sigint_handler) return app diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py index d1fe6f7..ddc08b8 100644 --- a/bci/browser/automation/terminal.py +++ b/bci/browser/automation/terminal.py @@ -7,22 +7,26 @@ 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]) -> subprocess.Popen: + 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) + return proc - 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 +34,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 1178ece..6b0c86d 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,9 +16,13 @@ class Browser: + process: subprocess.Popen | None - 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.process = None self.eval_config = eval_config self.binary = binary self.state = binary.state @@ -34,10 +39,22 @@ 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, url: str) -> None: + args = self._get_terminal_args() + args.append(url) + self.process = TerminalAutomation.open_browser(args) + + 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() @@ -46,11 +63,16 @@ def post_evaluation_cleanup(self): def pre_test_setup(self): self.__prepare_execution_folder() - self._prepare_profile_folder() def post_test_cleanup(self): self.__remove_execution_folder() + + def pre_try_setup(self): + self._prepare_profile_folder() + + def post_try_cleanup(self): self.__remove_profile_folder() + self.__empty_downloads_folder() def __fetch_binary(self): self.binary.fetch_binary() @@ -70,9 +92,15 @@ def __remove_execution_folder(self): util.rmtree(self.__get_execution_folder_path()) def __remove_profile_folder(self): + if self._profile_path is None: + return remove_profile_execution_folder(self._profile_path) self._profile_path = None + def __empty_downloads_folder(self): + download_folder = '/root/Downloads' + util.remove_all_in_folder(download_folder) + def __get_execution_folder_path(self) -> str: return os.path.join(EXECUTION_PARENT_FOLDER, str(self.state.name)) @@ -80,11 +108,17 @@ 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 + + @abstractmethod + def get_navigation_sleep_duration(self) -> int: 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 6108328..f2cd16c 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 643ae57..239934a 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(), @@ -21,11 +20,15 @@ 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 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): @@ -45,6 +48,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: @@ -92,7 +96,7 @@ def _prepare_profile_folder(self): if 'tp' in self.browser_config.browser_setting: self._profile_path = prepare_firefox_profile('tp-67') else: - self._profile_path = prepare_firefox_profile('default-67') + self._profile_path = prepare_firefox_profile() # Make Firefox trust the bughog CA diff --git a/bci/browser/configuration/profile.py b/bci/browser/configuration/profile.py index 520b612..79e03d1 100644 --- a/bci/browser/configuration/profile.py +++ b/bci/browser/configuration/profile.py @@ -1,4 +1,5 @@ import os +from typing import Optional from bci import cli @@ -6,7 +7,7 @@ PROFILE_EXECUTION_FOLDER = '/tmp/profiles' -def prepare_chromium_profile(profile_name: str = None) -> str: +def prepare_chromium_profile(profile_name: Optional[str] = None) -> str: # Create new execution profile folder profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile') profile_execution_path = __create_folder(profile_execution_path) @@ -14,21 +15,26 @@ def prepare_chromium_profile(profile_name: str = None) -> str: # Copy profile from storage to execution folder if profile_name is given if profile_name: if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'chromium', profile_name)): - raise Exception(f'Profile \'{profile_name}\' does not exist') + raise Exception(f"Profile '{profile_name}' does not exist") profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name, 'Default') cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}') return profile_execution_path -def prepare_firefox_profile(profile_name: str = None) -> str: +def prepare_firefox_profile(profile_name: Optional[str] = None) -> str: + """" + Create a temporary profile folder, based on the provided name of the premade profile. + + :param profile_name: Reference to the premade profile folder used for creating the temporary profile. + """ # Create new execution profile folder profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile') profile_execution_path = __create_folder(profile_execution_path) # Copy profile from storage to execution folder if profile_name is given - if profile_name is None: + if profile_name is not None: if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'firefox', profile_name)): - raise Exception(f'Profile \'{profile_name}\' does not exist') + raise Exception(f"Profile '{profile_name}' does not exist") profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name) cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}') return profile_execution_path diff --git a/bci/browser/interaction/__init__.py b/bci/browser/interaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bci/browser/interaction/elements/five.png b/bci/browser/interaction/elements/five.png new file mode 100644 index 0000000..92dfde6 Binary files /dev/null and b/bci/browser/interaction/elements/five.png differ diff --git a/bci/browser/interaction/elements/four.png b/bci/browser/interaction/elements/four.png new file mode 100644 index 0000000..055ece5 Binary files /dev/null and b/bci/browser/interaction/elements/four.png differ diff --git a/bci/browser/interaction/elements/one.png b/bci/browser/interaction/elements/one.png new file mode 100644 index 0000000..c87a2d0 Binary files /dev/null and b/bci/browser/interaction/elements/one.png differ diff --git a/bci/browser/interaction/elements/six.png b/bci/browser/interaction/elements/six.png new file mode 100644 index 0000000..4adc77b Binary files /dev/null and b/bci/browser/interaction/elements/six.png differ diff --git a/bci/browser/interaction/elements/three.png b/bci/browser/interaction/elements/three.png new file mode 100644 index 0000000..4368269 Binary files /dev/null and b/bci/browser/interaction/elements/three.png differ diff --git a/bci/browser/interaction/elements/two.png b/bci/browser/interaction/elements/two.png new file mode 100644 index 0000000..7f3c24a Binary files /dev/null and b/bci/browser/interaction/elements/two.png differ diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py new file mode 100644 index 0000000..14fb8d1 --- /dev/null +++ b/bci/browser/interaction/interaction.py @@ -0,0 +1,57 @@ +import logging +from inspect import signature + +from bci.browser.configuration.browser import Browser as BrowserConfig +from bci.browser.interaction.simulation import Simulation +from bci.evaluations.logic import TestParameters + +logger = logging.getLogger(__name__) + + +class Interaction: + browser: BrowserConfig + script: list[str] + params: TestParameters + + def __init__(self, browser: BrowserConfig, script: list[str], params: TestParameters) -> None: + self.browser = browser + self.script = script + self.params = params + + def execute(self) -> None: + simulation = Simulation(self.browser, self.params) + + if self._interpret(simulation): + simulation.sleep(str(self.browser.get_navigation_sleep_duration())) + simulation.navigate('https://a.test/report/?bughog_sanity_check=OK') + + def _interpret(self, simulation: Simulation) -> bool: + for statement in self.script: + if statement.strip() == '' or statement[0] == '#': + continue + + cmd, *args = statement.split() + method_name = cmd.lower() + + 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 = list(signature(method).parameters.values()) + + # 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 {len(method_params)}, got {len(args)}.' + ) + + logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}') + + try: + method(*args) + except: + return False + + return True diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py new file mode 100644 index 0000000..d4ba899 --- /dev/null +++ b/bci/browser/interaction/simulation.py @@ -0,0 +1,85 @@ +import os +from time import sleep + +import pyautogui as gui +import Xlib.display +from pyvirtualdisplay.display import Display + +from bci.browser.configuration.browser import Browser as BrowserConfig +from bci.evaluations.logic import TestParameters + + +class Simulation: + browser_config: BrowserConfig + params: TestParameters + + 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 + self.params = params + disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True) + disp.start() + gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY']) + + def __del__(self): + self.browser_config.terminate() + + def parse_position(self, position: str, max_value: int) -> int: + # Screen percentage + if position[-1] == '%': + return round(max_value * (int(position[:-1]) / 100)) + + # Absolute value in pixels + return int(position) + + # --- PUBLIC METHODS --- + def navigate(self, url: str): + self.browser_config.terminate() + self.browser_config.open(url) + self.sleep(str(self.browser_config.get_navigation_sleep_duration())) + + 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(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_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)) + + def screenshot(self, filename: str): + filename = f'{self.params.evaluation_configuration.project}-{self.params.mech_group}-{filename}-{type(self.browser_config).__name__}-{self.browser_config.version}.jpg' + filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../logs/screenshots', filename) + gui.screenshot(filepath) diff --git a/bci/configuration.py b/bci/configuration.py index c8472c0..166e0e0 100644 --- a/bci/configuration.py +++ b/bci/configuration.py @@ -170,5 +170,14 @@ def handle_exception(exc_type, exc_value, exc_traceback): bci_logger.debug('Loggers initialized') @staticmethod - def get_formatted_buffer_logs() -> list[str]: - return [Loggers.formatter.format(record) for record in Loggers.memory_handler.buffer] + def get_logs() -> list[str]: + return list( + map( + lambda x: Loggers.format_to_user_log(x.__dict__), + Loggers.memory_handler.buffer, + ) + ) + + @staticmethod + def format_to_user_log(log: dict) -> str: + return f'[{log["asctime"]}] [{log["levelname"]}] {log["name"]}: {log["msg"]}' diff --git a/bci/database/mongo/mongodb.py b/bci/database/mongo/mongodb.py index 6b87583..df8c567 100644 --- a/bci/database/mongo/mongodb.py +++ b/bci/database/mongo/mongodb.py @@ -18,7 +18,6 @@ StateResult, TestParameters, TestResult, - WorkerParameters, ) from bci.evaluations.outcome_checker import OutcomeChecker from bci.version_control.states.state import State, StateCondition @@ -178,7 +177,7 @@ def has_result(self, params: TestParameters) -> bool: def get_evaluated_states( self, params: EvaluationParameters, boundary_states: tuple[State, State], outcome_checker: OutcomeChecker ) -> list[State]: - collection = self.get_collection(params.database_collection) + collection = self.get_collection(params.database_collection, create_if_not_found=True) query = { 'browser_config': params.browser_configuration.browser_setting, 'mech_group': params.evaluation_range.mech_group, @@ -308,7 +307,7 @@ def get_build_id_firefox(self, state: State): return result['build_id'] def get_documents_for_plotting(self, params: PlotParameters, releases: bool = False): - collection = self.get_collection(params.database_collection) + collection = self.get_collection(params.database_collection, create_if_not_found=True) query = { 'mech_group': params.mech_group, 'browser_config': params.browser_config, @@ -340,6 +339,11 @@ def get_documents_for_plotting(self, params: PlotParameters, releases: bool = Fa ) return list(docs) + def remove_datapoint(self, params: TestParameters) -> None: + collection = self.get_collection(params.database_collection) + query = self.__to_query(params) + collection.delete_one(query) + def get_info(self) -> dict: if self.client and self.client.address: return {'type': 'mongo', 'host': self.client.address[0], 'connected': True} diff --git a/bci/database/mongo/revision_cache.py b/bci/database/mongo/revision_cache.py index 08cb5d4..6a06efd 100644 --- a/bci/database/mongo/revision_cache.py +++ b/bci/database/mongo/revision_cache.py @@ -44,7 +44,7 @@ def firefox_has_binary_for(revision_nb: Optional[int], revision_id: Optional[str @staticmethod def firefox_get_binary_info(revision_id: str) -> Optional[dict]: collection = MongoDB().get_collection('firefox_binary_availability') - return collection.find_one({'revision_id': revision_id}, {'files_url': 1, 'app_version': 1}) + return collection.find_one({'node': revision_id}, {'files_url': 1, 'app_version': 1}) @staticmethod def firefox_get_previous_and_next_revision_nb_with_binary(revision_nb: int) -> tuple[Optional[int], Optional[int]]: diff --git a/bci/evaluations/collectors/base.py b/bci/evaluations/collectors/base.py index 903ed5d..b85ee6f 100644 --- a/bci/evaluations/collectors/base.py +++ b/bci/evaluations/collectors/base.py @@ -3,23 +3,22 @@ class BaseCollector: - def __init__(self) -> None: self.data = {} @abstractmethod - def start(): + def start(self): pass @abstractmethod - def stop(): + def stop(self): pass @staticmethod def _parse_bughog_variables(raw_log_lines: list[str], regex) -> list[tuple[str, str]]: - ''' + """ Parses the given `raw_log_lines` for matches against the given `regex`. - ''' + """ data = [] regex_match_lists = [re.findall(regex, line) for line in raw_log_lines if re.search(regex, line)] # Flatten list @@ -27,15 +26,5 @@ def _parse_bughog_variables(raw_log_lines: list[str], regex) -> list[tuple[str, for match in regex_matches: var = match[0] val = match[1] - BaseCollector._add_val_var_pair(var, val, data) + data.append({'var': var, 'val': val}) return data - - @staticmethod - def _add_val_var_pair(var: str, val: str, data: list) -> list: - for entry in data: - if entry['var'] == var and entry['val'] == val: - return data - data.append({ - 'var': var, - 'val': val - }) diff --git a/bci/evaluations/collectors/requests.py b/bci/evaluations/collectors/requests.py index d5b9efe..db99803 100644 --- a/bci/evaluations/collectors/requests.py +++ b/bci/evaluations/collectors/requests.py @@ -1,6 +1,7 @@ import http.server import json import logging +import socket import socketserver from threading import Thread @@ -12,32 +13,49 @@ class RequestHandler(http.server.BaseHTTPRequestHandler): + """ + Handles requests sent to the collector. + """ def __init__(self, collector, request, client_address, server) -> None: self.collector = collector self.request_body = None super().__init__(request, client_address, server) - def log_message(self, *_): + def log_message(self, format: str, *args) -> None: + """ + Handle and store the received body. + """ if not self.request_body: logger.debug('Received request without body') return - logger.debug(f'Received request with body: {self.request_body}') request_body = json.loads(self.request_body) + logger.debug(f'Received request information with {len(request_body.keys())} attributes.') self.collector.data['requests'].append(request_body) def do_POST(self): - content_length = int(self.headers['Content-Length']) - body = self.rfile.read(content_length) - self.request_body = body.decode('utf-8') - self.send_response(200) - self.end_headers() - self.wfile.write(b'Post request received') + """ + This function is called upon receiving a POST request. + It sets `self.request_body`, which will be parsed later by `self.log_message`. + """ + # We have to read the body before allowing it to be thrashed when connection clusure is confirmed. + if self.headers['Content-Length'] is not None: + content_length = int(self.headers['Content-Length']) + body = self.rfile.read(content_length) + self.request_body = body.decode('utf-8') + # Because of our hacky NGINX methodology, we have to allow premature socket closings. + try: + self.send_response(200) + self.send_header('Content-Type', 'text/plain; charset=utf-8') + self.end_headers() + self.wfile.write('Post request received!\n'.encode('utf-8')) + except socket.error: + logger.debug('Socket closed by NGINX (expected)') -class RequestCollector(BaseCollector): +class RequestCollector(BaseCollector): def __init__(self): super().__init__() self.__httpd = None @@ -48,17 +66,20 @@ def __init__(self): def start(self): logger.debug('Starting collector...') socketserver.TCPServer.allow_reuse_address = True - self.__httpd = socketserver.TCPServer(("", PORT), lambda *args, **kwargs: RequestHandler(self, *args, **kwargs)) + self.__httpd = socketserver.TCPServer(('', PORT), lambda *args, **kwargs: RequestHandler(self, *args, **kwargs)) # self.__httpd.allow_reuse_address = True self.__thread = Thread(target=self.__httpd.serve_forever) self.__thread.start() def stop(self): data = [] - regex = r'bughog_(.+)=(.+)' + # Important: we only consider requests to the /report/ endpoint where the bughog parameter immediately follows. + # Otherwise conditional endpoints (e.g., /report/if/Referer/) cause false positives. + regex = r'/report/\?bughog_(.+)=(.+)' if self.__httpd: self.__httpd.shutdown() - self.__thread.join() + if self.__thread: + self.__thread.join() self.__httpd.server_close() request_urls = [request['url'] for request in self.data['requests'] if 'url' in request] data = self._parse_bughog_variables(request_urls, regex) diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py index b359c03..96a441b 100644 --- a/bci/evaluations/custom/custom_evaluation.py +++ b/bci/evaluations/custom/custom_evaluation.py @@ -1,8 +1,9 @@ import logging import os -import textwrap +from typing import Optional 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,26 +17,36 @@ 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: + """ + Initializes directory tree of experiments. + """ path = Global.custom_page_folder + dir_tree = {} - 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' - } + def set_nested_value(d: dict, keys: list[str], value: dict): + nested_dict = d + for key in keys[:-1]: + nested_dict = nested_dict[key] + nested_dict[keys[-1]] = value + + for root, dirs, files in os.walk(path): + # Remove base path from root + root = root[len(path):] + keys = root.split('/')[1:] + subdir_tree = {dir: {} for dir in dirs} | {file: None for file in files} + if root: + set_nested_value(dir_tree, keys, subdir_tree) else: - return os.path.basename(path) + dir_tree = subdir_tree - return path_to_dict(path) + return dir_tree @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,15 +54,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), - } + data = {} + + if interaction_script := CustomEvaluationFramework.__get_interaction_script(project_path, experiment): + data['script'] = interaction_script + elif 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, data) + + experiments_per_project[project][experiment] = data return experiments_per_project @staticmethod - def __get_url_queue(project: str, project_path: str, experiment: str) -> list[str]: + def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None: + 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: + return file.readlines() + return None + + @staticmethod + def __get_url_queue(project: str, project_path: str, experiment: str) -> Optional[list[str]]: url_queue_file_path = os.path.join(project_path, experiment, 'url_queue.txt') if os.path.isfile(url_queue_file_path): # If an URL queue is specified, it is parsed and used @@ -67,14 +92,21 @@ def __get_url_queue(project: str, project_path: str, experiment: str) -> list[st f'https://{domain}/{project}/{experiment}/main', 'https://a.test/report/?bughog_sanity_check=OK', ] - raise AttributeError(f"Could not infer url queue for experiment '{experiment}' in project '{project}'") + return None @staticmethod - def is_runnable_experiment(project: str, poc: str, dir_tree: dict) -> bool: + 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 'script' in data or 'url_queue' in data: + return True + + # Should have exactly one main folder otherwise domains = dir_tree[project][poc] - if not (poc_main_path := [paths for domain, paths in domains.items() if 'main' in paths]): + main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()] + if len(main_paths) != 1: return False - if 'index.html' not in poc_main_path[0]['main'].keys(): + # Main should have index.html + if 'index.html' not in main_paths[0]['main'].keys(): return False return True @@ -88,17 +120,25 @@ 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] + + max_tries = 3 + for _ in range(max_tries): + browser.pre_try_setup() + if 'script' in experiment: + interaction = Interaction(browser, experiment['script'], params) + interaction.execute() + else: + url_queue = experiment['url_queue'] + for url in url_queue: + browser.visit(url) + browser.post_try_cleanup() except Exception as e: logger.error(f'Error during test: {e}', exc_info=True) is_dirty = True finally: collector.stop() + results = collector.collect_results() if not is_dirty: # New way to perform sanity check @@ -124,18 +164,39 @@ def get_mech_groups(self, project: str) -> list[tuple[str, bool]]: def get_projects(self) -> list[str]: return sorted(list(self.tests_per_project.keys())) + def create_empty_project(self, project_name: str) -> bool: + if project_name is None or project_name == '': + logger.error('Given project name is invalid') + return False + + if project_name in self.dir_tree: + logger.error(f"Given project name '{project_name}' already exists") + return False + + new_project_path = os.path.join(Global.custom_page_folder, project_name) + os.mkdir(new_project_path) + self.sync_with_folders() + return True + 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') @@ -158,36 +219,63 @@ def add_page(self, project: str, poc: str, domain: str, path: str, file_type: st domain_path = os.path.join(Global.custom_page_folder, project, poc, domain) if not os.path.exists(domain_path): os.makedirs(domain_path) - page_path = os.path.join(domain_path, path) - if not os.path.exists(page_path): - os.makedirs(page_path) - new_file_name = f'index.{file_type}' - file_path = os.path.join(page_path, new_file_name) + + if file_type == 'py': + file_name = path if path.endswith('.py') else path + '.py' + file_path = os.path.join(domain_path, file_name) + else: + page_path = os.path.join(domain_path, path) + if not os.path.exists(page_path): + os.makedirs(page_path) + new_file_name = f'index.{file_type}' + file_path = os.path.join(page_path, new_file_name) + if os.path.exists(file_path): return False with open(file_path, 'w') as file: - file.write('') - headers_file_path = os.path.join(page_path, 'headers.json') - if not os.path.exists(headers_file_path): - with open(headers_file_path, 'w') as file: - file.write( - textwrap.dedent( - """\ - [ - { - "key": "Header-Name", - "value": "Header-Value" - } - ] - """ - ) - ) + file.write(self.get_default_file_content(file_type)) + + if self.include_file_headers(file_type): + headers_file_path = os.path.join(page_path, 'headers.json') + if not os.path.exists(headers_file_path): + with open(headers_file_path, 'w') as file: + file.write(self.get_default_file_content('headers.json')) self.sync_with_folders() # 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: + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'default_files/{file_type}') + + if not os.path.exists(path): + return '' + + with open(path, 'r') as file: + return file.read() + + @staticmethod + def include_file_headers(file_type: str) -> bool: + return file_type != 'py' + 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/bci/evaluations/custom/default_files/headers.json b/bci/evaluations/custom/default_files/headers.json new file mode 100644 index 0000000..81975b3 --- /dev/null +++ b/bci/evaluations/custom/default_files/headers.json @@ -0,0 +1,6 @@ +[ + { + "key": "Header-Name", + "value": "Header-Value" + } +] \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/html b/bci/evaluations/custom/default_files/html new file mode 100644 index 0000000..2af8232 --- /dev/null +++ b/bci/evaluations/custom/default_files/html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/js b/bci/evaluations/custom/default_files/js new file mode 100644 index 0000000..664997c --- /dev/null +++ b/bci/evaluations/custom/default_files/js @@ -0,0 +1 @@ +// TODO - implement your PoC \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/py b/bci/evaluations/custom/default_files/py new file mode 100644 index 0000000..91e823b --- /dev/null +++ b/bci/evaluations/custom/default_files/py @@ -0,0 +1,15 @@ +from flask import Request + +# Make sure that your page directory starts with 'py-' + +def main(req: Request): + # TODO - implement your functionality and return a Flask response + + return { + "agent": req.headers.get("User-Agent"), + "cookies": req.cookies, + "host": req.host, + "path": req.path, + "scheme": req.scheme, + "url": req.url + } \ No newline at end of file diff --git a/bci/evaluations/custom/default_files/script.cmd b/bci/evaluations/custom/default_files/script.cmd new file mode 100644 index 0000000..749e7a2 --- /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/evaluations/custom/default_files/url_queue.txt b/bci/evaluations/custom/default_files/url_queue.txt new file mode 100644 index 0000000..8918d8d --- /dev/null +++ b/bci/evaluations/custom/default_files/url_queue.txt @@ -0,0 +1,2 @@ +TODO - add your URLs to visit +https://a.test/report/?bughog_sanity_check=OK \ No newline at end of file diff --git a/bci/evaluations/logic.py b/bci/evaluations/logic.py index ad6f0e8..3097c59 100644 --- a/bci/evaluations/logic.py +++ b/bci/evaluations/logic.py @@ -190,6 +190,16 @@ class TestParameters: def create_test_result_with(self, browser_version: str, binary_origin: str, data: dict, dirty: bool) -> TestResult: return TestResult(self, browser_version, binary_origin, data, dirty) + @staticmethod + def from_dict(data) -> Optional[TestParameters]: + if data is None: + return None + browser_configuration = BrowserConfiguration.from_dict(data) + evaluation_configuration = EvaluationConfiguration.from_dict(data) + state = State.from_dict(data) + mech_group = data['mech_group'] + database_collection = data['db_collection'] + return TestParameters(browser_configuration, evaluation_configuration, state, mech_group, database_collection) @dataclass(frozen=True) class TestResult: @@ -228,6 +238,35 @@ class PlotParameters: dirty_allowed: bool = True target_cookie_name: Optional[str] = None + @staticmethod + def from_dict(data: dict) -> PlotParameters: + if data.get("lower_version", None) and data.get("upper_version", None): + major_version_range = (data["lower_version"], data["upper_version"]) + else: + major_version_range = None + if data.get("lower_revision_nb", None) and data.get("upper_revision_nb", None): + revision_number_range = ( + data["lower_revision_nb"], + data["upper_revision_nb"], + ) + else: + revision_number_range = None + return PlotParameters( + data.get("plot_mech_group"), + data.get("target_mech_id"), + data.get("browser_name"), + data.get("db_collection"), + major_version_range=major_version_range, + revision_number_range=revision_number_range, + browser_config=data.get("browser_setting", "default"), + extensions=data.get("extensions", []), + cli_options=data.get("cli_options", []), + dirty_allowed=data.get("dirty_allowed", True), + target_cookie_name=None + if data.get("check_for") == "request" + else data.get("target_cookie_name", "generic"), + ) + @staticmethod def evaluation_factory(kwargs: ImmutableMultiDict) -> list[EvaluationParameters]: diff --git a/bci/main.py b/bci/main.py index 127c1c2..a377f18 100644 --- a/bci/main.py +++ b/bci/main.py @@ -1,161 +1,193 @@ import logging -import bci.browser.binary.factory as binary_factory -from bci.analysis.plot_factory import PlotFactory -from bci.browser.support import get_chromium_support, get_firefox_support +import bci.database.mongo.container as mongodb_container from bci.configuration import Global, Loggers -from bci.database.mongo.mongodb import MongoDB -from bci.evaluations.logic import EvaluationParameters, PlotParameters -from bci.master import Master +from bci.database.mongo.mongodb import MongoDB, ServerException +from bci.database.mongo.revision_cache import RevisionCache +from bci.distribution.worker_manager import WorkerManager +from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework +from bci.evaluations.logic import ( + DatabaseParameters, + EvaluationParameters, + TestParameters, +) +from bci.evaluations.outcome_checker import OutcomeChecker +from bci.search_strategy.bgb_search import BiggestGapBisectionSearch +from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence +from bci.search_strategy.composite_search import CompositeSearch +from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy +from bci.version_control.factory import StateFactory +from bci.version_control.states.revisions.firefox import BINARY_AVAILABILITY_MAPPING +from bci.web.clients import Clients logger = logging.getLogger(__name__) class Main: - loggers = None - master = None - - @staticmethod - def initialize(): - Main.loggers = Loggers() - Main.loggers.configure_loggers() - if Global.check_required_env_parameters(): - Main.master = Master() - - @staticmethod - def is_ready() -> bool: - return Main.master is not None - - @staticmethod - def run(params: EvaluationParameters): - Main.master.run(params) - - @staticmethod - def stop_gracefully(): - Main.master.activate_stop_gracefully() - - @staticmethod - def stop_forcefully(): - Main.master.activate_stop_forcefully() - - @staticmethod - def get_state() -> str: - return Main.master.state - - @staticmethod - def connect_to_database(): - return Main.master.connect_to_database() - - @staticmethod - def get_logs() -> list[str]: - return list( - map( - lambda x: Main.format_to_user_log(x.__dict__), - Loggers.memory_handler.buffer, - ) - ) - - @staticmethod - def format_to_user_log(log: dict) -> str: - return f'[{log["asctime"]}] [{log["levelname"]}] {log["name"]}: {log["msg"]}' - - @staticmethod - def get_database_info() -> dict: - return MongoDB().get_info() - - @staticmethod - def get_browser_support() -> list[dict]: - return [get_chromium_support(), get_firefox_support()] - - @staticmethod - def list_downloaded_binaries(browser): - return binary_factory.list_downloaded_binaries(browser) - - @staticmethod - def list_artisanal_binaries(browser): - return binary_factory.list_artisanal_binaries(browser) - - @staticmethod - def update_artisanal_binaries(browser): - return binary_factory.update_artisanal_binaries(browser) - - @staticmethod - def download_online_binary(browser, rev_number): - binary_factory.download_online_binary(browser, rev_number) - - @staticmethod - def get_mech_groups_of_evaluation_framework(evaluation_name: str, project) -> list[tuple[str, bool]]: - return Main.master.evaluation_framework.get_mech_groups(project) - - @staticmethod - def get_projects_of_custom_framework() -> list[str]: - return Main.master.evaluation_framework.get_projects() - - @staticmethod - def convert_to_plotparams(data: dict) -> PlotParameters: - if data.get("lower_version", None) and data.get("upper_version", None): - major_version_range = (data["lower_version"], data["upper_version"]) + def __init__(self) -> None: + self.state = {'is_running': False, 'reason': 'init', 'status': 'idle'} + + self.stop_gracefully = False + self.stop_forcefully = False + + self.firefox_build = None + self.chromium_build = None + + self.eval_queue = [] + + Global.initialize_folders() + self.db_connection_params = Global.get_database_params() + self.connect_to_database(self.db_connection_params) + RevisionCache.store_firefox_binary_availability(BINARY_AVAILABILITY_MAPPING) # TODO: find better place + self.evaluation_framework = CustomEvaluationFramework() + logger.info('BugHog is ready!') + + def connect_to_database(self, db_connection_params: DatabaseParameters) -> None: + try: + MongoDB().connect(db_connection_params) + except ServerException: + logger.error('Could not connect to database.', exc_info=True) + + def run(self, eval_params_list: list[EvaluationParameters]) -> None: + # Sequence_configuration settings are the same over evaluation parameters (quick fix) + worker_manager = WorkerManager(eval_params_list[0].sequence_configuration.nb_of_containers) + self.stop_gracefully = False + self.stop_forcefully = False + try: + self.__init_eval_queue(eval_params_list) + for eval_params in eval_params_list: + if self.stop_gracefully or self.stop_forcefully: + break + self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'active') + self.__update_state(is_running=True, reason='user', status='running', queue=self.eval_queue) + self.run_single_evaluation(eval_params, worker_manager) + + except Exception as e: + logger.critical('A critical error occurred', exc_info=True) + raise e + finally: + # Gracefully exit + if self.stop_gracefully: + logger.info('Gracefully stopping experiment queue due to user end signal...') + self.state['reason'] = 'user' + if self.stop_forcefully: + logger.info('Forcefully stopping experiment queue due to user end signal...') + self.state['reason'] = 'user' + worker_manager.forcefully_stop_all_running_containers() + else: + logger.info('Gracefully stopping experiment queue since last experiment started.') + # MongoDB.disconnect() + logger.info('Waiting for remaining experiments to stop...') + worker_manager.wait_until_all_evaluations_are_done() + logger.info('BugHog has finished the evaluation!') + self.__update_state(is_running=False, status='idle', queue=self.eval_queue) + + def run_single_evaluation(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None: + browser_name = eval_params.browser_configuration.browser_name + experiment_name = eval_params.evaluation_range.mech_group + + logger.info(f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}'") + + search_strategy = self.create_sequence_strategy(eval_params) + + try: + while (self.stop_gracefully or self.stop_forcefully) is False: + # Update search strategy with new potentially new results + current_state = search_strategy.next() + + # Prepare worker parameters + worker_params = eval_params.create_worker_params_for(current_state, self.db_connection_params) + + # Start worker to perform evaluation + worker_manager.start_test(worker_params) + + except SequenceFinished: + logger.debug('Last experiment has started') + self.state['reason'] = 'finished' + self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'done') + + @staticmethod + def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrategy: + sequence_config = eval_params.sequence_configuration + search_strategy = sequence_config.search_strategy + sequence_limit = sequence_config.sequence_limit + outcome_checker = OutcomeChecker(sequence_config) + state_factory = StateFactory(eval_params, outcome_checker) + + if search_strategy == 'bgb_sequence': + strategy = BiggestGapBisectionSequence(state_factory, sequence_limit) + elif search_strategy == 'bgb_search': + strategy = BiggestGapBisectionSearch(state_factory) + elif search_strategy == 'comp_search': + strategy = CompositeSearch(state_factory, sequence_limit) else: - major_version_range = None - if data.get("lower_revision_nb", None) and data.get("upper_revision_nb", None): - revision_number_range = ( - data["lower_revision_nb"], - data["upper_revision_nb"], - ) + raise AttributeError("Unknown search strategy option '%s'" % search_strategy) + return strategy + + def activate_stop_gracefully(self): + if self.evaluation_framework: + self.stop_gracefully = True + self.__update_state(is_running=True, reason='user', status='waiting_to_stop') + self.evaluation_framework.stop_gracefully() + logger.info('Received user signal to gracefully stop.') else: - revision_number_range = None - return PlotParameters( - data.get("plot_mech_group"), - data.get("target_mech_id"), - data.get("browser_name"), - data.get("db_collection"), - major_version_range=major_version_range, - revision_number_range=revision_number_range, - browser_config=data.get("browser_setting", "default"), - extensions=data.get("extensions", []), - cli_options=data.get("cli_options", []), - dirty_allowed=data.get("dirty_allowed", True), - target_cookie_name=None - if data.get("check_for") == "request" - else data.get("target_cookie_name", "generic"), - ) - - @staticmethod - def get_data_sources(data: dict): - params = Main.convert_to_plotparams(data) - - if PlotFactory.validate_params(params): - return None, None - - return \ - PlotFactory.get_plot_revision_data(params, MongoDB()), \ - PlotFactory.get_plot_version_data(params, MongoDB()) - - @staticmethod - def get_poc(project: str, poc: str) -> dict: - return Main.master.evaluation_framework.get_poc_structure(project, poc) - - @staticmethod - def get_poc_file(project: str, poc: str, domain: str, path: str, file: str) -> str: - return Main.master.evaluation_framework.get_poc_file(project, poc, domain, path, file) - - @staticmethod - def update_poc_file(project: str, poc: str, domain: str, path: str, file: str, content: str) -> bool: - logger.debug(f'Updating file {file} of project {project} and poc {poc}') - return Main.master.evaluation_framework.update_poc_file(project, poc, domain, path, file, content) - - @staticmethod - def create_empty_poc(project: str, poc_name: str) -> bool: - return Main.master.evaluation_framework.create_empty_poc(project, poc_name) - - @staticmethod - def get_available_domains() -> list[str]: - return Global.get_available_domains() - - @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 sigint_handler(signum, frame): - return Main.master.stop_bughog() + logger.info('Received user signal to gracefully stop, but no evaluation is running.') + + def activate_stop_forcefully(self) -> None: + if self.evaluation_framework: + self.stop_forcefully = True + self.__update_state(is_running=True, reason='user', status='waiting_to_stop') + self.evaluation_framework.stop_gracefully() + WorkerManager.forcefully_stop_all_running_containers() + logger.info('Received user signal to forcefully stop.') + else: + logger.info('Received user signal to forcefully stop, but no evaluation is running.') + + def quit_bughog(self) -> None: + """ + Quits the bughog application, stopping all associated containers. + """ + logger.info('Stopping all running BugHog containers...') + self.activate_stop_forcefully() + mongodb_container.stop() + logger.info('Stopping BugHog core...') + exit(0) + + def sigint_handler(self, sig_number, stack_frame) -> None: + logger.debug(f'Sigint received with number {sig_number} for stack frame {stack_frame}') + self.quit_bughog() + + def push_info(self, ws, *args) -> None: + update = {} + all = 'all' in args + for arg in args: + if arg == 'db_info' or all: + update['db_info'] = MongoDB().get_info() + if arg == 'logs' or all: + update['logs'] = Loggers.get_logs() + if arg == 'state' or all: + update['state'] = self.state + Clients.push_info(ws, update) + + def remove_datapoint(self, params: TestParameters) -> None: + MongoDB().remove_datapoint(params) + Clients.push_results_to_all() + + def __update_state(self, **kwargs) -> None: + for key, value in kwargs.items(): + self.state[key] = value + Clients.push_info_to_all({'state': self.state}) + + def __init_eval_queue(self, eval_params_list: list[EvaluationParameters]) -> None: + self.eval_queue = [] + for eval_params in eval_params_list: + self.eval_queue.append({ + 'experiment': eval_params.evaluation_range.mech_group, + 'state': 'pending' + }) + + def __update_eval_queue(self, experiment: str, state: str) -> None: + for eval in self.eval_queue: + if eval['experiment'] == experiment: + eval['state'] = state + return diff --git a/bci/master.py b/bci/master.py deleted file mode 100644 index 60b36ce..0000000 --- a/bci/master.py +++ /dev/null @@ -1,169 +0,0 @@ -import logging - -import bci.database.mongo.container as mongodb_container -from bci.configuration import Global -from bci.database.mongo.mongodb import MongoDB, ServerException -from bci.database.mongo.revision_cache import RevisionCache -from bci.distribution.worker_manager import WorkerManager -from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework -from bci.evaluations.logic import ( - DatabaseParameters, - EvaluationParameters, -) -from bci.evaluations.outcome_checker import OutcomeChecker -from bci.search_strategy.bgb_search import BiggestGapBisectionSearch -from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence -from bci.search_strategy.composite_search import CompositeSearch -from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy -from bci.version_control.factory import StateFactory -from bci.version_control.states.revisions.firefox import BINARY_AVAILABILITY_MAPPING -from bci.web.clients import Clients - -logger = logging.getLogger(__name__) - - -class Master: - def __init__(self) -> None: - self.state = {'is_running': False, 'reason': 'init', 'status': 'idle'} - - self.stop_gracefully = False - self.stop_forcefully = False - - self.firefox_build = None - self.chromium_build = None - - self.eval_queue = [] - - Global.initialize_folders() - self.db_connection_params = Global.get_database_params() - self.connect_to_database(self.db_connection_params) - RevisionCache.store_firefox_binary_availability(BINARY_AVAILABILITY_MAPPING) # TODO: find better place - self.evaluation_framework = CustomEvaluationFramework() - logger.info('BugHog is ready!') - - def connect_to_database(self, db_connection_params: DatabaseParameters) -> None: - try: - MongoDB().connect(db_connection_params) - except ServerException: - logger.error('Could not connect to database.', exc_info=True) - - def run(self, eval_params_list: list[EvaluationParameters]) -> None: - # Sequence_configuration settings are the same over evaluation parameters (quick fix) - worker_manager = WorkerManager(eval_params_list[0].sequence_configuration.nb_of_containers) - self.stop_gracefully = False - self.stop_forcefully = False - try: - self.__init_eval_queue(eval_params_list) - for eval_params in eval_params_list: - if self.stop_gracefully or self.stop_forcefully: - break - self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'active') - self.__update_state(is_running=True,reason='user', status='running', queue=self.eval_queue) - self.run_single_evaluation(eval_params, worker_manager) - - except Exception as e: - logger.critical('A critical error occurred', exc_info=True) - raise e - finally: - # Gracefully exit - if self.stop_gracefully: - logger.info('Gracefully stopping experiment queue due to user end signal...') - self.state['reason'] = 'user' - if self.stop_forcefully: - logger.info('Forcefully stopping experiment queue due to user end signal...') - self.state['reason'] = 'user' - worker_manager.forcefully_stop_all_running_containers() - else: - logger.info('Gracefully stopping experiment queue since last experiment started.') - # MongoDB.disconnect() - logger.info('Waiting for remaining experiments to stop...') - worker_manager.wait_until_all_evaluations_are_done() - logger.info('BugHog has finished the evaluation!') - self.__update_state(is_running=False, status='idle', queue=self.eval_queue) - - def run_single_evaluation(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None: - browser_name = eval_params.browser_configuration.browser_name - experiment_name = eval_params.evaluation_range.mech_group - - logger.info(f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}'") - - search_strategy = self.create_sequence_strategy(eval_params) - - try: - while (self.stop_gracefully or self.stop_forcefully) is False: - # Update search strategy with new potentially new results - current_state = search_strategy.next() - - # Prepare worker parameters - worker_params = eval_params.create_worker_params_for(current_state, self.db_connection_params) - - # Start worker to perform evaluation - worker_manager.start_test(worker_params) - - except SequenceFinished: - logger.debug('Last experiment has started') - self.state['reason'] = 'finished' - self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'done') - - @staticmethod - def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrategy: - sequence_config = eval_params.sequence_configuration - search_strategy = sequence_config.search_strategy - sequence_limit = sequence_config.sequence_limit - outcome_checker = OutcomeChecker(sequence_config) - state_factory = StateFactory(eval_params, outcome_checker) - - if search_strategy == 'bgb_sequence': - strategy = BiggestGapBisectionSequence(state_factory, sequence_limit) - elif search_strategy == 'bgb_search': - strategy = BiggestGapBisectionSearch(state_factory) - elif search_strategy == 'comp_search': - strategy = CompositeSearch(state_factory, sequence_limit) - else: - raise AttributeError("Unknown search strategy option '%s'" % search_strategy) - return strategy - - def activate_stop_gracefully(self): - if self.evaluation_framework: - self.stop_gracefully = True - self.__update_state(is_running=True, reason='user', status='waiting_to_stop') - self.evaluation_framework.stop_gracefully() - logger.info('Received user signal to gracefully stop.') - else: - logger.info('Received user signal to gracefully stop, but no evaluation is running.') - - def activate_stop_forcefully(self) -> None: - if self.evaluation_framework: - self.stop_forcefully = True - self.__update_state(is_running=True, reason='user', status='waiting_to_stop') - self.evaluation_framework.stop_gracefully() - WorkerManager.forcefully_stop_all_running_containers() - logger.info('Received user signal to forcefully stop.') - else: - logger.info('Received user signal to forcefully stop, but no evaluation is running.') - - def stop_bughog(self) -> None: - logger.info('Stopping all running BugHog containers...') - self.activate_stop_forcefully() - mongodb_container.stop() - logger.info('Stopping BugHog core...') - exit(0) - - def __update_state(self, **kwargs) -> None: - for key, value in kwargs.items(): - self.state[key] = value - Clients.push_info_to_all('state') - - def __init_eval_queue(self, eval_params_list: list[EvaluationParameters]) -> None: - self.eval_queue = [] - for eval_params in eval_params_list: - self.eval_queue.append({ - 'experiment': eval_params.evaluation_range.mech_group, - 'state': 'pending' - }) - - def __update_eval_queue(self, experiment: str, state: str) -> None: - for eval in self.eval_queue: - if eval['experiment'] == experiment: - eval['state'] = state - return diff --git a/bci/search_strategy/bgb_search.py b/bci/search_strategy/bgb_search.py index a9f6a22..34fcf67 100644 --- a/bci/search_strategy/bgb_search.py +++ b/bci/search_strategy/bgb_search.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Optional @@ -16,13 +18,14 @@ class BiggestGapBisectionSearch(BiggestGapBisectionSequence): It stops when there are no more states to evaluate between two states with different outcomes. """ - def __init__(self, state_factory: StateFactory) -> None: + def __init__(self, state_factory: StateFactory, completed_states: Optional[list[State]]=None) -> None: """ Initializes the search strategy. :param state_factory: The factory to create new states. + :param completed_states: States that have already been returned. """ - super().__init__(state_factory, 0) + super().__init__(state_factory, 0, completed_states=completed_states) def next(self) -> State: """ @@ -86,3 +89,12 @@ def __get_next_pair_to_split(self) -> Optional[tuple[State, State]]: # splitter of the second gap with having to wait for the first gap to be fully evaluated. pairs.sort(key=lambda pair: pair[1].index - pair[0].index, reverse=True) return pairs[0] + + @staticmethod + def create_from_bgb_sequence(bgb_sequence: BiggestGapBisectionSequence) -> BiggestGapBisectionSearch: + """ + Returns a BGB search object, which continues on state of the given BGB sequence object. + + :param bgb_sequence: The BGB sequence object from which the state will be used to create the BGB search object. + """ + return BiggestGapBisectionSearch(bgb_sequence._state_factory, completed_states=bgb_sequence._completed_states) diff --git a/bci/search_strategy/bgb_sequence.py b/bci/search_strategy/bgb_sequence.py index 92a9b77..f59efde 100644 --- a/bci/search_strategy/bgb_sequence.py +++ b/bci/search_strategy/bgb_sequence.py @@ -13,14 +13,15 @@ class BiggestGapBisectionSequence(SequenceStrategy): This sequence strategy will split the biggest gap between two states in half and return the state in the middle. """ - def __init__(self, state_factory: StateFactory, limit: int) -> None: + def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: """ Initializes the sequence strategy. :param state_factory: The factory to create new states. :param limit: The maximum number of states to evaluate. 0 means no limit. + :param completed_states: States that have already been returned. """ - super().__init__(state_factory, limit) + super().__init__(state_factory, limit, completed_states=completed_states) self._unavailability_gap_pairs: set[tuple[State, State]] = set() """Tuples in this list are **strict** boundaries of ranges without any available binaries.""" diff --git a/bci/search_strategy/composite_search.py b/bci/search_strategy/composite_search.py index f59a7c1..90625e6 100644 --- a/bci/search_strategy/composite_search.py +++ b/bci/search_strategy/composite_search.py @@ -8,16 +8,19 @@ class CompositeSearch(): def __init__(self, state_factory: StateFactory, sequence_limit: int) -> None: self.sequence_strategy = BiggestGapBisectionSequence(state_factory, limit=sequence_limit) - self.search_strategy = BiggestGapBisectionSearch(state_factory) - self.sequence_strategy_finished = False + self.search_strategy = None def next(self) -> State: - # First we use the sequence strategy to select the next state - if not self.sequence_strategy_finished: + """ + Returns the next state, based on a sequence strategy and search strategy. + First, the sequence strategy decides which state to return until it returns the SequenceFinished exception. + From then on, the search strategy decides which state to return. + """ + if self.search_strategy is None: try: return self.sequence_strategy.next() except SequenceFinished: - self.sequence_strategy_finished = True + self.search_strategy = BiggestGapBisectionSearch.create_from_bgb_sequence(self.sequence_strategy) return self.search_strategy.next() else: return self.search_strategy.next() diff --git a/bci/search_strategy/sequence_strategy.py b/bci/search_strategy/sequence_strategy.py index 3a6eaff..c93585f 100644 --- a/bci/search_strategy/sequence_strategy.py +++ b/bci/search_strategy/sequence_strategy.py @@ -10,17 +10,18 @@ class SequenceStrategy: - def __init__(self, state_factory: StateFactory, limit) -> None: + def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None: """ Initializes the sequence strategy. :param state_factory: The factory to create new states. :param limit: The maximum number of states to evaluate. 0 means no limit. + :param completed_states: States that have already been returned. """ self._state_factory = state_factory self._limit = limit self._lower_state, self._upper_state = self.__create_available_boundary_states() - self._completed_states = [] + self._completed_states = completed_states if completed_states else [] @abstractmethod def next(self) -> State: diff --git a/bci/util.py b/bci/util.py index ffaf965..3175ca1 100644 --- a/bci/util.py +++ b/bci/util.py @@ -42,6 +42,16 @@ def copy_folder(src_path, dst_path): shutil.copytree(src_path, dst_path, dirs_exist_ok=True) +def remove_all_in_folder(folder_path: str) -> None: + for root, dirs, files in os.walk(folder_path): + for file_name in files: + file_path = os.path.join(root, file_name) + os.remove(file_path) + for dir_name in dirs: + dir_path = os.path.join(root, dir_name) + shutil.rmtree(dir_path) + + def rmtree(src_path): """ Removes folder at given src_path. diff --git a/bci/version_control/states/revisions/firefox.py b/bci/version_control/states/revisions/firefox.py index f9eb831..928864b 100644 --- a/bci/version_control/states/revisions/firefox.py +++ b/bci/version_control/states/revisions/firefox.py @@ -28,6 +28,8 @@ def has_online_binary(self) -> bool: def get_online_binary_url(self) -> str: result = RevisionCache.firefox_get_binary_info(self._revision_id) + if result is None: + raise AttributeError(f"Could not find binary url for '{self._revision_id}") binary_base_url = result['files_url'] app_version = result['app_version'] binary_url = f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2' diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py index 592fbca..607edcf 100644 --- a/bci/web/blueprints/api.py +++ b/bci/web/blueprints/api.py @@ -3,11 +3,15 @@ import os import threading -from flask import Blueprint, request +from flask import Blueprint, current_app, request +import bci.browser.support as browser_support +import bci.evaluations.logic as application_logic +from bci.analysis.plot_factory import PlotFactory from bci.app import sock -from bci.evaluations.logic import evaluation_factory -from bci.main import Main as bci_api +from bci.configuration import Global, Loggers +from bci.evaluations.logic import PlotParameters +from bci.main import Main from bci.web.clients import Clients logger = logging.getLogger(__name__) @@ -16,8 +20,10 @@ THREAD = None -def start_thread(func, args=None) -> bool: +def __start_thread(func, args=None) -> bool: global THREAD + if args is None: + args = [] if THREAD and THREAD.is_alive(): return False else: @@ -26,15 +32,24 @@ def start_thread(func, args=None) -> bool: return True +def __get_main() -> Main: + if main := current_app.config['main']: + return main + raise Exception('Main object is not instantiated') + @api.before_request def check_readiness(): - if not bci_api.is_ready(): + try: + pass + # _ = ____get_main() + except Exception as e: + logger.critical(e) return { 'status': 'NOK', 'msg': 'BugHog is not ready', 'info': { - 'log': bci_api.get_logs(), + 'log': Loggers.get_logs() } } @@ -55,9 +70,15 @@ def add_headers(response): @api.route('/evaluation/start/', methods=['POST']) def start_evaluation(): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No evaluation parameters found" + } + data = request.json.copy() - params = evaluation_factory(data) - if start_thread(bci_api.run, args=[params]): + params = application_logic.evaluation_factory(data) + if __start_thread(__get_main().run, args=[params]): return { 'status': 'OK' } @@ -69,12 +90,18 @@ def start_evaluation(): @api.route('/evaluation/stop/', methods=['POST']) def stop_evaluation(): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No stop parameters found" + } + data = request.json.copy() forcefully = data.get('forcefully', False) if forcefully: - bci_api.stop_forcefully() + __get_main().activate_stop_forcefully() else: - bci_api.stop_gracefully() + __get_main().activate_stop_gracefully() return { 'status': 'OK' } @@ -100,7 +127,7 @@ def init_websocket(ws): if params := message.get('select_project', None): Clients.associate_project(ws, params) if requested_variables := message.get('get', []): - Clients.push_info(ws, *requested_variables) + __get_main().push_info(ws, *requested_variables) except ValueError: logger.warning('Ignoring invalid message from client.') ws.send('Connected to BugHog') @@ -110,7 +137,7 @@ def init_websocket(ws): def get_browsers(): return { 'status': 'OK', - 'browsers': bci_api.get_browser_support() + 'browsers': [browser_support.get_chromium_support(), browser_support.get_firefox_support()] } @@ -118,7 +145,21 @@ def get_browsers(): def get_projects(): return { 'status': 'OK', - 'projects': bci_api.get_projects_of_custom_framework() + 'projects': __get_main().evaluation_framework.get_projects() + } + + +@api.route('/projects/', methods=['POST']) +def create_project(): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No parameters found" + } + project_name = request.json.get('project_name') + return { + 'status': 'OK', + 'projects': __get_main().evaluation_framework.create_empty_project(project_name) } @@ -140,24 +181,28 @@ def log(): @api.route('/data/', methods=['PUT']) def data_source(): - params = request.json.copy() - revision_data, version_data = bci_api.get_data_sources(params) - if revision_data or version_data: + if request.json is None: return { - 'status': 'OK', - 'revision': revision_data, - 'version': version_data + 'status': 'NOK', + 'msg': "No data parameters found" } - else: + + params = request.json.copy() + plot_params = PlotParameters.from_dict(params) + if missing_params := PlotFactory.validate_params(plot_params): return { 'status': 'NOK', - 'msg': 'Invalid type' + 'msg': f'Missing plot parameters: {missing_params}' + } + return { + 'status': 'OK', + 'revision': PlotFactory.get_plot_revision_data(params), + 'version': PlotFactory.get_plot_version_data(params) } - @api.route('/poc//', methods=['GET']) def get_experiments(project: str): - experiments = bci_api.get_mech_groups_of_evaluation_framework('custom', project) + experiments = __get_main().evaluation_framework.get_mech_groups(project) return { 'status': 'OK', 'experiments': experiments @@ -168,36 +213,71 @@ def get_experiments(project: str): def poc(project: str, poc: str): return { 'status': 'OK', - 'tree': bci_api.get_poc(project, poc) + 'tree': __get_main().evaluation_framework.get_poc_structure(project, poc) } -@api.route('/poc//////', methods=['GET']) -def poc_file(project: str, poc: str, domain: str, path: str, file: str): - return { - 'status': 'OK', - 'content': bci_api.get_poc_file(project, poc, domain, path, file) - } +@api.route('/poc////', methods=['GET', 'POST']) +def get_poc_file_content(project: str, poc: str, file: str): + domain = request.args.get('domain', '') + path = request.args.get('path', '') + if request.method == 'GET': + return { + 'status': 'OK', + 'content': __get_main().evaluation_framework.get_poc_file(project, poc, domain, path, file) + } + else: + if not request.json: + return { + 'status': 'NOK', + 'msg': 'No content to update file with' + } + data = request.json.copy() + content = data['content'] + success = __get_main().evaluation_framework.update_poc_file(project, poc, domain, path, file, content) + if success: + return { + 'status': 'OK' + } + else : + return { + 'status': 'NOK' + } -@api.route('/poc//////', methods=['POST']) -def update_poc_file(project: str, poc: str, domain: str, path: str, file: str): +@api.route('/poc///', methods=['POST']) +def add_page(project: str, poc: str): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No page parameters found" + } + data = request.json.copy() - success = bci_api.update_poc_file(project, poc, domain, path, file, data['content']) + domain = data['domain'] + path = data['page'] + file_type = data['file_type'] + success = __get_main().evaluation_framework.add_page(project, poc, domain, path, file_type) if success: return { 'status': 'OK' } - else : + else: return { 'status': 'NOK' } -@api.route('/poc///', methods=['POST']) -def add_page(project: str, poc: str): +@api.route('/poc///config', methods=['POST']) +def add_config(project: str, poc: str): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No parameters found" + } data = request.json.copy() - success = bci_api.add_page(project, poc, data['domain'], data['page'], data['file_type']) + type = data['type'] + success = __get_main().evaluation_framework.add_config(project, poc, type) if success: return { 'status': 'OK' @@ -212,19 +292,26 @@ def add_page(project: str, poc: str): def get_available_domains(): return { 'status': 'OK', - 'domains': bci_api.get_available_domains() + 'domains': Global.get_available_domains() } @api.route('/poc//', methods=['POST']) def create_experiment(project: str): + if request.json is None: + return { + 'status': 'NOK', + 'msg': "No experiment parameters found" + } + data = request.json.copy() if 'poc_name' not in data.keys(): return { 'status': 'NOK', 'msg': 'Missing experiment name' } - if bci_api.create_empty_poc(project, data['poc_name']): + poc_name = data['poc_name'] + if __get_main().evaluation_framework.create_empty_poc(project, poc_name): return { 'status': 'OK' } @@ -233,3 +320,17 @@ def create_experiment(project: str): 'status': 'NOK', 'msg': 'Could not create experiment' } + + +@api.route('/data/remove/', methods=['POST']) +def remove_datapoint(): + if (params := application_logic.TestParameters.from_dict(request.json)) is None: + return { + 'status': 'NOK', + 'msg': "No parameters found" + } + __get_main().remove_datapoint(params) + return { + 'status': 'OK' + } + diff --git a/bci/web/blueprints/experiments.py b/bci/web/blueprints/experiments.py index 96297cc..a594240 100644 --- a/bci/web/blueprints/experiments.py +++ b/bci/web/blueprints/experiments.py @@ -1,9 +1,11 @@ import datetime import logging import threading +import importlib.util +import sys import requests -from flask import Blueprint, make_response, render_template, request, url_for +from flask import Blueprint, Request, make_response, render_template, request, url_for from bci.web.page_parser import load_experiment_pages @@ -24,6 +26,7 @@ @exp.before_request def before_request(): + __report(request) host = request.host.lower() if host not in ALLOWED_DOMAINS: logger.error( @@ -32,36 +35,13 @@ def before_request(): return f"Host '{host}' is not supported by this framework." -@exp.route("/") -def index(): - return f"This page is visited over {request.scheme}." - - -@exp.route("/report/", methods=["GET", "POST"]) -def report(): - leak = request.args.get("leak") - if leak is not None: - resp = make_response( - render_template("cookies.html", title="Report", to_report=leak) - ) - else: - resp = make_response( - render_template( - "cookies.html", title="Report", to_report="Nothing to report" - ) - ) - - cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4) - resp.set_cookie("generic", "1", expires=cookie_exp_date) - resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True) - resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True) - resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax") - resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict") - +def __report(request: Request) -> None: + """ + Submit report to BugHog + """ # Respond to collector on same IP # remote_ip = request.remote_addr remote_ip = request.headers.get("X-Real-IP") - response_data = { "url": request.url, "method": request.method, @@ -77,6 +57,29 @@ def send_report_to_collector(): threading.Thread(target=send_report_to_collector).start() + +def __get_all_GET_parameters(request) -> dict[str,str]: + return {k: v for k, v in request.args.items()} + + +@exp.route("/") +def index(): + return f"This page is visited over {request.scheme}." + + +@exp.route("/report/", methods=["GET", "POST"]) +def report_endpoint(): + get_params = [item for item in __get_all_GET_parameters(request).items()] + resp = make_response( + render_template("cookies.html", title="Report", get_params=get_params) + ) + + cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4) + resp.set_cookie("generic", "1", expires=cookie_exp_date) + resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True) + resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True) + resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax") + resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict") return resp @@ -86,9 +89,9 @@ def report_leak_if_using_http(target_scheme): Triggers request to /report/ if a request was received over the specified `scheme`. """ used_scheme = request.headers.get("X-Forwarded-Proto") - params = get_all_bughog_GET_parameters(request) + params = __get_all_GET_parameters(request) if used_scheme == target_scheme: - return "Redirect", 307, {"Location": url_for("experiments.report", **params)} + return "Redirect", 307, {"Location": url_for("experiments.report_endpoint", **params)} else: return f"Request was received over {used_scheme}, instead of {target_scheme}", 200, {} @@ -101,12 +104,12 @@ def report_leak_if_present(expected_header_name: str): if expected_header_name not in request.headers: return f"Header {expected_header_name} not found", 200, {"Allow-CSP-From": "*"} - params = get_all_bughog_GET_parameters(request) + params = __get_all_GET_parameters(request) return ( "Redirect", 307, { - "Location": url_for("experiments.report", **params), + "Location": url_for("experiments.report_endpoint", **params), "Allow-CSP-From": "*", }, ) @@ -126,16 +129,32 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st {"Allow-CSP-From": "*"}, ) - params = get_all_bughog_GET_parameters(request) + params = __get_all_GET_parameters(request) return ( "Redirect", 307, { - "Location": url_for("experiments.report", **params), + "Location": url_for("experiments.report_endpoint", **params), "Allow-CSP-From": "*", }, ) -def get_all_bughog_GET_parameters(request): - return {k: v for k, v in request.args.items() if k.startswith("bughog_")} +@exp.route("///.py") +def python_evaluation(project: str, experiment: str, file_name: str): + """ + Evaluates the python script and returns its result. + """ + host = request.host.lower() + + module_name = f"{host}/{project}/{experiment}" + path = f"experiments/pages/{project}/{experiment}/{host}/{file_name}.py" + + # Dynamically import the file + sys.dont_write_bytecode = True + spec = importlib.util.spec_from_file_location(module_name, path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + + return module.main(request) diff --git a/bci/web/clients.py b/bci/web/clients.py index eee5d24..acb2045 100644 --- a/bci/web/clients.py +++ b/bci/web/clients.py @@ -1,8 +1,13 @@ import json import threading +from venv import logger +from flask import current_app from simple_websocket import Server +from bci.analysis.plot_factory import PlotFactory +from bci.evaluations.logic import PlotParameters + class Clients: __semaphore = threading.Semaphore() @@ -39,10 +44,17 @@ def associate_project(ws_client: Server, project: str): @staticmethod def push_results(ws_client: Server): - from bci.main import Main as bci_api - if params := Clients.__clients.get(ws_client, None): - revision_data, version_data = bci_api.get_data_sources(params) + plot_params = PlotParameters.from_dict(params) + + if PlotFactory.validate_params(plot_params): + revision_data = None + version_data = None + else: + revision_data = PlotFactory.get_plot_revision_data(plot_params) + version_data = PlotFactory.get_plot_version_data(plot_params) + + ws_client.send( json.dumps( { @@ -63,32 +75,27 @@ def push_results_to_all(): Clients.push_results(ws_client) @staticmethod - def push_info(ws_client: Server, *requested_vars: str): - from bci.main import Main as bci_api - - update = {} - all = not requested_vars or 'all' in requested_vars - if 'db_info' in requested_vars or all: - update['db_info'] = bci_api.get_database_info() - if 'logs' in requested_vars or all: - update['logs'] = bci_api.get_logs() - if 'state' in requested_vars or all: - update['state'] = bci_api.get_state() + def push_info(ws_client: Server, update: dict): ws_client.send(json.dumps({'update': update})) @staticmethod - def push_info_to_all(*requested_vars: str): + def push_info_to_all(update: dict): Clients.__remove_disconnected_clients() for ws_client in Clients.__clients.keys(): - Clients.push_info(ws_client, *requested_vars) + Clients.push_info(ws_client, update) @staticmethod def push_experiments(ws_client: Server): - from bci.main import Main as bci_api + client_info = Clients.__clients[ws_client] + if client_info is None: + logger.error('Could not find any associated info for this client') + return - project = Clients.__clients[ws_client].get('project', None) + project = client_info.get('project', None) if project: - experiments = bci_api.get_mech_groups_of_evaluation_framework('custom', project) + from bci.main import Main + main: Main = current_app.config['main'] + experiments = main.evaluation_framework.get_mech_groups(project) ws_client.send(json.dumps({'update': {'experiments': experiments}})) @staticmethod diff --git a/bci/web/page_parser.py b/bci/web/page_parser.py index 1cff781..57dc618 100644 --- a/bci/web/page_parser.py +++ b/bci/web/page_parser.py @@ -68,6 +68,10 @@ def get_content(subdir_folder_path: str): { "file_name": "index.js", "content_type": "text/javascript" + }, + { + "file_name": "index.py", + "content_type": "text/x-python" } ] content = None diff --git a/bci/web/templates/cookies.html b/bci/web/templates/cookies.html index 320a390..2e1b0b0 100644 --- a/bci/web/templates/cookies.html +++ b/bci/web/templates/cookies.html @@ -2,10 +2,15 @@ {% block content %} -{% if to_report %} +{% if get_params %}

Reported:

- -

{{ to_report }}

+
    +{% for get_param in get_params %} +
  • {{ get_param[0] }}: {{ get_param[1] }}
  • +{% endfor %} +
+{% else %} +No GET parameters. {% endif %}

Cookies

@@ -20,4 +25,4 @@

Cookies

elem.appendChild(cookie); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/bci/web/vue/index.html b/bci/web/vue/index.html index a427ba2..6bf8ba4 100644 --- a/bci/web/vue/index.html +++ b/bci/web/vue/index.html @@ -4,8 +4,8 @@ - - + + BugHog diff --git a/bci/web/vue/package-lock.json b/bci/web/vue/package-lock.json index 1a1d035..e393c6a 100644 --- a/bci/web/vue/package-lock.json +++ b/bci/web/vue/package-lock.json @@ -29,6 +29,7 @@ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -40,6 +41,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -48,16 +50,18 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", - "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.26.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -67,9 +71,10 @@ } }, "node_modules/@babel/types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", - "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -86,6 +91,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -102,6 +108,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -118,6 +125,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -134,6 +142,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -150,6 +159,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -166,6 +176,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -182,6 +193,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -198,6 +210,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -214,6 +227,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -230,6 +244,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -246,6 +261,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -262,6 +278,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -278,6 +295,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -294,6 +312,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -310,6 +329,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -326,6 +346,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -342,6 +363,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -358,6 +380,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -374,6 +397,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -390,6 +414,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -406,6 +431,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -422,6 +448,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -435,6 +462,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -452,6 +480,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -466,6 +495,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -475,6 +505,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -482,13 +513,15 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -499,6 +532,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -512,6 +546,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -521,6 +556,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -534,6 +570,7 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -543,6 +580,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -553,6 +591,7 @@ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", "dev": true, + "license": "MIT", "engines": { "node": "^14.18.0 || >=16.0.0" }, @@ -562,111 +601,123 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", - "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.12", + "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", - "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", - "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.12", - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", - "postcss": "^8.4.47", + "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", - "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/reactivity": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", - "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", "dependencies": { - "@vue/shared": "3.5.12" + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", - "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", - "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", - "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/runtime-core": "3.5.12", - "@vue/shared": "3.5.12", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", - "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { - "vue": "3.5.12" + "vue": "3.5.13" } }, "node_modules/@vue/shared": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", - "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==" + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" }, "node_modules/@vueform/slider": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@vueform/slider/-/slider-2.1.10.tgz", - "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw==" + "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw==", + "license": "MIT" }, "node_modules/ace-builds": { - "version": "1.36.3", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.3.tgz", - "integrity": "sha512-YcdwV2IIaJSfjkWAR1NEYN5IxBiXefTgwXsJ//UlaFrjXDX5hQpvPFvEePHz2ZBUfvO54RjHeRUQGX8MS5HaMQ==" + "version": "1.36.5", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.5.tgz", + "integrity": "sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg==", + "license": "BSD-3-Clause" }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -679,6 +730,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -690,13 +742,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -709,12 +763,14 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/autoprefixer": { "version": "10.4.20", @@ -735,6 +791,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -754,9 +811,10 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -767,13 +825,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -786,6 +846,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -795,6 +856,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -821,6 +883,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -839,14 +902,15 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001669", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", - "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "version": "1.0.30001684", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", + "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", "dev": true, "funding": [ { @@ -861,13 +925,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -892,6 +958,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -904,6 +971,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -915,12 +983,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -933,15 +1003,17 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -956,6 +1028,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -966,12 +1039,14 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -980,36 +1055,42 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.43", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz", - "integrity": "sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ==", - "dev": true + "version": "1.5.66", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.66.tgz", + "integrity": "sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1023,6 +1104,7 @@ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1059,6 +1141,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1066,13 +1149,15 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1089,6 +1174,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1101,6 +1187,7 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -1110,6 +1197,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1121,6 +1209,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-1.8.1.tgz", "integrity": "sha512-lXTcO8a6dRTPFpINyOLcATCN/pK1Of/jY4PryklPllAiqH64tSDUsOdQpar3TO59ZXWwugm2e92oaqwH6X90Xg==", + "license": "MIT", "dependencies": { "@popperjs/core": "^2.9.3", "mini-svg-data-uri": "^1.4.3" @@ -1136,6 +1225,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1150,6 +1240,7 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1165,6 +1256,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -1179,6 +1271,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -1193,6 +1286,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1206,6 +1300,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1215,6 +1310,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -1235,6 +1331,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1247,6 +1344,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1259,6 +1357,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1271,6 +1370,7 @@ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -1286,6 +1386,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1295,6 +1396,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1304,6 +1406,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1316,6 +1419,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1324,13 +1428,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -1346,6 +1452,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -1355,6 +1462,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -1363,18 +1471,21 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -1384,6 +1495,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -1393,6 +1505,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1405,6 +1518,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1413,6 +1527,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1424,6 +1539,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", "bin": { "mini-svg-data-uri": "cli.js" } @@ -1433,6 +1549,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1448,6 +1565,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -1457,6 +1575,7 @@ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -1464,15 +1583,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1484,13 +1604,15 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1500,6 +1622,7 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1509,6 +1632,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1518,6 +1642,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -1526,6 +1651,7 @@ "version": "1.0.0-rc3", "resolved": "https://registry.npmjs.org/oh-vue-icons/-/oh-vue-icons-1.0.0-rc3.tgz", "integrity": "sha512-+k2YC6piK7sEZnwbkQF3UokFPMmgqpiLP6f/H0ovQFLl20QA5V4U8EcI6EclD2Lt5NMQ3k6ilLGo8XyXqdVSvg==", + "license": "MIT", "dependencies": { "vue-demi": "^0.12.5" }, @@ -1544,6 +1670,7 @@ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.5.tgz", "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", "hasInstallScript": true, + "license": "MIT", "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" @@ -1568,13 +1695,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1583,13 +1712,15 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -1604,13 +1735,15 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1623,6 +1756,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1632,14 +1766,15 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -1654,9 +1789,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1668,6 +1804,7 @@ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -1685,6 +1822,7 @@ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -1714,6 +1852,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -1739,6 +1878,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -1761,6 +1901,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.1" }, @@ -1776,6 +1917,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -1788,12 +1930,14 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -1813,13 +1957,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.3.0" } @@ -1829,6 +1975,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1841,6 +1988,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -1858,6 +2006,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -1868,6 +2017,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -1898,6 +2048,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -1907,6 +2058,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1919,6 +2071,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1928,6 +2081,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -1939,6 +2093,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1948,6 +2103,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1966,6 +2122,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1980,6 +2137,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1988,13 +2146,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2007,6 +2167,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2023,6 +2184,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2035,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2044,6 +2207,7 @@ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -2066,6 +2230,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2074,33 +2239,34 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", - "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", + "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", + "jiti": "^1.21.6", "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -2115,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -2124,6 +2291,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -2136,6 +2304,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2147,7 +2316,8 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/update-browserslist-db": { "version": "1.1.1", @@ -2168,6 +2338,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" @@ -2183,13 +2354,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vite": { "version": "4.5.5", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -2241,15 +2414,16 @@ } }, "node_modules/vue": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", - "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-sfc": "3.5.12", - "@vue/runtime-dom": "3.5.12", - "@vue/server-renderer": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" @@ -2264,6 +2438,7 @@ "version": "2.1.9", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.9.tgz", "integrity": "sha512-nGEppmzhQQT2iDz4cl+ZCX3BpeNhygK50zWFTIRS+r7K7i61uWXJWSioMuf+V/161EPQjexI8NaEBdUlF3dp+g==", + "license": "MIT", "engines": { "node": ">= 4.0.0", "npm": ">= 3.0.0" @@ -2274,6 +2449,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2289,6 +2465,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2307,6 +2484,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2324,6 +2502,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2333,6 +2512,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -2347,13 +2527,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2368,6 +2550,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2376,10 +2559,11 @@ } }, "node_modules/yaml": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, diff --git a/bci/web/vue/public/.gitkeep b/bci/web/vue/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bci/web/vue/src/App.vue b/bci/web/vue/src/App.vue index cac9425..4d316bf 100644 --- a/bci/web/vue/src/App.vue +++ b/bci/web/vue/src/App.vue @@ -78,7 +78,8 @@ export default { project: null, }, dialog: { - new_experiment_name: null + new_experiment_name: null, + new_project_name: null }, darkmode: null, darkmode_toggle: null, @@ -131,9 +132,6 @@ export default { }, }, watch: { - "selected.project": function (val) { - this.set_curr_project(val); - }, "selected.experiment": function (val) { if (val !== null) { this.hide_poc_editor = false; @@ -185,7 +183,8 @@ export default { created: function () { this.websocket = this.create_socket(); this.get_projects(); - this.get_browser_support();const path = `/api/poc/domain/`; + this.get_browser_support(); + const path = `/api/poc/domain/`; axios.get(path) .then((res) => { if (res.data.status === "OK") { @@ -235,10 +234,13 @@ export default { "get": ["all"], }); // This might be a re-open after connection loss, which means we might have to propagate our params again - this.propagate_new_params() + if (this.selected.project !== null) { + this.set_curr_project(this.selected.project); + } + this.propagate_new_params(); }); - websocket.addEventListener("message", () => { - const data = JSON.parse(event.data); + websocket.addEventListener("message", (e) => { + const data = JSON.parse(e.data); if (data.hasOwnProperty("update")) { if (data.update.hasOwnProperty("plot_data")) { const revision_data = data.update.plot_data.revision_data; @@ -288,12 +290,15 @@ export default { localStorage.setItem('theme', 'light'); } }, - get_projects() { + get_projects(cb) { const path = `/api/projects/`; axios.get(path) .then((res) => { if (res.data.status == "OK") { this.projects = res.data.projects; + if (cb !== undefined) { + cb(); + } } }) .catch((error) => { @@ -336,11 +341,20 @@ export default { console.log("Missing plot parameters: ", this.missing_plot_params); } }, + project_dropdown_change(event) { + const option = event.target.value; + if (option == "Create new project...") { + create_project_dialog.showModal(); + } else { + this.set_curr_project(option) + } + }, set_curr_project(project) { this.eval_params.project = project; this.send_with_socket({ "select_project": project }) + this.selected.project = project; this.eval_params.tests = []; }, set_curr_browser(browser) { @@ -389,7 +403,21 @@ export default { this.dialog.new_experiment_name = null; }) .catch((error) => { - console.log('Could not create new experiment'); + console.error('Could not create new experiment'); + }); + }, + create_new_project() { + const url = `/api/projects/`; + const new_project_name = this.dialog.new_project_name; + axios.post(url, {'project_name': new_project_name}) + .then((res) => { + this.dialog.new_project_name = null; + this.get_projects(() => { + this.set_curr_project(new_project_name); + }); + }) + .catch((error) => { + console.error('Could not create new project', error); }); }, }, @@ -465,9 +493,10 @@ export default { - + @@ -605,21 +634,21 @@ export default {
+ value="bgb_sequence">
+ value="bgb_search">
+ value="comp_search">
@@ -629,7 +658,7 @@ export default { - +
@@ -683,5 +712,23 @@ export default {
+ + + +
+

+

+ Enter new project name: +
+ + +

+
+ + +
+
+
diff --git a/bci/web/vue/src/components/gantt.vue b/bci/web/vue/src/components/gantt.vue index a23bf78..fcbad9d 100644 --- a/bci/web/vue/src/components/gantt.vue +++ b/bci/web/vue/src/components/gantt.vue @@ -1,8 +1,14 @@ diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue index 7bac6b8..98385d7 100644 --- a/bci/web/vue/src/components/poc-editor.vue +++ b/bci/web/vue/src/components/poc-editor.vue @@ -1,5 +1,10 @@ @@ -187,35 +303,66 @@
+
+ + + +
+ -
  • - Add page -
  • +
    +
  • + Add page +
  • +
  • + Add script +
  • +
    @@ -251,4 +398,24 @@ + + + +
    +

    +

    Choose config type:
    + + +

    +
    + + +
    +
    +
    diff --git a/bci/web/vue/src/components/section-header.vue b/bci/web/vue/src/components/section-header.vue index 916479a..1740b38 100644 --- a/bci/web/vue/src/components/section-header.vue +++ b/bci/web/vue/src/components/section-header.vue @@ -30,7 +30,7 @@ }, "experiments": { "title": "Experiments", - "tooltip": "Pick a project from the dropdown menu to access all available experiments. Then, mark the experiments you wish to conduct. Keep in mind that if multiple experiments are chosen, only a binary sequence will be performed. For a binary search or composite search, select only one experiment." + "tooltip": "Pick a project from the dropdown menu to access all available experiments. Then, mark the experiments you wish to conduct. A selection of multiple experiments will be conducted one by one." }, "parallel_containers": { "title": "Number of parallel containers", @@ -42,7 +42,7 @@ }, "results": { "title": "Results", - "tooltip": "Choose an experiment from the dropdown menu to visualize its results in the Gantt chart below. Squares represent (approximate) release binaries, while dots represent revision binaries. Clicking on a dot will open the web page for the associated revision in the public browser repository." + "tooltip": "Choose an experiment from the dropdown menu to visualize its results in the Gantt chart below. Squares represent (approximate) release binaries, while dots represent revision binaries. Clicking on a dot will open the web page for the associated revision in the public browser repository. Holding shift while clicking on any dot or square will delete the particular result." }, "search_strategy": { "title": "Search strategy", diff --git a/bci/web/vue/src/interaction_script_mode.js b/bci/web/vue/src/interaction_script_mode.js new file mode 100644 index 0000000..4413d52 --- /dev/null +++ b/bci/web/vue/src/interaction_script_mode.js @@ -0,0 +1,47 @@ +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"); + const TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + + const HighlightRules = function() { + + var keywordMapper = this.createKeywordMapper({ + "keyword": KEYWORDS, + }, "argument", true); + + this.$rules = { + "start" : [ { + token : "comment", + regex : "#.*$" + }, { + token : "constant.numeric", // float + regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" + }, { + token : keywordMapper, + regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" + } ] + }; + this.normalizeRules(); + }; + + oop.inherits(HighlightRules, TextHighlightRules); + exports.HighlightRules = HighlightRules; +}); + +ace.define("ace/mode/interaction_script",["require","exports","module","ace/lib/oop","ace/mode/text", "ace/mode/interaction_script_highlight_rules"], function(require, exports){"use strict"; + const oop = require("ace/lib/oop"); + const TextMode = require("ace/mode/text").Mode; + const HighlightRules = require("ace/mode/interaction_script_highlight_rules").HighlightRules; + const Mode = function() { + this.HighlightRules = HighlightRules; + }; + oop.inherits(Mode, TextMode); + exports.Mode = Mode; +}); + +const getMode = () => new Promise((resolve) => ace.require(["ace/mode/interaction_script"], resolve)); + +export { + getMode, +}; \ No newline at end of file diff --git a/bci/web/vue/src/main.js b/bci/web/vue/src/main.js index 70ff98b..121570e 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 713de6c..c52649c 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 { @@ -208,6 +208,12 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +button.no-style { + padding: 0; + background: transparent; + border: none; +} + .card { padding: 2em; } 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 0000000..fe51488 --- /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 0000000..93eb4fc --- /dev/null +++ b/experiments/pages/Support/AutoGUI/a.test/main/index.html @@ -0,0 +1,14 @@ + + + + + + + +
    + + + +
    + + \ No newline at end of file diff --git a/experiments/pages/Support/AutoGUI/script.cmd b/experiments/pages/Support/AutoGUI/script.cmd new file mode 100644 index 0000000..f72b0ef --- /dev/null +++ b/experiments/pages/Support/AutoGUI/script.cmd @@ -0,0 +1,18 @@ +NAVIGATE https://a.test/Support/AutoGUI/main + +SCREENSHOT click1 +CLICK one +WRITE AutoGUI +HOTKEY ctrl a +HOTKEY ctrl c + +SCREENSHOT click2 +CLICK two + +# Equivalent to HOTKEY ctrl v +HOLD ctrl +HOLD v +RELEASE v +RELEASE ctrl + +PRESS Enter diff --git a/experiments/pages/Support/PythonServer/a.test/main/headers.json b/experiments/pages/Support/PythonServer/a.test/main/headers.json new file mode 100644 index 0000000..270dd21 --- /dev/null +++ b/experiments/pages/Support/PythonServer/a.test/main/headers.json @@ -0,0 +1,6 @@ +[ + { + "key": "Header-Name", + "value": "Header-Value" + } +] diff --git a/experiments/pages/Support/PythonServer/a.test/main/index.html b/experiments/pages/Support/PythonServer/a.test/main/index.html new file mode 100644 index 0000000..e2d4e78 --- /dev/null +++ b/experiments/pages/Support/PythonServer/a.test/main/index.html @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/experiments/pages/Support/PythonServer/a.test/server.py b/experiments/pages/Support/PythonServer/a.test/server.py new file mode 100644 index 0000000..9e364ea --- /dev/null +++ b/experiments/pages/Support/PythonServer/a.test/server.py @@ -0,0 +1,15 @@ +from flask import Request + +# Make sure that your file ends with '.py' + +def main(req: Request): + # TODO - implement your functionality and return a Flask response + + return { + "agent": req.headers.get("User-Agent"), + "cookies": req.cookies, + "host": req.host, + "path": req.path, + "scheme": req.scheme, + "url": req.url + } diff --git a/experiments/res/bughog.css b/experiments/res/bughog.css new file mode 100644 index 0000000..8fe7043 --- /dev/null +++ b/experiments/res/bughog.css @@ -0,0 +1,63 @@ +#fullscreen, #one, #two, #three, #four, #five, #six { + position: fixed; + z-index: 10; + font-size: 1px; + border: none; + outline: none; + resize: none; +} + +#fullscreen { + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: red; +} + +#one, #two, #three, #four, #five, #six { + width: 25px; + height: 20px; +} + +#one { + background-color: #800000; + color: #800000; + top: 10px; + left: 10px; +} + +#two { + background-color: #dcbeff; + color: #dcbeff; + top: 70px; + left: 10px; +} + +#three { + background-color: #ffe119; + color: #ffe119; + top: 130px; + left: 10px; +} + +#four { + background-color: #4363d8; + color: #4363d8; + top: 10px; + left: 85px; +} + +#five { + background-color: #f58231; + color: #f58231; + top: 70px; + left: 85px; +} + +#six { + background-color: #000075; + color: #000075; + top: 130px; + left: 85px; +} \ No newline at end of file diff --git a/bci/web/vue/public/bughog.ico b/experiments/res/bughog.ico similarity index 100% rename from bci/web/vue/public/bughog.ico rename to experiments/res/bughog.ico diff --git a/logs/screenshots/.gitkeep b/logs/screenshots/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nginx/config/core_dev.conf b/nginx/config/core_dev.conf index 3e1da78..78a0ca8 100644 --- a/nginx/config/core_dev.conf +++ b/nginx/config/core_dev.conf @@ -1,5 +1,9 @@ access_log /logs/nginx-access-api.log default_format; +location = /favicon.ico { + alias /www/data/res/bughog.ico; +} + location / { proxy_pass http://node:5173; proxy_set_header Host $host; diff --git a/nginx/config/core_prod.conf b/nginx/config/core_prod.conf index 835f344..43ce77b 100644 --- a/nginx/config/core_prod.conf +++ b/nginx/config/core_prod.conf @@ -1,5 +1,9 @@ access_log /logs/nginx-access-api.log default_format; +location = /favicon.ico { + alias /www/data/res/bughog.ico; +} + location / { root /www/data; } diff --git a/nginx/config/experiments.conf b/nginx/config/experiments.conf index 92c6447..36531f6 100644 --- a/nginx/config/experiments.conf +++ b/nginx/config/experiments.conf @@ -1,14 +1,34 @@ access_log /logs/nginx-access-poc.log default_format; -location /res/ { +location = /favicon.ico { + alias /www/data/res/bughog.ico; +} + +# Home +location = / { + proxy_pass http://core:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} + +# Shared static resources +location ^~ /res/ { root /www/data; + mirror /notify_collector; } -location ~ index\.(.+)$ { - root /www/data/pages; +# Reporting endpoint +location ~ /report/.*$ { + proxy_pass http://core:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } -location ~ ^/report(/.+)*/?$ { +# Dynamic experiment resources +location ~ (.+).py$ { proxy_pass http://core:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -16,11 +36,16 @@ location ~ ^/report(/.+)*/?$ { proxy_set_header X-Forwarded-Proto $scheme; } -location ~ ^/(.+)/(.+)/(.+)/$ { +# Static experiment resources +location ~ index\.(html|js)$ { + root /www/data/pages; +} + +location ~ ^/[^/]+/[^/]+/[^/]+/?$ { root /www/data/pages; index index.html index.js; # Rewrite URLs conform to experiment file structure - rewrite ^/(.+)/(.+)/(.+)/$ /$1/$2/$host/$3/ break; + rewrite ^/([^/]+)/([^/]+)/([^/]+)/?$ /$1/$2/$host/$3/ break; # Add experiment headers access_by_lua_block { local cjson = require "cjson" @@ -45,15 +70,10 @@ location ~ ^/(.+)/(.+)/(.+)/$ { ngx.log(ngx.WARN, "Could not find headers: " .. file_path) end } -} -location ~ ^/(.+)/(.+)/(.+)$ { - rewrite ^/(.+)$ /$1/; + mirror /notify_collector; } -location / { - proxy_pass http://core:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +location = /notify_collector { + include /etc/nginx/config/notify_collector.conf; } diff --git a/nginx/config/notify_collector.conf b/nginx/config/notify_collector.conf new file mode 100644 index 0000000..540ba0b --- /dev/null +++ b/nginx/config/notify_collector.conf @@ -0,0 +1,25 @@ +# We want to notify worker-specific request collectors of every request to our experiment server. +# This module is called for every received experiment-related request by using `mirror`. + +proxy_pass http://$remote_addr:5001/report/; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; + +proxy_method POST; +proxy_set_header Content-Type "application/json"; + +set $request_body_data ''; +if ($request_body) { + set $request_body_data "$request_body"; +} + +set $url '"url": "${scheme}://${host}${request_uri}"'; +set $method '"method": "$request_method"'; +set $content '"content": "${request_body_data}"'; +set $report '{${url}, ${method}, ${content}}'; +proxy_set_body $report; + +proxy_connect_timeout 2s; +proxy_send_timeout 2s; +proxy_read_timeout 2s; diff --git a/requirements.in b/requirements.in index b7055ad..f099327 100644 --- a/requirements.in +++ b/requirements.in @@ -5,3 +5,7 @@ flatten-dict gunicorn pymongo requests +pyautogui +pyvirtualdisplay +Pillow +Xlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d2968cc..10050f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile requirements.in # -blinker==1.8.2 +blinker==1.9.0 # via flask certifi==2024.8.30 # via requests @@ -16,7 +16,7 @@ dnspython==2.7.0 # via pymongo docker==7.1.0 # via -r requirements.in -flask==3.0.3 +flask==3.1.0 # via # -r requirements.in # flask-sock @@ -38,10 +38,36 @@ markupsafe==3.0.2 # via # jinja2 # werkzeug -packaging==24.1 +mouseinfo==0.1.3 + # via pyautogui +packaging==24.2 # via gunicorn +pillow==11.0.0 + # via + # -r requirements.in + # pyscreeze +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 +pyvirtualdisplay==3.0 + # via -r requirements.in requests==2.32.3 # via # -r requirements.in @@ -49,12 +75,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 -werkzeug==3.0.4 +werkzeug==3.1.3 # via flask wsproto==1.2.0 # via simple-websocket +xlib==0.21 + # via -r requirements.in diff --git a/requirements_dev.txt b/requirements_dev.txt index c9143ac..3ef374b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,13 +10,13 @@ astroid==3.3.5 # via pylint autopep8==2.3.1 # via -r requirements_dev.in -blinker==1.8.2 +blinker==1.9.0 # via # -r requirements.txt # flask -boto3==1.35.45 +boto3==1.35.71 # via -r requirements_dev.in -botocore==1.35.45 +botocore==1.35.71 # via # -r requirements_dev.in # boto3 @@ -33,11 +33,11 @@ click==8.1.7 # via # -r requirements.txt # flask -coverage[toml]==7.6.4 +coverage[toml]==7.6.8 # via # -r requirements_dev.in # pytest-cov -debugpy==1.8.7 +debugpy==1.8.9 # via -r requirements_dev.in dill==0.3.9 # via pylint @@ -51,7 +51,7 @@ flake8==7.1.1 # via # -r requirements_dev.in # pytest-flake8 -flask==3.0.3 +flask==3.1.0 # via # -r requirements.txt # flask-sock @@ -96,42 +96,83 @@ mccabe==0.7.0 # via # flake8 # pylint -packaging==24.1 +mouseinfo==0.1.3 + # via + # -r requirements.txt + # pyautogui +packaging==24.2 # via # -r requirements.txt # anybadge # gunicorn # pytest +pillow==11.0.0 + # via + # -r requirements.txt + # pyscreeze platformdirs==4.3.6 # via pylint pluggy==1.5.0 # via pytest +pyautogui==0.9.54 + # via -r requirements.txt pycodestyle==2.12.1 # via # autopep8 # flake8 pyflakes==3.2.0 # via flake8 +pygetwindow==0.0.9 + # via + # -r requirements.txt + # pyautogui pylint==3.3.1 # via -r requirements_dev.in pymongo==4.10.1 # via -r requirements.txt +pymsgbox==1.0.9 + # via + # -r requirements.txt + # pyautogui +pyperclip==1.9.0 + # via + # -r requirements.txt + # mouseinfo +pyrect==0.2.0 + # via + # -r requirements.txt + # pygetwindow +pyscreeze==1.0.1 + # via + # -r requirements.txt + # pyautogui pytest==8.3.3 # via # -r requirements_dev.in # pytest-cov # pytest-flake8 -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via -r requirements_dev.in -pytest-flake8==1.2.1 +pytest-flake8==1.3.0 # via -r requirements_dev.in python-dateutil==2.9.0.post0 # via botocore +python3-xlib==0.15 + # via + # -r requirements.txt + # mouseinfo + # pyautogui +pytweening==1.2.0 + # via + # -r requirements.txt + # pyautogui +pyvirtualdisplay==3.0 + # via -r requirements.txt requests==2.32.3 # via # -r requirements.txt # docker -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 simple-websocket==1.1.0 # via @@ -142,6 +183,7 @@ six==1.16.0 # -r requirements.txt # flatten-dict # python-dateutil + # xlib tomlkit==0.13.2 # via pylint urllib3==2.2.3 @@ -150,7 +192,7 @@ urllib3==2.2.3 # botocore # docker # requests -werkzeug==3.0.4 +werkzeug==3.1.3 # via # -r requirements.txt # flask @@ -158,3 +200,5 @@ wsproto==1.2.0 # via # -r requirements.txt # simple-websocket +xlib==0.21 + # via -r requirements.txt diff --git a/scripts/node_update.sh b/scripts/node_update.sh new file mode 100755 index 0000000..24cd657 --- /dev/null +++ b/scripts/node_update.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec docker run -v ${PWD}/bci/web/vue/:/app -w /app node:lts-alpine npm update diff --git a/scripts/pip_update.sh b/scripts/pip_update.sh new file mode 100755 index 0000000..150c577 --- /dev/null +++ b/scripts/pip_update.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +pip-compile -U requirements.in +pip-compile -U requirements_dev.in diff --git a/test/sequence/test_composite_search.py b/test/sequence/test_composite_search.py index 5a7d247..8728c8f 100644 --- a/test/sequence/test_composite_search.py +++ b/test/sequence/test_composite_search.py @@ -23,7 +23,6 @@ def test_binary_sequence_always_available_composite(self): outcome_func=lambda x: True if x < 50 else False, evaluated_indexes=[0, 99, 49, 74, 24, 36, 61, 86, 12, 42] ) - sequence.search_strategy._state_factory = state_factory # Sequence index_sequence = [sequence.next().index for _ in range(3)] @@ -47,7 +46,6 @@ def test_binary_sequence_always_available_composite_two_shifts(self): outcome_func=lambda x: True if x < 33 or 81 < x else False, evaluated_indexes=[0, 99, 49, 74, 24, 36, 61, 86, 12, 42] ) - sequence.search_strategy._state_factory = state_factory while True: try: @@ -57,7 +55,7 @@ def test_binary_sequence_always_available_composite_two_shifts(self): evaluated_indexes = [state.index for state in sequence.search_strategy._completed_states] - assert sequence.sequence_strategy_finished + assert sequence.search_strategy is not None assert 32 in evaluated_indexes assert 33 in evaluated_indexes assert 81 in evaluated_indexes