From dce258e7c476720ba1771c6e299994e9057f6b31 Mon Sep 17 00:00:00 2001 From: Krishan Sharma Date: Tue, 19 Aug 2025 07:12:04 +0000 Subject: [PATCH 01/16] Migrates build backend from setuptools to hatch. --- .github/workflows/release-to-pypi.yml | 4 +- .gitignore | 1 + hatch_build.py | 73 +++++++++++++++ matlab_proxy/app.py | 25 ++--- matlab_proxy/default_configuration.py | 4 +- matlab_proxy/util/mwi/validators.py | 7 +- pyproject.toml | 104 +++++++++++++++++++++ setup.cfg | 4 - setup.py | 129 -------------------------- 9 files changed, 194 insertions(+), 157 deletions(-) create mode 100644 hatch_build.py create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/release-to-pypi.yml b/.github/workflows/release-to-pypi.yml index 809ba6dc..d1336641 100644 --- a/.github/workflows/release-to-pypi.yml +++ b/.github/workflows/release-to-pypi.yml @@ -54,12 +54,12 @@ jobs: - name: Install Python build dependencies run: | python3 -m pip install --upgrade pip - python3 -m pip install --upgrade setuptools + python3 -m pip install --upgrade build python3 -m pip install wheel shell: bash - name: Build Source and Binary wheel distributions - run: python3 setup.py bdist_wheel sdist + run: python3 -m build shell: bash - name: Publish to PyPI. diff --git a/.gitignore b/.gitignore index f82fe7d6..44cee3ee 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ coverage.xml .ipynb_checkpoints/ *.ipynb .env +matlab_proxy/gui \ No newline at end of file diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 00000000..839f8573 --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,73 @@ +# Copyright 2025 The MathWorks, Inc. + +import os +import subprocess +from pathlib import Path +from shutil import which, copytree +from typing import Any, Dict + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class CustomBuildHook(BuildHookInterface): + # Identifier that connects this Python hook class to pyproject.toml configuration + PLUGIN_NAME = "custom" + + def initialize(self, version: str, build_data: Dict[str, Any]) -> None: + """Run npm install and build, then copy files to package.""" + + # Ensure npm is present + npm_path = which("npm") + if not npm_path: + raise Exception( + "npm must be installed and on the path during package build!" + ) + + npm_install = [npm_path, "install"] + npm_build = [npm_path, "run", "build"] + + pwd = Path.cwd() + gui_path = pwd / "gui" + gui_build_path = gui_path / "build" + + if not gui_path.exists(): + raise Exception(f"GUI directory not found: {gui_path}") + + # Cleanup the build folder to ensure latest artifacts are generated + if gui_build_path.exists(): + import shutil + + shutil.rmtree(gui_build_path) + + # Change to directory where GUI files are present + original_cwd = str(pwd) + os.chdir(gui_path) + + try: + # Install dependencies and build GUI files + subprocess.run(npm_install, check=True) + subprocess.run(npm_build, check=True) + finally: + os.chdir(original_cwd) + + if not gui_build_path.exists(): + raise Exception(f"GUI build directory not found: {gui_build_path}") + + # Copy built files to a temporary location that will be included in wheel + temp_gui_path = pwd / "matlab_proxy" / "gui" + temp_gui_path.mkdir(parents=True, exist_ok=True) + + # Cleanup pre-existing gui files + if temp_gui_path.exists(): + import shutil + + shutil.rmtree(temp_gui_path) + + copytree(gui_build_path, temp_gui_path) + + # Create __init__.py files to make directories into Python modules + (temp_gui_path / "__init__.py").touch(exist_ok=True) + for root, dirs, _ in os.walk(temp_gui_path): + for directory in dirs: + (Path(root) / directory / "__init__.py").touch(exist_ok=True) + print("Build hook step completed!") diff --git a/matlab_proxy/app.py b/matlab_proxy/app.py index 521f21ca..9d1bc9ba 100644 --- a/matlab_proxy/app.py +++ b/matlab_proxy/app.py @@ -6,6 +6,7 @@ import pkgutil import secrets import sys +from importlib import resources import aiohttp from aiohttp import client_exceptions, web @@ -497,15 +498,9 @@ def make_static_route_table(app): Returns: Dict: Containing information about the static files and header information. """ - import importlib_resources - - from matlab_proxy import gui # noqa: F401 - from matlab_proxy.gui import static # noqa: F401 - from matlab_proxy.gui.static import ( - css, # noqa: F401 - js, # noqa: F401 - media, # noqa: F401 - ) + from matlab_proxy import gui + from matlab_proxy.gui import static + from matlab_proxy.gui.static import css, js, media base_url = app["settings"]["base_url"] @@ -513,14 +508,14 @@ def make_static_route_table(app): for mod, parent in [ (gui.__name__, ""), - (gui.static.__name__, "/static"), - (gui.static.css.__name__, "/static/css"), - (gui.static.js.__name__, "/static/js"), - (gui.static.media.__name__, "/static/media"), + (static.__name__, "/static"), + (css.__name__, "/static/css"), + (js.__name__, "/static/js"), + (media.__name__, "/static/media"), ]: - for entry in importlib_resources.files(mod).iterdir(): + for entry in resources.files(mod).iterdir(): name = entry.name - if not importlib_resources.files(mod).joinpath(name).is_dir(): + if not resources.files(mod).joinpath(name).is_dir(): if name != "__init__.py": # Special case for manifest.json if "manifest.json" in name: diff --git a/matlab_proxy/default_configuration.py b/matlab_proxy/default_configuration.py index 298ddbad..b1ac1d70 100644 --- a/matlab_proxy/default_configuration.py +++ b/matlab_proxy/default_configuration.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 The MathWorks, Inc. +# Copyright 2020-2025 The MathWorks, Inc. from enum import Enum from typing import List @@ -45,7 +45,7 @@ def get_required_config() -> List[str]: list: A list of strings representing the required configuration keys. """ - required_keys: List[str] = [ + required_keys: List[ConfigKeys] = [ ConfigKeys.DOC_URL, ConfigKeys.EXT_NAME, ConfigKeys.EXT_NAME_DESC, diff --git a/matlab_proxy/util/mwi/validators.py b/matlab_proxy/util/mwi/validators.py index 65e674ee..0715ba07 100644 --- a/matlab_proxy/util/mwi/validators.py +++ b/matlab_proxy/util/mwi/validators.py @@ -15,6 +15,7 @@ import math import os import socket +from importlib import metadata from pathlib import Path from typing import List @@ -214,11 +215,7 @@ def __get_configs(): Dict: Contains all the values present in 'matlab_web_desktop_configs' entry_point from all the packages installed in the current environment. """ - import importlib_metadata - - matlab_proxy_eps = importlib_metadata.entry_points( - group=matlab_proxy.get_entrypoint_name() - ) + matlab_proxy_eps = metadata.entry_points(group=matlab_proxy.get_entrypoint_name()) configs = {} for entry_point in matlab_proxy_eps: configs[entry_point.name.lower()] = entry_point.load() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..30d36361 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,104 @@ +# Copyright 2025 The MathWorks, Inc. + +[build-system] +requires = ["hatchling >= 1.27"] +build-backend = "hatchling.build" + +[project] +name = "matlab-proxy" +version = "0.26.0" +description = "Python® package enables you to launch MATLAB® and access it from a web browser." +readme = "README.md" +license = "LicenseRef-MATHWORKS-CLOUD-REFERENCE-ARCHITECTURE-LICENSE" +license-files = ["LICENSE.md"] +requires-python = ">=3.8" +authors = [ + { name = "The MathWorks Inc.", email = "cloud@mathworks.com" }, +] +keywords = [ + "Proxy", + "MATLAB Proxy", + "MATLAB", + "MATLAB Javascript Desktop", + "MATLAB Web Desktop", + "Remote MATLAB Web Access", +] +classifiers = [ + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +dependencies = [ + "aiohttp>=3.7.4", + "aiohttp_session[secure]", + "psutil", + "watchdog", + "requests", +] + +[project.urls] +Homepage = "https://github.com/mathworks/matlab-proxy/" +Documentation = "https://github.com/mathworks/matlab-proxy/blob/main/README.md" +Issues = "https://github.com/mathworks/matlab-proxy/issues" + +# Testing dependencies +# Note: pytest-asyncio is pinned to 0.24.0 for event loop compatibility +[project.optional-dependencies] +test = [ + "pytest", + "pytest-env", + "pytest-cov", + "pytest-timeout", + "pytest-mock", + "pytest-aiohttp", + "psutil", + "urllib3", + "pytest-playwright", + "pytest-asyncio==0.24.0", +] +dev = [ + "aiohttp-devtools", + "black", + "ruff", + "matlab-proxy[test]", +] + +[project.entry-points.matlab_proxy_configs] +default_configuration_matlab_proxy = "matlab_proxy.default_configuration:config" + +[project.scripts] +matlab-proxy-app = "matlab_proxy.app:main" +matlab-proxy-app-list-servers = "matlab_proxy.util.list_servers:print_server_info" +matlab-proxy-manager-app = "matlab_proxy_manager.web.app:main" + +[tool.hatch.build.hooks.custom] +path = "hatch_build.py" + +[tool.hatch.build.targets.wheel] +packages = ["matlab_proxy", "matlab_proxy_manager"] +# To package gui files that are ignored by gitignore, we are using the artifacts option +artifacts = ["matlab_proxy/gui/**/*"] + +[tool.hatch.build.targets.sdist] +include = [ + "README.md", + "matlab_proxy/**", + "matlab_proxy_manager/**", + "gui/**", + "tests/**", +] +exclude = [ + "gui/node_modules/**", + "gui/build/**", + "matlab_proxy/gui/**", + "install_guides/**", + "examples/**", + "img/**", + "troubleshooting/**", +] +ignore-vcs = true \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3668bbdf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[metadata] -license_files = LICENSE.md -[aliases] -test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 86770548..00000000 --- a/setup.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2020-2025 The MathWorks, Inc. -import os -from pathlib import Path -from shutil import which - -import setuptools -from setuptools.command.install import install - -import matlab_proxy -import matlab_proxy_manager -from matlab_proxy.default_configuration import config - - -class InstallNpm(install): - def run(self): - # Ensure npm is present - npm_path = which("npm") - if not npm_path: - raise Exception( - "npm must be installed and on the path during package install!" - ) - - npm_install = [npm_path, "install"] - npm_build = [npm_path, "run", "build"] - - pwd = Path(os.getcwd()) - gui_path = pwd / "gui" - - # Change to directory where GUI files are present - os.chdir(gui_path) - - # Install dependencies and build GUI files - self.spawn(npm_install) - self.spawn(npm_build) - - # Change back to matlab_proxy root folder - os.chdir(pwd) - - # Copy the built GUI files and move them inside matlab_proxy - target_dir = Path(self.build_lib) / matlab_proxy.__name__ / "gui" - self.mkpath(str(target_dir)) - self.copy_tree("gui/build", str(target_dir)) - - # In order to be accessible in the package, turn the built gui into modules - (Path(target_dir) / "__init__.py").touch(exist_ok=True) - for path, directories, filenames in os.walk(target_dir): - for directory in directories: - (Path(path) / directory / "__init__.py").touch(exist_ok=True) - - super().run() - - -# Testing dependencies -# Note: pytest-asyncio is pinned to 0.24.0 for event loop compatibility -TESTS_REQUIRES = [ - "pytest", - "pytest-env", - "pytest-cov", - "pytest-timeout", - "pytest-mock", - "pytest-aiohttp", - "pytest-timeout", - "psutil", - "urllib3", - "pytest-playwright", - "pytest-asyncio==0.24.0", -] - -INSTALL_REQUIRES = [ - "aiohttp>=3.7.4", - "aiohttp_session[secure]", - "importlib-metadata", - "importlib-resources", - "psutil", - "watchdog", - "requests", -] - -HERE = Path(__file__).parent.resolve() -long_description = (HERE / "README.md").read_text() - -setuptools.setup( - name="matlab-proxy", - version="0.26.0", - url=config["doc_url"], - author="The MathWorks, Inc.", - author_email="cloud@mathworks.com", - license="MATHWORKS CLOUD REFERENCE ARCHITECTURE LICENSE", - description="Python® package enables you to launch MATLAB® and access it from a web browser.", - long_description=long_description, - long_description_content_type="text/markdown", - packages=setuptools.find_packages(exclude=["devel", "tests", "anaconda"]), - keywords=[ - "Proxy", - "MATLAB Proxy", - "MATLAB", - "MATLAB Javascript Desktop", - "MATLAB Web Desktop", - "Remote MATLAB Web Access", - ], - classifiers=[ - "Intended Audience :: Developers", - "Natural Language :: English", - "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - ], - python_requires="~=3.8", - install_requires=INSTALL_REQUIRES, - tests_require=TESTS_REQUIRES, - extras_require={"dev": ["aiohttp-devtools", "black", "ruff"] + TESTS_REQUIRES}, - # The entrypoint will be used by multiple packages that have this package as an installation - # dependency. These packages can use the same API, get_entrypoint_name(), to make their configs discoverable - entry_points={ - matlab_proxy.get_entrypoint_name(): [ - f"{matlab_proxy.get_default_config_name()} = matlab_proxy.default_configuration:config" - ], - "console_scripts": [ - f"{matlab_proxy.get_executable_name()} = matlab_proxy.app:main", - f"{matlab_proxy.get_executable_name()}-list-servers = matlab_proxy.util.list_servers:print_server_info", - f"{matlab_proxy_manager.get_executable_name()} = matlab_proxy_manager.web.app:main", - ], - }, - include_package_data=True, - zip_safe=False, - cmdclass={"install": InstallNpm}, -) From 0c56cd4b447109b36da116963e8992317888d354 Mon Sep 17 00:00:00 2001 From: Krishan Sharma Date: Wed, 20 Aug 2025 07:16:49 +0000 Subject: [PATCH 02/16] Revert importlib.metadata entry_points usage for Python 3.8/3.9 Reverts changes from mathworks/matlab-proxy#56 that broke compatibility with Python 3.8/3.9 due to different entry_points() API behavior. --- matlab_proxy/app.py | 3 ++- matlab_proxy/util/mwi/validators.py | 3 ++- pyproject.toml | 2 ++ tests/unit/proxy-manager/lib/test_api.py | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/matlab_proxy/app.py b/matlab_proxy/app.py index 9d1bc9ba..17cf1eeb 100644 --- a/matlab_proxy/app.py +++ b/matlab_proxy/app.py @@ -6,7 +6,6 @@ import pkgutil import secrets import sys -from importlib import resources import aiohttp from aiohttp import client_exceptions, web @@ -498,6 +497,8 @@ def make_static_route_table(app): Returns: Dict: Containing information about the static files and header information. """ + import importlib_resources as resources + from matlab_proxy import gui from matlab_proxy.gui import static from matlab_proxy.gui.static import css, js, media diff --git a/matlab_proxy/util/mwi/validators.py b/matlab_proxy/util/mwi/validators.py index 0715ba07..dfca96da 100644 --- a/matlab_proxy/util/mwi/validators.py +++ b/matlab_proxy/util/mwi/validators.py @@ -15,7 +15,6 @@ import math import os import socket -from importlib import metadata from pathlib import Path from typing import List @@ -215,6 +214,8 @@ def __get_configs(): Dict: Contains all the values present in 'matlab_web_desktop_configs' entry_point from all the packages installed in the current environment. """ + import importlib_metadata as metadata + matlab_proxy_eps = metadata.entry_points(group=matlab_proxy.get_entrypoint_name()) configs = {} for entry_point in matlab_proxy_eps: diff --git a/pyproject.toml b/pyproject.toml index 30d36361..d2fa1713 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ dependencies = [ "psutil", "watchdog", "requests", + "importlib-metadata", + "importlib-resources", ] [project.urls] diff --git a/tests/unit/proxy-manager/lib/test_api.py b/tests/unit/proxy-manager/lib/test_api.py index 7bcc0d87..1f5bde61 100644 --- a/tests/unit/proxy-manager/lib/test_api.py +++ b/tests/unit/proxy-manager/lib/test_api.py @@ -2,8 +2,8 @@ import pytest from matlab_proxy_manager.lib import api as mpm_api -from matlab_proxy_manager.utils import exceptions from matlab_proxy_manager.storage.server import ServerProcess +from matlab_proxy_manager.utils import exceptions @pytest.fixture @@ -62,7 +62,7 @@ async def test_start_matlab_proxy_without_existing_server(mocker): ) mock_start_subprocess = mocker.patch( "matlab_proxy_manager.lib.api._start_subprocess", - return_value={1, "url"}, + return_value=(1, "url"), ) mock_check_readiness = mocker.patch( "matlab_proxy_manager.lib.api._check_for_process_readiness", return_value=None @@ -112,7 +112,7 @@ async def test_start_matlab_proxy_with_existing_server(mocker, mock_server_proce ) mock_start_subprocess = mocker.patch( "matlab_proxy_manager.lib.api._start_subprocess", - return_value={1, "url"}, + return_value=(1, "url"), ) parent_id = "test_parent" From dac9bbfef6e9bddbba9ba4a77ab2d26bdc6830ac Mon Sep 17 00:00:00 2001 From: Kumar Pallav Date: Wed, 20 Aug 2025 11:27:06 +0000 Subject: [PATCH 03/16] Refactors test to use mocks instead of real endpoints. --- tests/unit/test_app.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 1ffb6d39..aa86e141 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -734,15 +734,35 @@ async def test_matlab_view_websocket_success( assert mock_ws_server.pong.call_count == 1 -async def test_set_licensing_info_put_mhlm(test_server): +async def test_set_licensing_info_put_mhlm( + mocker, + test_server, + set_licensing_info_mock_expand_token, + set_licensing_info_mock_access_token, + set_licensing_info_mock_fetch_multiple_entitlements, +): """Test to check endpoint : "/set_licensing_info" Test which sends HTTP PUT request with MHLM licensing information. Args: test_server (aiohttp_client): A aiohttp_client server to send HTTP GET request. """ - # FIXME: This test is talking to production loginws endpoint and is resulting in an exception. - # TODO: Use mocks to test the mhlm workflows is working as expected + + mocker.patch( + "matlab_proxy.app_state.mw.fetch_expand_token", + return_value=set_licensing_info_mock_expand_token, + ) + + mocker.patch( + "matlab_proxy.app_state.mw.fetch_access_token", + return_value=set_licensing_info_mock_access_token, + ) + + mocker.patch( + "matlab_proxy.app_state.mw.fetch_entitlements", + return_value=set_licensing_info_mock_fetch_multiple_entitlements, + ) + data = { "type": "mhlm", "status": "starting", From 28ff96228009c5f92b03b640d42062c72be5a243 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Thu, 21 Aug 2025 08:42:37 +0000 Subject: [PATCH 04/16] Adds MWI_SESSION_NAME environment variable for custom browser tab titles. --- Advanced-Usage.md | 1 + gui/src/components/App/index.jsx | 12 ++++---- gui/src/selectors/index.js | 20 +++++++++++-- matlab_proxy/app.py | 2 ++ matlab_proxy/app_state.py | 4 +-- matlab_proxy/settings.py | 5 ++-- matlab_proxy/util/list_servers.py | 4 +-- .../util/mwi/environment_variables.py | 5 ++++ matlab_proxy/util/mwi/session_name.py | 27 +++++++++++++++++ tests/unit/test_app.py | 1 + tests/unit/test_settings.py | 29 +++++++++++++++++++ 11 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 matlab_proxy/util/mwi/session_name.py diff --git a/Advanced-Usage.md b/Advanced-Usage.md index 0f9348d6..a3cad287 100644 --- a/Advanced-Usage.md +++ b/Advanced-Usage.md @@ -33,6 +33,7 @@ The following table describes all the environment variables that you can set to | **MWI_PROCESS_START_TIMEOUT** | integer (optional) | `1234` | This field controls the time (in seconds) for which `matlab-proxy` waits for the processes it spins up, viz: MATLAB & Xvfb, to respond. By default, this value is `600 seconds`. A timeout could either indicate an issue with the spawned processes or be a symptom of a resource-constrained environment. Increase this value if your environment needs more time for the spawned processes to start.| | **MWI_MATLAB_STARTUP_SCRIPT** | string (optional) | `"addpath('/path/to/a/folder'), c=12"` | Executes string provided at MATLAB startup. For details, see [Run Custom MATLAB Startup Code](#run-custom-matlab-startup-code) | | **MWI_SHUTDOWN_ON_IDLE_TIMEOUT** | integer (optional) | 60 | Defines the duration in minutes, that `matlab-proxy` remains idle before shutting down. When you do not set the variable, `matlab-proxy` will not shut down when idle. For details, [see Shutdown on Idle](#shutdown-on-idle). | +| **MWI_SESSION_NAME** | string (optional) | "My MATLAB" | Specifies the title of the browser tab, displayed as `{MWI_SESSION_NAME} - MATLAB R20xxy`. Default value is `MATLAB R20XXy`. | ## Shutdown on Idle diff --git a/gui/src/components/App/index.jsx b/gui/src/components/App/index.jsx index 01e25394..3766818d 100644 --- a/gui/src/components/App/index.jsx +++ b/gui/src/components/App/index.jsx @@ -40,6 +40,7 @@ import { selectMatlabStarting, selectIsIdleTimeoutEnabled, selectMatlabStopping, + selectBrowserTitle, selectIntegrationName } from '../../selectors'; @@ -54,7 +55,7 @@ import blurredBackground from './MATLAB-env-blur.png'; import EntitlementSelector from '../EntitlementSelector'; import { BUFFER_TIMEOUT_DURATION, MWI_AUTH_TOKEN_NAME_FOR_HTTP } from '../../constants'; -function App () { +function App() { const dispatch = useDispatch(); const overlayVisible = useSelector(selectOverlayVisible); @@ -76,6 +77,7 @@ function App () { const isConcurrencyEnabled = useSelector(selectIsConcurrencyEnabled); const wasEverActive = useSelector(selectWasEverActive); const integrationName = useSelector(selectIntegrationName); + const browserTitle = useSelector(selectBrowserTitle); // Timeout duration is specified in seconds, but useTimeoutFn accepts timeout values in ms. const idleTimeoutDurationInMS = useSelector(selectIdleTimeoutDurationInMS); @@ -93,7 +95,7 @@ function App () { // callback that will fire once the IDLE timer expires - function terminationFn () { + function terminationFn() { // Reset the timer if MATLAB is either starting or stopping or is busy if (isMatlabStarting || isMatlabStopping || isMatlabBusy) { idleTimerReset(); @@ -303,7 +305,7 @@ function App () { if (hasFetchedEnvConfig) { const queryParams = parseQueryParams(window.location); const token = queryParams.get(MWI_AUTH_TOKEN_NAME_FOR_HTTP); - + document.title = `${browserTitle}`; if (token) { dispatch(updateAuthStatus(token)); } @@ -331,7 +333,7 @@ function App () { overlayContent = { - // Restart IDLE timer + // Restart IDLE timer idleTimerReset(); setIdleTimerHasExpired(false); @@ -386,7 +388,7 @@ function App () { if (matlabUp) { matlabJsd = (!authEnabled || isAuthenticated) ? () - : Blurred MATLAB environment; + : Blurred MATLAB environment; } const overlayTrigger = overlayVisible diff --git a/gui/src/selectors/index.js b/gui/src/selectors/index.js index 46c497e9..3ed0a4f5 100644 --- a/gui/src/selectors/index.js +++ b/gui/src/selectors/index.js @@ -260,9 +260,23 @@ export const selectIsIdleTimeoutEnabled = createSelector( export const selectIdleTimeoutDurationInMS = createSelector( selectIsIdleTimeoutEnabled, selectIdleTimeoutDuration, - (isTimeoutEnabled, idleTimeoutDuration) => { return isTimeoutEnabled - ? idleTimeoutDuration * 1000 - : undefined; } + (isTimeoutEnabled, idleTimeoutDuration) => { + return isTimeoutEnabled + ? idleTimeoutDuration * 1000 + : undefined; + } +); + +export const selectBrowserTitle = createSelector( + selectHasFetchedEnvConfig, + selectEnvConfig, + (hasFetchedEnvConfig, envConfig) => { + if (hasFetchedEnvConfig) { + return envConfig.browserTitle; + } else { + return 'MATLAB'; + } + } ); export const selectIntegrationName = createSelector( diff --git a/matlab_proxy/app.py b/matlab_proxy/app.py index 17cf1eeb..982ab343 100644 --- a/matlab_proxy/app.py +++ b/matlab_proxy/app.py @@ -209,6 +209,8 @@ async def get_env_config(req): "supportedVersions": constants.SUPPORTED_MATLAB_VERSIONS, } + config["browserTitle"] = state.settings["browser_title"] + # Send timeout duration for the idle timer as part of the response config["idleTimeoutDuration"] = state.settings["mwi_idle_timeout"] diff --git a/matlab_proxy/app_state.py b/matlab_proxy/app_state.py index ebbb3463..10090277 100644 --- a/matlab_proxy/app_state.py +++ b/matlab_proxy/app_state.py @@ -896,8 +896,8 @@ def create_server_info_file(self): mwi_server_info_file = mwi_logs_dir / "mwi_server.info" mwi_auth_token_str = token_auth.get_mwi_auth_token_access_str(self.settings) - with open(mwi_server_info_file, "w") as fh: - fh.write(self.settings["mwi_server_url"] + mwi_auth_token_str + "\n") + with open(mwi_server_info_file, "w", encoding="utf-8") as fh: + fh.write(self.settings["mwi_server_url"] + mwi_auth_token_str + "\n" + self.settings["browser_title"] + "\n") self.mwi_server_session_files["mwi_server_info_file"] = mwi_server_info_file logger.debug(f"Server info stored into: {mwi_server_info_file}") diff --git a/matlab_proxy/settings.py b/matlab_proxy/settings.py index 091b4ac3..0bc23615 100644 --- a/matlab_proxy/settings.py +++ b/matlab_proxy/settings.py @@ -21,7 +21,7 @@ from matlab_proxy.util import mwi, system from matlab_proxy.util.cookie_jar import HttpOnlyCookieJar from matlab_proxy.util.mwi import environment_variables as mwi_env -from matlab_proxy.util.mwi import token_auth +from matlab_proxy.util.mwi import token_auth, session_name from matlab_proxy.util.mwi.exceptions import ( FatalError, MatlabInstallError, @@ -222,6 +222,7 @@ def get_dev_settings(config): "is_windowmanager_available": False, "mwi_idle_timeout": None, "cookie_jar": _get_cookie_jar(), + "browser_title": session_name.get_browser_title("R2020b"), } @@ -324,7 +325,6 @@ def get_server_settings(config_name): ) cookie_jar = _get_cookie_jar() - return { "create_xvfb_cmd": create_xvfb_cmd, "base_url": mwi.validators.validate_base_url( @@ -406,6 +406,7 @@ def get_matlab_settings(): **mw_licensing_urls, "nlm_conn_str": nlm_conn_str, "has_custom_code_to_execute": has_custom_code_to_execute, + "browser_title": session_name.get_browser_title(matlab_version), } diff --git a/matlab_proxy/util/list_servers.py b/matlab_proxy/util/list_servers.py index 564a70a6..4dfb02ea 100644 --- a/matlab_proxy/util/list_servers.py +++ b/matlab_proxy/util/list_servers.py @@ -31,8 +31,8 @@ def print_server_info(): for server in search_results: server_number += 1 with open(server) as f: - server_info = f.read() - print_output += str(server_number) + ". " + str(server_info) + server_info = f.readline().strip() + print_output += str(server_number) + ". " + str(server_info) + "\n" print_output += str( mwi_util.prettify( diff --git a/matlab_proxy/util/mwi/environment_variables.py b/matlab_proxy/util/mwi/environment_variables.py index 001c3eef..79f10c22 100644 --- a/matlab_proxy/util/mwi/environment_variables.py +++ b/matlab_proxy/util/mwi/environment_variables.py @@ -172,6 +172,11 @@ def get_env_name_shutdown_on_idle_timeout(): return "MWI_SHUTDOWN_ON_IDLE_TIMEOUT" +def get_env_name_session_name(): + """User specified session name for the MATLAB Proxy instance, used to set the browser title.""" + return "MWI_SESSION_NAME" + + class Experimental: """This class houses functions which are undocumented APIs and Environment variables. Note: Never add any state to this class. Its only intended for use as an abstraction layer diff --git a/matlab_proxy/util/mwi/session_name.py b/matlab_proxy/util/mwi/session_name.py new file mode 100644 index 00000000..84dfb89f --- /dev/null +++ b/matlab_proxy/util/mwi/session_name.py @@ -0,0 +1,27 @@ +# Copyright 2025 The MathWorks, Inc. +"""This file provides functions to set a session name for the MATLAB Proxy instance.""" + +import os +from matlab_proxy.util.mwi import environment_variables as mwi_env +from matlab_proxy.util.mwi import logger as mwi_logger + +logger = mwi_logger.get() + +def _get_session_name(): + """Get the session name for the MATLAB Proxy instance. + + Returns: + str: returns the user-defined session name if set, otherwise returns None. + """ + return os.getenv(mwi_env.get_env_name_session_name(), None) + + +def get_browser_title(matlab_version) -> str: + """Get the browser title for the MATLAB Proxy instance.""" + + browser_title = "MATLAB " + (matlab_version or "") + session_name = _get_session_name() + if session_name: + browser_title = session_name + " - " + browser_title + logger.info("Session Name set to : %s", session_name) + return browser_title diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index aa86e141..3d1fa19e 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -360,6 +360,7 @@ async def test_get_env_config(test_server): "should_show_shutdown_button": True, "isConcurrencyEnabled": "foobar", "idleTimeoutDuration": 100, + "browserTitle": "MATLAB R2020b", } resp = await test_server.get("/get_env_config") assert resp.status == HTTPStatus.OK diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index e0e9add8..baa72f5e 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -635,6 +635,35 @@ def test_get_matlab_settings_valid_custom_matlab_root(mocker, monkeypatch, tmp_p assert matlab_settings["matlab_path"] == matlab_root_path assert matlab_settings["matlab_version"] == matlab_version assert matlab_settings["error"] is None + assert matlab_settings["browser_title"] == f"MATLAB {matlab_version}" + + +def test_get_matlab_settings_valid_custom_matlab_root_and_session_name( + mocker, monkeypatch, tmp_path +): + # Arrange + matlab_root_path = Path(tmp_path) + matlab_exec_path = matlab_root_path / "bin" / "matlab" + matlab_version = "R2024b" + monkeypatch.setenv(mwi_env.get_env_name_custom_matlab_root(), str(matlab_root_path)) + monkeypatch.setenv(mwi_env.get_env_name_session_name(), "Test Session") + mocker.patch( + "matlab_proxy.settings.mwi.validators.validate_matlab_root_path", + return_value=matlab_root_path, + ) + mocker.patch( + "matlab_proxy.settings.get_matlab_version", return_value=matlab_version + ) + + # Act + matlab_settings = settings.get_matlab_settings() + + # Assert + assert str(matlab_exec_path) in str(matlab_settings["matlab_cmd"][0]) + assert matlab_settings["matlab_path"] == matlab_root_path + assert matlab_settings["matlab_version"] == matlab_version + assert matlab_settings["error"] is None + assert matlab_settings["browser_title"] == f"Test Session - MATLAB {matlab_version}" def test_get_matlab_settings_invalid_custom_matlab_root(mocker, monkeypatch, tmp_path): From 5967f446759143b13d4fa292705312d4ab76cd4a Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 25 Aug 2025 14:23:49 +0530 Subject: [PATCH 05/16] Applies black formatting. --- matlab_proxy/app_state.py | 8 +++++++- matlab_proxy/util/mwi/session_name.py | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/matlab_proxy/app_state.py b/matlab_proxy/app_state.py index 10090277..b77d32a2 100644 --- a/matlab_proxy/app_state.py +++ b/matlab_proxy/app_state.py @@ -897,7 +897,13 @@ def create_server_info_file(self): mwi_server_info_file = mwi_logs_dir / "mwi_server.info" mwi_auth_token_str = token_auth.get_mwi_auth_token_access_str(self.settings) with open(mwi_server_info_file, "w", encoding="utf-8") as fh: - fh.write(self.settings["mwi_server_url"] + mwi_auth_token_str + "\n" + self.settings["browser_title"] + "\n") + fh.write( + self.settings["mwi_server_url"] + + mwi_auth_token_str + + "\n" + + self.settings["browser_title"] + + "\n" + ) self.mwi_server_session_files["mwi_server_info_file"] = mwi_server_info_file logger.debug(f"Server info stored into: {mwi_server_info_file}") diff --git a/matlab_proxy/util/mwi/session_name.py b/matlab_proxy/util/mwi/session_name.py index 84dfb89f..e1f7d597 100644 --- a/matlab_proxy/util/mwi/session_name.py +++ b/matlab_proxy/util/mwi/session_name.py @@ -7,6 +7,7 @@ logger = mwi_logger.get() + def _get_session_name(): """Get the session name for the MATLAB Proxy instance. From d035da0eab5595adc78bb02b19b5a4f88e9d10e6 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 25 Aug 2025 15:34:42 +0530 Subject: [PATCH 06/16] Update to v0.27.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d2fa1713..652841ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "matlab-proxy" -version = "0.26.0" +version = "0.27.0" description = "Python® package enables you to launch MATLAB® and access it from a web browser." readme = "README.md" license = "LicenseRef-MATHWORKS-CLOUD-REFERENCE-ARCHITECTURE-LICENSE" From c772f2e9a4c3196610ac617f67d94bd95ac0f123 Mon Sep 17 00:00:00 2001 From: Krishan Sharma Date: Mon, 1 Sep 2025 06:48:39 +0000 Subject: [PATCH 07/16] Adds retries to npm install step to avoid transient rate limiting issues. Example failure: https://github.com/mathworks/matlab-proxy/actions/runs/17205709807/job/48805769105 --- hatch_build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index 839f8573..f4f207cf 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -3,7 +3,7 @@ import os import subprocess from pathlib import Path -from shutil import which, copytree +from shutil import copytree, which from typing import Any, Dict from hatchling.builders.hooks.plugin.interface import BuildHookInterface @@ -23,7 +23,8 @@ def initialize(self, version: str, build_data: Dict[str, Any]) -> None: "npm must be installed and on the path during package build!" ) - npm_install = [npm_path, "install"] + # Adding retries to npm install to avoid transient rate limiting issues + npm_install = [npm_path, "install", "--fetch-retries", "10"] npm_build = [npm_path, "run", "build"] pwd = Path.cwd() From bec8206d3d5d8a1bcd5f15994c82502f35537510 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 1 Sep 2025 06:57:38 +0000 Subject: [PATCH 08/16] Removes warning about missing FLUXBOX executable from the settings panel, and updates deprecated API from the logging package. fixes mathworks/matlab-proxy#55 --- matlab_proxy/app_state.py | 2 +- matlab_proxy/settings.py | 5 ++--- matlab_proxy/util/__init__.py | 4 ++-- matlab_proxy/util/mwi/token_auth.py | 4 ++-- matlab_proxy/util/mwi/validators.py | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/matlab_proxy/app_state.py b/matlab_proxy/app_state.py index b77d32a2..d3e769c3 100644 --- a/matlab_proxy/app_state.py +++ b/matlab_proxy/app_state.py @@ -484,7 +484,7 @@ async def __update_matlab_state(self) -> None: "'busy' status endpoint returned an invalid response, falling back to using 'ping' endpoint to determine MATLAB state" ) warning = f"{mwi_env.get_env_name_shutdown_on_idle_timeout()} environment variable is supported only for MATLAB versions R2021a or later" - logger.warn(warning) + logger.warning(warning) self.warnings.append(warning) else: diff --git a/matlab_proxy/settings.py b/matlab_proxy/settings.py index 0bc23615..9f4402d8 100644 --- a/matlab_proxy/settings.py +++ b/matlab_proxy/settings.py @@ -289,7 +289,6 @@ def get(config_name=matlab_proxy.get_default_config_name(), dev=False): if not settings["is_windowmanager_available"]: warning = " Unable to find fluxbox on the system PATH. To use Simulink Online, add Fluxbox to the system PATH and restart matlab-proxy. For details, see https://github.com/mathworks/matlab-proxy#requirements." logger.warning(warning) - settings["warnings"].append(warning) settings.update(get_matlab_settings()) @@ -500,7 +499,7 @@ def _validate_ssl_files_and_get_ssl_context(mwi_config_folder): # Don't use SSL if the user has explicitly disabled SSL communication or not set the respective env var if not is_ssl_enabled: if ssl_cert_file: - logger.warn( + logger.warning( f"Ignoring provided SSL files, as {env_name_enable_ssl} is either unset or set to false" ) return None @@ -597,7 +596,7 @@ def generate_new_self_signed_certs(mwi_certs_dir): f.write(cert.public_bytes(serialization.Encoding.PEM)) except Exception as ex: - logger.warn( + logger.warning( f"Failed to generate self-signed certificates, proceeding with non-secure mode! Error: {ex}" ) cert_file = priv_key_file = None diff --git a/matlab_proxy/util/__init__.py b/matlab_proxy/util/__init__.py index d87ca40b..f3820cc6 100644 --- a/matlab_proxy/util/__init__.py +++ b/matlab_proxy/util/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 The MathWorks, Inc. +# Copyright 2020-2025 The MathWorks, Inc. import argparse import inspect import os @@ -312,7 +312,7 @@ def __init__(self, purpose): self._acquired_by = None self._lock = asyncio.Lock() if not purpose: - logger.warn("Provide a purpose for this instance of TrackingLock") + logger.warning("Provide a purpose for this instance of TrackingLock") self._purpose = purpose @property diff --git a/matlab_proxy/util/mwi/token_auth.py b/matlab_proxy/util/mwi/token_auth.py index 9129356f..55c9d02d 100644 --- a/matlab_proxy/util/mwi/token_auth.py +++ b/matlab_proxy/util/mwi/token_auth.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 The MathWorks, Inc. +# Copyright 2020-2025 The MathWorks, Inc. # This file contains functions required to enable token based authentication in the server. @@ -39,7 +39,7 @@ def generate_mwi_auth_token_and_hash(): if enable_token_auth == "false": if auth_token: - logger.warn( + logger.warning( f"Ignoring {env_name_mwi_auth_token}, as {env_name_enable_mwi_token_auth} explicitly set to false" ) return _format_token_as_dictionary(None) diff --git a/matlab_proxy/util/mwi/validators.py b/matlab_proxy/util/mwi/validators.py index dfca96da..ae87dffa 100644 --- a/matlab_proxy/util/mwi/validators.py +++ b/matlab_proxy/util/mwi/validators.py @@ -380,7 +380,7 @@ def validate_idle_timeout(timeout): return timeout except ValueError: - logger.warn( + logger.warning( f"Invalid value supplied for {mwi_env.get_env_name_shutdown_on_idle_timeout()}: {timeout}. Continuing without any IDLE timeout." ) return None From a259841ee609c7a3386a3810047ef5237eb8326d Mon Sep 17 00:00:00 2001 From: Krishan Sharma Date: Mon, 1 Sep 2025 08:25:43 +0000 Subject: [PATCH 09/16] Fixes issues related to updates from web logging API from AIOHTTP, and refactors shutdown process to use AIOHTTP's shutdown hooks instead of the cleanup hook. --- .vscode/settings.json | 9 +++-- matlab_proxy/app.py | 61 ++++++++++++++++++++------------- matlab_proxy/util/mwi/logger.py | 3 +- tests/unit/test_app.py | 5 +-- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3d9856b9..03fc5563 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,11 @@ "source.fixAll": "explicit", "source.organizeImports": "explicit" }, - "editor.defaultFormatter": "charliermarsh.ruff" - } + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } \ No newline at end of file diff --git a/matlab_proxy/app.py b/matlab_proxy/app.py index 982ab343..6eea605e 100644 --- a/matlab_proxy/app.py +++ b/matlab_proxy/app.py @@ -432,25 +432,38 @@ async def shutdown_integration_delete(req): req (HTTPRequest): HTTPRequest Object """ state = req.app["state"] - logger.info(f"Shutting down {state.settings['integration_name']}...") - # Send response manually because this has to happen before the application exits res = create_status_response(req.app, "../") - await res.prepare(req) - await res.write_eof() - # Gracefully shutdown the server - await req.app.shutdown() - await req.app.cleanup() + # Schedule the shutdown to happen after the response is sent + asyncio.create_task(_shutdown_after_response(req.app)) + + return res + + +async def _shutdown_after_response(app): + # Shutdown the application after a short delay to allow the response to be fully sent + # back to the client before the server is stopped + await asyncio.sleep(0.1) + + # aiohttp shutdown to be invoked before cleanup - + # https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Application.shutdown + await app.shutdown() + await app.cleanup() loop = util.get_event_loop() - # Run the current batch of coroutines in the event loop and then exit. - # This completes the loop.run_forever() blocking call and subsequent code - # in create_and_start_app() resumes execution. - loop.stop() - return res + # Cancel remaining tasks (except this one: _shutdown_after_response) + running_tasks = asyncio.all_tasks(loop) + current_task = asyncio.current_task() + if current_task: + running_tasks.discard(current_task) + + await util.cancel_tasks(running_tasks) + + # Stop the event loop from this task + loop.call_soon_threadsafe(loop.stop) # @token_auth.authenticate_access_decorator @@ -873,8 +886,6 @@ def configure_and_start(app): """ loop = util.get_event_loop() - web_logger = None if not mwi_env.is_web_logging_enabled() else logger - # Setup the session storage, # Uniqified per session to prevent multiple proxy servers on the same FQDN from interfering with each other. uniqify_session_cookie = secrets.token_hex() @@ -888,7 +899,9 @@ def configure_and_start(app): ) # Setup runner - runner = web.AppRunner(app, logger=web_logger, access_log=web_logger) + runner = web.AppRunner( + app, access_log=logger if mwi_env.is_web_logging_enabled() else None + ) loop.run_until_complete(runner.setup()) # Prepare site to start, then set port of the app. @@ -961,7 +974,7 @@ def create_app(config_name=matlab_proxy.get_default_config_name()): app.router.add_route("*", f"{base_url}", root_redirect) app.router.add_route("*", f"{base_url}/{{proxyPath:.*}}", matlab_view) - app.on_cleanup.append(cleanup_background_tasks) + app.on_shutdown.append(cleanup_background_tasks) return app @@ -1011,15 +1024,15 @@ def create_and_start_app(config_name): # After handling the interrupt, proceed with shutting down the server gracefully. try: + # aiohttp shutdown to be invoked before cleanup - + # https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Application.shutdown + loop.run_until_complete(app.shutdown()) + loop.run_until_complete(app.cleanup()) + running_tasks = asyncio.all_tasks(loop) - loop.run_until_complete( - asyncio.gather( - app.shutdown(), - app.cleanup(), - util.cancel_tasks(running_tasks), - return_exceptions=False, - ) - ) + + # Gracefully cancel all running background tasks + loop.run_until_complete(util.cancel_tasks(running_tasks)) except Exception: pass diff --git a/matlab_proxy/util/mwi/logger.py b/matlab_proxy/util/mwi/logger.py index 9d282d3f..fe339caf 100644 --- a/matlab_proxy/util/mwi/logger.py +++ b/matlab_proxy/util/mwi/logger.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 The MathWorks, Inc. +# Copyright 2020-2025 The MathWorks, Inc. """Functions to access & control the logging behavior of the app""" import logging @@ -8,7 +8,6 @@ from . import environment_variables as mwi_env - logging.getLogger("aiohttp_session").setLevel(logging.ERROR) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 3d1fa19e..0b01343e 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -79,9 +79,10 @@ def test_create_app(event_loop): # Verify router is configured with some routes assert test_server.router._resources is not None - # Verify app server has a cleanup task + # Verify app server has a shutdown task # By default there is 1 for clean up task - assert len(test_server.on_cleanup) > 1 + assert len(test_server.on_shutdown) == 1 + assert len(test_server.on_cleanup) == 1 event_loop.run_until_complete(test_server["state"].stop_server_tasks()) From 16844c8651d58193ce13a126727169fe621a6789 Mon Sep 17 00:00:00 2001 From: Sourabh Kondapaka Date: Mon, 8 Sep 2025 08:01:23 +0000 Subject: [PATCH 10/16] Fixes bugs related to overlapping CSS includes and removes deprecated tag. Fixes mathworks/matlab-proxy#63 --- gui/src/components/App/index.jsx | 2 -- gui/src/components/LicensingGatherer/MHLM.css | 7 +++++++ gui/src/components/LicensingGatherer/MHLM.jsx | 5 ++--- gui/src/components/MatlabJsd/index.jsx | 1 - 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 gui/src/components/LicensingGatherer/MHLM.css diff --git a/gui/src/components/App/index.jsx b/gui/src/components/App/index.jsx index 3766818d..319e4c1e 100644 --- a/gui/src/components/App/index.jsx +++ b/gui/src/components/App/index.jsx @@ -4,8 +4,6 @@ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react' import { useSelector, useDispatch } from 'react-redux'; import { useInterval, useTimeoutFn } from 'react-use'; import './App.css'; -import './3p/css/bootstrap.min.css'; -import './3p/css/site7.min.css'; import Confirmation from '../Confirmation'; import OverlayTrigger from '../OverlayTrigger'; import Overlay from '../Overlay'; diff --git a/gui/src/components/LicensingGatherer/MHLM.css b/gui/src/components/LicensingGatherer/MHLM.css new file mode 100644 index 00000000..2ffb4be9 --- /dev/null +++ b/gui/src/components/LicensingGatherer/MHLM.css @@ -0,0 +1,7 @@ +/* Copyright 2025 The MathWorks, Inc. */ + +iframe { + border: 0px; + height: 380px; + width: 100%; +} \ No newline at end of file diff --git a/gui/src/components/LicensingGatherer/MHLM.jsx b/gui/src/components/LicensingGatherer/MHLM.jsx index d6513be0..4908bb27 100644 --- a/gui/src/components/LicensingGatherer/MHLM.jsx +++ b/gui/src/components/LicensingGatherer/MHLM.jsx @@ -13,6 +13,8 @@ import { fetchSetLicensing } from '../../actionCreators'; +import './MHLM.css'; + // Send a generated nonce to the login iframe function setLoginNonce (username) { const clientNonce = (Math.random() + '').substr(2); @@ -150,9 +152,6 @@ function MHLM ({ mhlmLicensingInfo = null }) { id="loginframe" title="MathWorks Embedded Login" type="text/html" - height="380" - width="100%" - frameBorder="0" src={embeddedLoginUrl} onLoad={handleIFrameLoaded} > diff --git a/gui/src/components/MatlabJsd/index.jsx b/gui/src/components/MatlabJsd/index.jsx index bcb3cf6c..45862113 100644 --- a/gui/src/components/MatlabJsd/index.jsx +++ b/gui/src/components/MatlabJsd/index.jsx @@ -39,7 +39,6 @@ function MatlabJsd ({ url, iFrameRef, shouldListenForEvents, handleUserInteracti ref={iFrameRef} title="MATLAB JSD" src={url} - frameBorder="0" allowFullScreen /> ); From e91244f3ad8c566046d6ec5ecde9c0ce9657bfbf Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Sep 2025 13:32:09 +0530 Subject: [PATCH 11/16] Update to v0.27.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 652841ad..90b77f49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "matlab-proxy" -version = "0.27.0" +version = "0.27.1" description = "Python® package enables you to launch MATLAB® and access it from a web browser." readme = "README.md" license = "LicenseRef-MATHWORKS-CLOUD-REFERENCE-ARCHITECTURE-LICENSE" From 598f656355406d1bd590517679787295bbb20118 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Mon, 8 Sep 2025 08:15:49 +0000 Subject: [PATCH 12/16] Update environment variables with default values only if value has not been set. --- matlab_proxy/app_state.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/matlab_proxy/app_state.py b/matlab_proxy/app_state.py index d3e769c3..8cb62884 100644 --- a/matlab_proxy/app_state.py +++ b/matlab_proxy/app_state.py @@ -965,10 +965,11 @@ async def __setup_env_for_matlab(self) -> dict: matlab_env["MLM_LICENSE_FILE"] = self.licensing["conn_str"] # Env setup related to MATLAB - matlab_env["MW_CRASH_MODE"] = "native" - matlab_env["MATLAB_WORKER_CONFIG_ENABLE_LOCAL_PARCLUSTER"] = "true" - matlab_env["PCT_ENABLED"] = "true" - matlab_env["HTTP_MATLAB_CLIENT_GATEWAY_PUBLIC_PORT"] = "1" + ## Update the values only if it does not already exist in the environment + matlab_env["MW_CRASH_MODE"] = matlab_env.get("MW_CRASH_MODE", "native") + matlab_env["MATLAB_WORKER_CONFIG_ENABLE_LOCAL_PARCLUSTER"] = matlab_env.get( + "MATLAB_WORKER_CONFIG_ENABLE_LOCAL_PARCLUSTER", "true" + ) matlab_env["MW_DOCROOT"] = os.path.join("ui", "webgui", "src") matlab_env["MWAPIKEY"] = self.settings["mwapikey"] From af9bb5939137fd715eeb20861291cdad7c07da4d Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Fri, 26 Sep 2025 11:14:09 +0000 Subject: [PATCH 13/16] Adds rich formatting of logs and updates the default logging styles. Additionally, `matlab-proxy-app-list-servers` renders the output as a table, and now supports a `--quiet | -q` flag to only print links to running servers. --- matlab_proxy/app.py | 6 +- matlab_proxy/app_state.py | 33 ++-- matlab_proxy/settings.py | 5 +- matlab_proxy/util/__init__.py | 65 +++----- matlab_proxy/util/list_servers.py | 95 +++++++++--- .../util/mwi/environment_variables.py | 10 ++ matlab_proxy/util/mwi/logger.py | 145 ++++++++++++++---- matlab_proxy/util/mwi/validators.py | 2 +- pyproject.toml | 9 +- tests/unit/util/mwi/test_logger.py | 26 +++- tests/unit/util/test_util.py | 12 +- 11 files changed, 262 insertions(+), 146 deletions(-) diff --git a/matlab_proxy/app.py b/matlab_proxy/app.py index 6eea605e..6d7642b5 100644 --- a/matlab_proxy/app.py +++ b/matlab_proxy/app.py @@ -996,7 +996,7 @@ def configure_no_proxy_in_env(): os.environ["no_proxy"] = ",".join( set(existing_no_proxy_env + no_proxy_whitelist) ) - logger.info(f"Setting no_proxy to: {os.environ.get('no_proxy')}") + logger.debug(f"Setting no_proxy to: {os.environ.get('no_proxy')}") def create_and_start_app(config_name): @@ -1053,12 +1053,12 @@ def print_version_and_exit(): def main(): """Starting point of the integration. Creates the web app and runs indefinitely.""" - if util.parse_cli_args()["version"]: + if util.parse_main_cli_args()["version"]: print_version_and_exit() # The integration needs to be called with --config flag. # Parse the passed cli arguments. - desired_configuration_name = util.parse_cli_args()["config"] + desired_configuration_name = util.parse_main_cli_args()["config"] create_and_start_app(config_name=desired_configuration_name) diff --git a/matlab_proxy/app_state.py b/matlab_proxy/app_state.py index 8cb62884..376199b6 100644 --- a/matlab_proxy/app_state.py +++ b/matlab_proxy/app_state.py @@ -875,15 +875,8 @@ def create_logs_dir_for_MATLAB(self): user_code_output_file ) logger.info( - util.prettify( - boundary_filler="*", - text_arr=[ - f"When MATLAB starts, you can see the output for your startup code at:", - f"{self.matlab_session_files.get('startup_code_output_file', ' ')}", - ], - ) + f"The results of executing MWI_MATLAB_STARTUP_SCRIPT are stored at: {user_code_output_file} " ) - return def create_server_info_file(self): @@ -909,17 +902,17 @@ def create_server_info_file(self): # By default mwi_server_url usually points to 0.0.0.0 as the hostname, but this does not work well # on some browsers. Specifically on Safari (MacOS) - logger.info( - util.prettify( - boundary_filler="=", - text_arr=[ - f"Access MATLAB at:", - self.settings["mwi_server_url"].replace("0.0.0.0", "localhost") - + mwi_auth_token_str, - ], - ) + server_url = ( + self.settings["mwi_server_url"].replace("0.0.0.0", "localhost") + + mwi_auth_token_str ) + mwi.logger.log_startup_info( + title=f"matlab-proxy-app running on {self.settings['app_port']}", + matlab_url=server_url, + ) + logger.info(f"MATLAB Root: {self.settings['matlab_path']}") + def clean_up_mwi_server_session(self): # Clean up mwi_server_session_files try: @@ -1004,7 +997,7 @@ async def __setup_env_for_matlab(self) -> dict: # Set MW_CONNECTOR_CONTEXT_ROOT matlab_env["MW_CONNECTOR_CONTEXT_ROOT"] = self.settings.get("base_url", "/") - logger.info( + logger.debug( f"MW_CONNECTOR_CONTEXT_ROOT is set to: {matlab_env['MW_CONNECTOR_CONTEXT_ROOT']}" ) @@ -1316,6 +1309,8 @@ async def start_matlab(self, restart_matlab=False): # to MATLAB state by other functions/tasks until the lock is released, ensuring consistency. It's released early only in case of exceptions. await self.matlab_state_updater_lock.acquire() self.set_matlab_state("starting") + logger.info(f"Starting MATLAB...") + # Clear MATLAB errors and logging self.error = None self.logs["matlab"].clear() @@ -1535,7 +1530,7 @@ async def stop_matlab(self, force_quit=False): except: pass - logger.info("Stopped (any running) MATLAB process.") + logger.debug("Stopped (any running) MATLAB process.") # Terminating Xvfb if system.is_posix(): diff --git a/matlab_proxy/settings.py b/matlab_proxy/settings.py index 9f4402d8..32ba2d91 100644 --- a/matlab_proxy/settings.py +++ b/matlab_proxy/settings.py @@ -53,7 +53,7 @@ def get_process_startup_timeout(): ) return constants.DEFAULT_PROCESS_START_TIMEOUT - logger.info( + logger.debug( f"Using {constants.DEFAULT_PROCESS_START_TIMEOUT} seconds as the default timeout value" ) @@ -94,7 +94,7 @@ def get_matlab_executable_and_root_path(): if matlab_executable_path: matlab_root_path = Path(matlab_executable_path).resolve().parent.parent - logger.info(f"Found MATLAB executable at: {matlab_executable_path}") + logger.debug(f"MATLAB root folder: {matlab_root_path}") matlab_root_path = mwi.validators.validate_matlab_root_path( matlab_root_path, is_custom_matlab_root=False ) @@ -698,7 +698,6 @@ def _get_matlab_cmd(matlab_executable_path, code_to_execute, nlm_conn_str): "-nosplash", *flag_to_hide_desktop, "-softwareopengl", - # " v=mvm ", *matlab_lic_mode, "-externalUI", profile_matlab_startup, diff --git a/matlab_proxy/util/__init__.py b/matlab_proxy/util/__init__.py index f3820cc6..d108ad9a 100644 --- a/matlab_proxy/util/__init__.py +++ b/matlab_proxy/util/__init__.py @@ -26,7 +26,7 @@ interrupt_signal_caught = False -def parse_cli_args(): +def parse_main_cli_args(): """Parses CLI arguments passed to the main() function. Returns: @@ -56,6 +56,28 @@ def parse_cli_args(): return parsed_args +def parse_list_cli_args(): + """Parses CLI arguments passed to the matlab-proxy-app-list-servers entrypoint. + + Returns: + dict: Containing the parsed arguments + """ + # Parse the --config flag provided to the console script executable. + parsed_args = {} + parser = argparse.ArgumentParser() + parser.add_argument( + "-q", + "--quiet", + help="Return the server list without any additional text.", + action="store_true", + ) + args = parser.parse_args() + + parsed_args["quiet"] = args.quiet + + return parsed_args + + def prepare_site(app, runner): """Prepares to launch a TCPSite. If MWI_APP_PORT env variable is set, it will setup a site to launch on that port, else will launch on a random available port. @@ -148,47 +170,6 @@ def catch_interrupt_signal(*args): return loop -def prettify(boundary_filler=" ", text_arr=[]): - """Prettify array of strings with borders for stdout - - Args: - boundary_filler (str, optional): Upper and lower border filler for text. Defaults to " ". - text_arr (list, optional):The text array to prettify. Each element will be added to a newline. Defaults to []. - - Returns: - [str]: Prettified String - """ - - import sys - - if not sys.stdout.isatty(): - return ( - "\n============================\n" - + "\n".join(text_arr) - + "\n============================\n" - ) - - size = os.get_terminal_size() - cols, _ = size.columns, size.lines - - if any(len(text) > cols for text in text_arr): - result = "" - for text in text_arr: - result += text + "\n" - return result - - upper = "\n" + "".ljust(cols, boundary_filler) + "\n" if len(text_arr) > 0 else "" - lower = "".ljust(cols, boundary_filler) if len(text_arr) > 0 else "" - - content = "" - for text in text_arr: - content += text.center(cols) + "\n" - - result = upper + content + lower - - return result - - def get_child_processes(parent_process, max_attempts=10, sleep_interval=1): """Get list of child processes from a parent process. diff --git a/matlab_proxy/util/list_servers.py b/matlab_proxy/util/list_servers.py index 4dfb02ea..303e6136 100644 --- a/matlab_proxy/util/list_servers.py +++ b/matlab_proxy/util/list_servers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022 The MathWorks, Inc. +# Copyright (c) 2020-2025 The MathWorks, Inc. # Script to print information about all running matlab-proxy servers for current user on current machine. import glob @@ -7,6 +7,66 @@ import matlab_proxy.settings as mwi_settings import matlab_proxy.util as mwi_util +from datetime import datetime +from rich.console import Console +from rich.table import Table + +__NO_SERVERS_MSG = "No MATLAB-PROXY Servers are currently running." + + +def _extract_version_and_session(title): + """Extracts session name and MATLAB version from the title.""" + parts = title.split("-") + if len(parts) < 2: + return title.replace("MATLAB ", ""), "" + session_name = parts[0].strip() + matlab_version = parts[1].strip().replace("MATLAB ", "") + return matlab_version, session_name + + +def _get_server_info(server): + """Helper function to parse info from server file.""" + with open(server) as f: + # Assumes that the server file contains the address on the first line, + # the browser_title on the second line, and the timestamp is derived from the file's last modified time. + address = f.readline().strip() + browser_title = f.readline().strip() + matlab_version, session_name = _extract_version_and_session(browser_title) + timestamp = _get_timestamp(server) + return timestamp, matlab_version, session_name, address + + +def _print_server_info_as_table(servers): + console = Console() + table = Table( + title="MATLAB Proxy Servers", + title_style="cyan", + title_justify="center", + caption="No servers found." if not servers else "", + caption_style="bold red", + show_header=True, + header_style="yellow", + show_lines=True, + show_edge=True, + ) + table.add_column("Created On") + table.add_column("MATLAB\nVersion") + table.add_column("Session Name") + table.add_column("Server URL", overflow="fold") + + # Build server information + for server in servers: + table.add_row(*_get_server_info(server)) + + console.print(table) + + +def _get_timestamp(filename): + """Get the last modified timestamp of the file in a human-readable format.""" + timestamp = os.path.getmtime(filename) + readable_time = datetime.fromtimestamp(timestamp).strftime("%d/%m/%y %H:%M:%S") + return readable_time + def print_server_info(): """Print information about all matlab-proxy servers (with version > 0.4.0) running on this machine""" @@ -15,30 +75,15 @@ def print_server_info(): # Look for files in port folders ports_folder = home_folder / "ports" search_string = str(ports_folder) + "/**/mwi_server.info" + servers = sorted(glob.glob(search_string), key=os.path.getmtime) - print_output = str( - mwi_util.prettify( - boundary_filler="-", - text_arr=["Your running servers are:"], - ) - ) - print_output += "\n" - search_results = sorted(glob.glob(search_string), key=os.path.getmtime) - if len(search_results) == 0: - print_output += "No MATLAB-PROXY Servers are currently running." - else: - server_number = 0 - for server in search_results: - server_number += 1 + args = mwi_util.parse_list_cli_args() + + if args["quiet"]: + for server in servers: with open(server) as f: server_info = f.readline().strip() - print_output += str(server_number) + ". " + str(server_info) + "\n" - - print_output += str( - mwi_util.prettify( - boundary_filler="-", - text_arr=["Thank you."], - ) - ) - - return print_output + print(f"{server_info}", end="\n") + else: + _print_server_info_as_table(servers) + return diff --git a/matlab_proxy/util/mwi/environment_variables.py b/matlab_proxy/util/mwi/environment_variables.py index 79f10c22..8e5a3019 100644 --- a/matlab_proxy/util/mwi/environment_variables.py +++ b/matlab_proxy/util/mwi/environment_variables.py @@ -213,3 +213,13 @@ def get_env_name_use_cookie_cache(): def should_use_cookie_cache(): """Returns true if the cookie jar support is enabled.""" return _is_env_set_to_true(Experimental.get_env_name_use_cookie_cache()) + + @staticmethod + def get_env_name_use_rich_logging(): + """Set to True to enable rich logging to console.""" + return "MWI_USE_RICH_LOGGING" + + @staticmethod + def use_rich_logger(): + """Returns true if the cookie jar support is enabled.""" + return _is_env_set_to_true(Experimental.get_env_name_use_rich_logging()) diff --git a/matlab_proxy/util/mwi/logger.py b/matlab_proxy/util/mwi/logger.py index fe339caf..a23d2a81 100644 --- a/matlab_proxy/util/mwi/logger.py +++ b/matlab_proxy/util/mwi/logger.py @@ -1,12 +1,15 @@ # Copyright 2020-2025 The MathWorks, Inc. """Functions to access & control the logging behavior of the app""" +from . import environment_variables as mwi_env + +from pathlib import Path +from rich.console import Console +from rich.table import Table import logging import os import sys -from pathlib import Path - -from . import environment_variables as mwi_env +import time logging.getLogger("aiohttp_session").setLevel(logging.ERROR) @@ -47,26 +50,43 @@ def __set_logging_configuration(): Returns: Logger: Logger object with the set configuration. """ - # query for user specified environment variables + # Create the Logger for MATLABProxy + logger = __get_mw_logger() + + # log_level is either set by environment or is the default value. log_level = os.getenv( mwi_env.get_env_name_logging_level(), __get_default_log_level() ).upper() - valid = __is_valid_log_level(log_level) - - if not valid: + if __is_not_valid_log_level(log_level): default_log_level = __get_default_log_level() - logging.warn( + logging.warning( f"Unknown log level '{log_level}' set. Defaulting to log level '{default_log_level}'..." ) log_level = default_log_level - log_file = os.getenv(mwi_env.get_env_name_log_file(), None) - ## Set logging object - logger = __get_mw_logger() - try: - if log_file: + if mwi_env.Experimental.use_rich_logger(): + from rich.logging import RichHandler + + rich_handler = RichHandler( + keywords=[__get_mw_logger_name()], + ) + rich_handler.setFormatter(logging.Formatter("%(name)s %(message)s")) + logger.addHandler(rich_handler) + else: + colored_formatter = _ColoredFormatter( + "%(color)s[%(levelname)1.1s %(asctime)s %(name)s]%(end_color)s %(message)s" + ) + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(colored_formatter) + logger.addHandler(stream_handler) + + logger.setLevel(log_level) + + log_file = os.getenv(mwi_env.get_env_name_log_file(), None) + if log_file: + try: log_file = Path(log_file) # Need to create the file if it doesn't exist or else logging.FileHandler # would open it in 'write' mode instead of 'append' mode. @@ -74,27 +94,21 @@ def __set_logging_configuration(): logger.info(f"Initializing logger with log file:{log_file}") file_handler = logging.FileHandler(filename=log_file, mode="a") formatter = logging.Formatter( - fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + fmt="[%(levelname)s %(asctime)s %(name)s] %(message)s" ) file_handler.setFormatter(formatter) file_handler.setLevel(log_level) logger.addHandler(file_handler) - except PermissionError: - print(f"PermissionError: Permission denied to create log file at: {log_file}") - sys.exit(1) - - except Exception as err: - print(f"Failed to use log file: {log_file} with error: {err}") - sys.exit(1) - - # log_level is either set by environment or is the default value. - logger.info(f"Initializing logger with log_level: {log_level}") - logger.setLevel(log_level) + except PermissionError: + print( + f"PermissionError: Permission denied to create log file at: {log_file}" + ) + sys.exit(1) - # Allow other libraries used by this integration to - # also print their logs at the specified level - logging.basicConfig(level=log_level) + except Exception as err: + print(f"Failed to use log file: {log_file} with error: {err}") + sys.exit(1) return logger @@ -120,11 +134,82 @@ def __get_default_log_level(): return "INFO" -def __is_valid_log_level(log_level): +def __is_not_valid_log_level(log_level): """Helper to check if the log level is valid. Returns: Boolean: Whether log level exists """ - return hasattr(logging, log_level) + return log_level not in logging.getLevelNamesMapping().keys() + + +def log_startup_info(title=None, matlab_url=None): + """Logs the startup information to the console and log file if specified.""" + logger = __get_mw_logger() + print_as_table = False + header_info = "Access MATLAB at:" + + if sys.stdout.isatty(): + # Width cannot be determined in non-interactive sessions + console = Console() + # Number of additional characters used by the table + padding = 4 + print_as_table = len(matlab_url) + padding <= console.width + + if print_as_table: + table = Table( + caption=title, + show_header=False, + show_lines=True, + show_edge=True, + highlight=True, + expand=True, + ) + table.add_column(overflow="fold", style="bold green", justify="center") + table.add_row(header_info) + table.add_row(matlab_url) + console.print(table) + + if os.getenv(mwi_env.get_env_name_log_file(), None) or not print_as_table: + logger.critical(f"{header_info} {matlab_url}") + + +class _ColoredFormatter(logging.Formatter): + """Custom formatter to add colors based on log level and modify time format.""" + + def format(self, record): + # Example: Add 'color' and 'end_color' attributes based on log level + if record.levelno == logging.INFO: + record.color = "\033[32m" # Green + record.end_color = "\033[0m" + elif record.levelno == logging.DEBUG: + record.color = "\033[94m" # Blue + record.end_color = "\033[0m" + elif record.levelno == logging.WARNING: + record.color = "\033[93m" # Yellow + record.end_color = "\033[0m" + elif record.levelno == logging.ERROR: + record.color = "\033[91m" # Red + record.end_color = "\033[0m" + elif record.levelno == logging.CRITICAL: + record.color = "\033[35m" # Magenta + record.end_color = "\033[0m" + else: + record.color = "" + record.end_color = "" + + # Call the original format method + return super().format(record) + + def formatTime(self, record, datefmt=None): + # Default behavior of formatTime + ct = self.converter(record.created) + if datefmt: + s = time.strftime(datefmt, ct) + else: + t = time.strftime("%Y-%m-%d %H:%M:%S", ct) + s = "%s,%03d" % (t, record.msecs) + + # Replace the comma with a period + return s.replace(",", ".") diff --git a/matlab_proxy/util/mwi/validators.py b/matlab_proxy/util/mwi/validators.py index ae87dffa..fe4bfbc0 100644 --- a/matlab_proxy/util/mwi/validators.py +++ b/matlab_proxy/util/mwi/validators.py @@ -70,7 +70,7 @@ def validate_mlm_license_file(nlm_connections_str): # regex = Start of Line, Any number of 0-9 digits , @, any number of nonwhite space characters with "- _ ." allowed # "^[0-9]+[@](\w|\_|\-|\.)+$" # Server triad is of the form : port@host1 or port@host1,port@host2,port@host3 - nlm_connection_str_regex = "(^[0-9]+[@](\w|\_|\-|\.)+$)" + nlm_connection_str_regex = r"(^[0-9]+[@](\w|\_|\-|\.)+$)" error_message = ( f"MLM_LICENSE_FILE validation failed for {nlm_connections_str}. " f"If set, the MLM_LICENSE_FILE environment variable must contain server names (each of the form port@hostname) separated by ':' on unix or ';' on windows(server triads however must be comma seperated)" diff --git a/pyproject.toml b/pyproject.toml index 90b77f49..b13b593f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,13 +34,14 @@ classifiers = [ ] dependencies = [ - "aiohttp>=3.7.4", "aiohttp_session[secure]", - "psutil", - "watchdog", - "requests", + "aiohttp>=3.7.4", "importlib-metadata", "importlib-resources", + "psutil", + "requests", + "rich", + "watchdog", ] [project.urls] diff --git a/tests/unit/util/mwi/test_logger.py b/tests/unit/util/mwi/test_logger.py index 37ece6ff..17e995b8 100644 --- a/tests/unit/util/mwi/test_logger.py +++ b/tests/unit/util/mwi/test_logger.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 The MathWorks, Inc. +# Copyright 2020-2025 The MathWorks, Inc. """This file tests methods present in matlab_proxy/util/mwi_logger.py""" import logging @@ -7,6 +7,14 @@ from matlab_proxy.util.mwi import logger as mwi_logger +@pytest.fixture +def reset_logger_handlers(): + """Fixture to reset logger handlers after each test.""" + yield + logger = mwi_logger.get() + logger.handlers.clear() + + def test_get(): """This test checks if the get method returns a logger with expected name""" logger = mwi_logger.get() @@ -20,7 +28,7 @@ def test_get_mw_logger_name(): assert "MATLABProxyApp" == mwi_logger.__get_mw_logger_name() -def test_get_with_no_environment_variables(monkeypatch): +def test_get_with_no_environment_variables(monkeypatch, reset_logger_handlers): """This test checks if the get method returns a logger with default settings if no environment variable is set""" # Delete the environment variables if they do exist env_names_list = mwi_logger.get_environment_variable_names() @@ -29,10 +37,10 @@ def test_get_with_no_environment_variables(monkeypatch): logger = mwi_logger.get(init=True) assert logger.isEnabledFor(logging.INFO) == True - assert len(logger.handlers) == 0 + assert len(logger.handlers) == 1 -def test_get_with_environment_variables(monkeypatch, tmp_path): +def test_get_with_environment_variables(monkeypatch, tmp_path, reset_logger_handlers): """This test checks if the get method returns a logger with the specified settings""" env_names_list = mwi_logger.get_environment_variable_names() monkeypatch.setenv(env_names_list[0], "CRITICAL") @@ -44,8 +52,8 @@ def test_get_with_environment_variables(monkeypatch, tmp_path): assert logger.isEnabledFor(logging.CRITICAL) == True # Verify that environment variable setting the file is respected - assert len(logger.handlers) == 1 - assert os.path.basename(logger.handlers[0].baseFilename) == "testing123.log" + assert len(logger.handlers) == 2 + assert os.path.basename(logger.handlers[1].baseFilename) == "testing123.log" @pytest.mark.parametrize( @@ -60,7 +68,7 @@ def test_get_with_environment_variables(monkeypatch, tmp_path): ], ) def test_set_logging_configuration_known_logging_levels( - monkeypatch, log_level, expected_level + monkeypatch, log_level, expected_level, reset_logger_handlers ): """This test checks if the logger is set with correct level for known log levels""" env_names_list = mwi_logger.get_environment_variable_names() @@ -72,7 +80,9 @@ def test_set_logging_configuration_known_logging_levels( @pytest.mark.parametrize("log_level", ["ABC", "abc"]) -def test_set_logging_configuration_unknown_logging_levels(monkeypatch, log_level): +def test_set_logging_configuration_unknown_logging_levels( + monkeypatch, log_level, reset_logger_handlers +): """This test checks if the logger is set with INFO level for unknown log levels""" env_names_list = mwi_logger.get_environment_variable_names() monkeypatch.setenv(env_names_list[0], log_level) diff --git a/tests/unit/util/test_util.py b/tests/unit/util/test_util.py index c991c2ea..f6388e93 100644 --- a/tests/unit/util/test_util.py +++ b/tests/unit/util/test_util.py @@ -7,7 +7,7 @@ import inspect from matlab_proxy import util -from matlab_proxy.util import get_child_processes, system, add_signal_handlers, prettify +from matlab_proxy.util import get_child_processes, system, add_signal_handlers from matlab_proxy.util import system from matlab_proxy.util.mwi.exceptions import ( UIVisibleFatalError, @@ -41,16 +41,6 @@ def test_add_signal_handlers(event_loop: asyncio.AbstractEventLoop): assert signal.getsignal(interrupt_signal) is not None -def test_prettify(): - """Tests if text is prettified""" - txt_arr = ["Hello world"] - - prettified_txt = prettify(boundary_filler="=", text_arr=txt_arr) - - assert txt_arr[0] in prettified_txt - assert "=" in prettified_txt - - def test_get_child_processes_no_children_initially(mocker): import time From 80b09a9c7fd59112af1c27e5367326f814207bc2 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Fri, 26 Sep 2025 16:45:55 +0530 Subject: [PATCH 14/16] Update to v0.27.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b13b593f..abe80f0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "matlab-proxy" -version = "0.27.1" +version = "0.27.2" description = "Python® package enables you to launch MATLAB® and access it from a web browser." readme = "README.md" license = "LicenseRef-MATHWORKS-CLOUD-REFERENCE-ARCHITECTURE-LICENSE" From bdd5f28d537e39c3e70cb748e5b122f69081a31a Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Fri, 26 Sep 2025 22:45:12 +0530 Subject: [PATCH 15/16] Fixes to resolve unit test failures --- matlab_proxy/util/mwi/logger.py | 6 +++--- tests/unit/util/mwi/test_logger.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/matlab_proxy/util/mwi/logger.py b/matlab_proxy/util/mwi/logger.py index a23d2a81..8e28553b 100644 --- a/matlab_proxy/util/mwi/logger.py +++ b/matlab_proxy/util/mwi/logger.py @@ -58,7 +58,7 @@ def __set_logging_configuration(): mwi_env.get_env_name_logging_level(), __get_default_log_level() ).upper() - if __is_not_valid_log_level(log_level): + if __is_invalid_log_level(log_level): default_log_level = __get_default_log_level() logging.warning( f"Unknown log level '{log_level}' set. Defaulting to log level '{default_log_level}'..." @@ -134,14 +134,14 @@ def __get_default_log_level(): return "INFO" -def __is_not_valid_log_level(log_level): +def __is_invalid_log_level(log_level): """Helper to check if the log level is valid. Returns: Boolean: Whether log level exists """ - return log_level not in logging.getLevelNamesMapping().keys() + return not hasattr(logging, log_level) def log_startup_info(title=None, matlab_url=None): diff --git a/tests/unit/util/mwi/test_logger.py b/tests/unit/util/mwi/test_logger.py index 17e995b8..84367dca 100644 --- a/tests/unit/util/mwi/test_logger.py +++ b/tests/unit/util/mwi/test_logger.py @@ -10,7 +10,6 @@ @pytest.fixture def reset_logger_handlers(): """Fixture to reset logger handlers after each test.""" - yield logger = mwi_logger.get() logger.handlers.clear() From 03a10f0831332907a5ef21b4640f3229f698b3e6 Mon Sep 17 00:00:00 2001 From: Prabhakar Kumar Date: Fri, 26 Sep 2025 22:52:50 +0530 Subject: [PATCH 16/16] Update to v0.27.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index abe80f0f..4497fe79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "matlab-proxy" -version = "0.27.2" +version = "0.27.3" description = "Python® package enables you to launch MATLAB® and access it from a web browser." readme = "README.md" license = "LicenseRef-MATHWORKS-CLOUD-REFERENCE-ARCHITECTURE-LICENSE"