Skip to content

Commit 489b962

Browse files
diningPhilosopher64krisctl
authored andcommitted
Fixes bug which stops the user interface from showing an error when MATLAB is not found on PATH.
fixes #39
1 parent 41f40e2 commit 489b962

File tree

6 files changed

+403
-125
lines changed

6 files changed

+403
-125
lines changed

matlab_proxy/app_state.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
import uuid
1111
from collections import deque
1212
from datetime import datetime, timedelta, timezone
13-
from typing import Final, Optional, Callable
13+
from typing import Callable, Final, Optional
1414

1515
from matlab_proxy import util
1616
from matlab_proxy.constants import (
17+
CHECK_MATLAB_STATUS_INTERVAL_SECONDS,
1718
CONNECTOR_SECUREPORT_FILENAME,
1819
IS_CONCURRENCY_CHECK_ENABLED,
1920
MATLAB_LOGS_FILE_NAME,
2021
USER_CODE_OUTPUT_FILE_NAME,
21-
CHECK_MATLAB_STATUS_INTERVAL_SECONDS,
2222
)
2323
from matlab_proxy.settings import get_process_startup_timeout
2424
from matlab_proxy.util import mw, mwi, system, windows
@@ -30,10 +30,10 @@
3030
FatalError,
3131
LicensingError,
3232
MatlabError,
33+
MatlabInstallError,
3334
OnlineLicensingError,
3435
UIVisibleFatalError,
3536
XvfbError,
36-
LockAcquisitionError,
3737
log_error,
3838
)
3939

@@ -90,10 +90,6 @@ def __init__(self, settings):
9090
self.error = settings["error"]
9191
self.warnings = settings["warnings"]
9292

93-
if self.error is not None:
94-
self.logs["matlab"].clear()
95-
return
96-
9793
# Keep track of when the Embedded connector starts.
9894
# Would be initialized appropriately by get_embedded_connector_state() task.
9995
self.embedded_connector_start_time = None
@@ -1109,6 +1105,12 @@ async def __start_matlab_process(self, matlab_env):
11091105
Returns:
11101106
(asyncio.subprocess.Process | psutil.Process): If process creation is successful, else return None.
11111107
"""
1108+
# If there's no matlab_cmd available, it means that MATLAB is not available on system PATH.
1109+
if not self.settings["matlab_cmd"]:
1110+
raise MatlabInstallError(
1111+
"Unable to find MATLAB on the system PATH. Add MATLAB to the system PATH, and restart matlab-proxy."
1112+
)
1113+
11121114
if system.is_posix():
11131115
import pty
11141116

@@ -1328,7 +1330,14 @@ async def start_matlab(self, restart_matlab=False):
13281330
# Start MATLAB Process
13291331
logger.debug("Starting MATLAB")
13301332

1331-
matlab = await self.__start_matlab_process(matlab_env)
1333+
try:
1334+
matlab = await self.__start_matlab_process(matlab_env)
1335+
1336+
# If there's an error with starting MATLAB, set the error to the state and matlab to None
1337+
except MatlabInstallError as err:
1338+
log_error(logger, err)
1339+
self.error = err
1340+
matlab = None
13321341

13331342
# Release the lock after MATLAB process has started.
13341343
await self.matlab_state_updater_lock.release()

matlab_proxy/settings.py

+136-86
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2024 The MathWorks, Inc.
1+
# Copyright 2020-2025 The MathWorks, Inc.
22

33
import datetime
44
import os
@@ -47,7 +47,7 @@ def get_process_startup_timeout():
4747
return int(custom_startup_timeout)
4848

4949
else:
50-
logger.warn(
50+
logger.warning(
5151
f"The value set for {mwi_env.get_env_name_process_startup_timeout()}:{custom_startup_timeout} is not a number. Using {constants.DEFAULT_PROCESS_START_TIMEOUT} as the default value"
5252
)
5353
return constants.DEFAULT_PROCESS_START_TIMEOUT
@@ -74,7 +74,7 @@ def get_matlab_executable_and_root_path():
7474
matlab_root_path = Path(custom_matlab_root_path)
7575

7676
# Terminate process if invalid Custom Path was provided!
77-
mwi.validators.validate_matlab_root_path(
77+
matlab_root_path = mwi.validators.validate_matlab_root_path(
7878
matlab_root_path, is_custom_matlab_root=True
7979
)
8080

@@ -101,10 +101,9 @@ def get_matlab_executable_and_root_path():
101101

102102
# Control only gets here if custom matlab root was not set AND which matlab returned no results.
103103
# Note, error messages are formatted as multi-line strings and the front end displays them as is.
104-
error_message = "Unable to find MATLAB on the system PATH. Add MATLAB to the system PATH, and restart matlab-proxy."
105-
106-
logger.error(error_message)
107-
raise MatlabInstallError(error_message)
104+
raise MatlabInstallError(
105+
"Unable to find MATLAB on the system PATH. Add MATLAB to the system PATH, and restart matlab-proxy."
106+
)
108107

109108

110109
def get_matlab_version(matlab_root_path):
@@ -126,7 +125,16 @@ def get_matlab_version(matlab_root_path):
126125
tree = ET.parse(version_info_file_path)
127126
root = tree.getroot()
128127

129-
return root.find("release").text
128+
matlab_version = root.find("release").text
129+
130+
# If the matlab on system PATH is a wrapper script, then it would not be possible to determine MATLAB root (inturn not being able to determine MATLAB version)
131+
# unless MWI_CUSTOM_MATLAB_ROOT is set. Raising only a warning as the matlab version is only required for communicating with MHLM.
132+
if not matlab_version:
133+
logger.warning(
134+
f"Could not determine MATLAB version from MATLAB root path: {matlab_root_path}. Set {mwi_env.get_env_name_custom_matlab_root()} to a valid MATLAB root path"
135+
)
136+
137+
return matlab_version
130138

131139

132140
def get_ws_env_settings():
@@ -270,15 +278,7 @@ def get(config_name=matlab_proxy.get_default_config_name(), dev=False):
270278
logger.warning(warning)
271279
settings["warnings"].append(warning)
272280

273-
try:
274-
# Update settings with matlab specific values.
275-
settings.update(get_matlab_settings())
276-
except UIVisibleFatalError as error:
277-
logger.error(f"Exception raised during initialization: {error}")
278-
settings["error"] = error
279-
# Exceptions of this kind must propagate to the UI.
280-
# Returning settings that have been created without exceptions
281-
pass
281+
settings.update(get_matlab_settings())
282282

283283
return settings
284284

@@ -362,84 +362,33 @@ def get_matlab_settings():
362362
Unless they are of type UIVisibleFatalError
363363
"""
364364

365-
matlab_executable_path, matlab_root_path = get_matlab_executable_and_root_path()
366-
367365
ws_env, ws_env_suffix = get_ws_env_settings()
366+
mw_licensing_urls = _get_mw_licensing_urls(ws_env_suffix)
367+
nlm_conn_str = _get_nlm_conn_str()
368+
has_custom_code_to_execute, code_to_execute = _get_matlab_code_to_execute()
369+
err = None
368370

369-
# MATLAB Proxy gives precedence to the licensing information conveyed
370-
# by the user. If MLM_LICENSE_FILE is set, it should be prioritised over
371-
# other ways of licensing. But existence of license_info.xml in matlab/licenses
372-
# folder may cause hinderance in this workflow. So specifying -licmode as 'file'
373-
# overrides license_info.xml and enforces MLM_LICENSE_FILE to be the topmost priority
374-
375-
# NLM Connection String provided by MLM_LICENSE_FILE environment variable
376-
nlm_conn_str = mwi.validators.validate_mlm_license_file(
377-
os.environ.get(mwi_env.get_env_name_network_license_manager())
378-
)
379-
matlab_lic_mode = ["-licmode", "file"] if nlm_conn_str else ""
380-
# flag to hide MATLAB Window
381-
flag_to_hide_desktop = ["-nodesktop"]
382-
if system.is_windows():
383-
flag_to_hide_desktop.extend(["-noDisplayDesktop", "-wait", "-log"])
371+
try:
372+
matlab_executable_path, matlab_root_path = get_matlab_executable_and_root_path()
384373

385-
matlab_code_dir = Path(__file__).resolve().parent / "matlab"
386-
matlab_startup_file = str(matlab_code_dir / "startup.m")
387-
matlab_code_file = str(matlab_code_dir / "evaluateUserMatlabCode.m")
374+
except UIVisibleFatalError as error:
375+
logger.error(f"Exception raised during initialization: {error}")
376+
# Set matlab root and executable path to None as MATLAB root could not be determined
377+
matlab_executable_path = matlab_root_path = None
378+
err = error
388379

389380
matlab_version = get_matlab_version(matlab_root_path)
390-
391-
# If the matlab on system PATH is a wrapper script, then it would not be possible to determine MATLAB root (inturn not being able to determine MATLAB version)
392-
# unless MWI_CUSTOM_MATLAB_ROOT is set. Raising only a warning as the matlab version is only required for communicating with MHLM.
393-
if not matlab_version:
394-
logger.warn(
395-
f"Could not determine MATLAB version from MATLAB root path: {matlab_root_path}"
396-
)
397-
logger.warn(
398-
f"Set {mwi_env.get_env_name_custom_matlab_root()} to a valid MATLAB root path"
399-
)
400-
401-
mpa_flags = (
402-
mwi_env.Experimental.get_mpa_flags()
403-
if mwi_env.Experimental.is_mpa_enabled()
404-
else ""
405-
)
406-
profile_matlab_startup = (
407-
"-timing" if mwi_env.Experimental.is_matlab_startup_profiling_enabled() else ""
408-
)
409-
410-
has_custom_code_to_execute = (
411-
len(os.getenv(mwi_env.get_env_name_custom_matlab_code(), "").strip()) > 0
412-
)
413-
414-
# Sanitize file paths to avoid MATLAB not running the script due to early breakup of character array.
415-
mp_code_to_execute = f"try; run('{_sanitize_file_path_for_matlab(matlab_startup_file)}'); catch MATLABProxyInitializationError; disp(MATLABProxyInitializationError.message); end;"
416-
custom_code_to_execute = f"try; run('{_sanitize_file_path_for_matlab(matlab_code_file)}'); catch MATLABCustomStartupCodeError; disp(MATLABCustomStartupCodeError.message); end;"
417-
code_to_execute = (
418-
mp_code_to_execute + custom_code_to_execute
419-
if has_custom_code_to_execute
420-
else mp_code_to_execute
421-
)
381+
matlab_version_determined_on_startup = bool(matlab_version)
382+
matlab_cmd = _get_matlab_cmd(matlab_executable_path, code_to_execute, nlm_conn_str)
422383

423384
return {
424-
"matlab_path": matlab_root_path,
385+
"error": err,
425386
"matlab_version": matlab_version,
426-
"matlab_version_determined_on_startup": True if matlab_version else False,
427-
"matlab_cmd": [
428-
matlab_executable_path,
429-
"-nosplash",
430-
*flag_to_hide_desktop,
431-
"-softwareopengl",
432-
# " v=mvm ",
433-
*matlab_lic_mode,
434-
*mpa_flags,
435-
profile_matlab_startup,
436-
"-r",
437-
code_to_execute,
438-
],
387+
"matlab_path": matlab_root_path,
388+
"matlab_version_determined_on_startup": matlab_version_determined_on_startup,
389+
"matlab_cmd": matlab_cmd,
439390
"ws_env": ws_env,
440-
"mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
441-
"mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
442-
"mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
391+
**mw_licensing_urls,
443392
"nlm_conn_str": nlm_conn_str,
444393
"has_custom_code_to_execute": has_custom_code_to_execute,
445394
}
@@ -646,3 +595,104 @@ def _sanitize_file_path_for_matlab(filepath: str) -> str:
646595
"""
647596
filepath_with_single_quotes_escaped = filepath.replace("'", "''")
648597
return filepath_with_single_quotes_escaped
598+
599+
600+
def _get_matlab_code_to_execute():
601+
"""Returns the code that needs to run on MATLAB startup.
602+
Will check for user provided custom MATLAB code and execute it along with the default startup script.
603+
604+
Returns:
605+
tuple: With the first value representing whether there is custom MATLAB code to execute, and the second value representing the MATLAB code to execute.
606+
"""
607+
matlab_code_dir = Path(__file__).resolve().parent / "matlab"
608+
matlab_startup_file = str(matlab_code_dir / "startup.m")
609+
matlab_code_file = str(matlab_code_dir / "evaluateUserMatlabCode.m")
610+
611+
has_custom_code_to_execute = (
612+
len(os.getenv(mwi_env.get_env_name_custom_matlab_code(), "").strip()) > 0
613+
)
614+
615+
# Sanitize file paths to avoid MATLAB not running the script due to early breakup of character array.
616+
mp_code_to_execute = f"try; run('{_sanitize_file_path_for_matlab(matlab_startup_file)}'); catch MATLABProxyInitializationError; disp(MATLABProxyInitializationError.message); end;"
617+
custom_code_to_execute = f"try; run('{_sanitize_file_path_for_matlab(matlab_code_file)}'); catch MATLABCustomStartupCodeError; disp(MATLABCustomStartupCodeError.message); end;"
618+
code_to_execute = (
619+
mp_code_to_execute + custom_code_to_execute
620+
if has_custom_code_to_execute
621+
else mp_code_to_execute
622+
)
623+
624+
return has_custom_code_to_execute, code_to_execute
625+
626+
627+
def _get_nlm_conn_str():
628+
"""Get the Network License Manager (NLM) connection string.
629+
630+
Returns:
631+
str: The NLM connection string provided by the MLM_LICENSE_FILE environment variable.
632+
"""
633+
# NLM Connection String provided by MLM_LICENSE_FILE environment variable
634+
nlm_conn_str = mwi.validators.validate_mlm_license_file(
635+
os.environ.get(mwi_env.get_env_name_network_license_manager())
636+
)
637+
638+
return nlm_conn_str
639+
640+
641+
def _get_mw_licensing_urls(ws_env_suffix):
642+
"""Get the MathWorks licensing URLs.
643+
644+
Args:
645+
ws_env_suffix (str): The environment suffix for the licensing URLs.
646+
647+
Returns:
648+
dict: A dictionary containing the MathWorks licensing URLs for authentication and entitlement.
649+
"""
650+
return {
651+
"mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
652+
"mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
653+
"mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
654+
}
655+
656+
657+
def _get_matlab_cmd(matlab_executable_path, code_to_execute, nlm_conn_str):
658+
"""Construct the MATLAB command with appropriate flags and arguments.
659+
660+
Args:
661+
matlab_executable_path (str): The path to the MATLAB executable.
662+
code_to_execute (str): The MATLAB code to execute on startup.
663+
nlm_conn_str (str): The Network License Manager connection string.
664+
665+
Returns:
666+
list: A list of command-line arguments to launch MATLAB with the specified configuration.
667+
"""
668+
if not matlab_executable_path:
669+
return None
670+
671+
matlab_lic_mode = ["-licmode", "file"] if nlm_conn_str else ""
672+
# flag to hide MATLAB Window
673+
flag_to_hide_desktop = ["-nodesktop"]
674+
if system.is_windows():
675+
flag_to_hide_desktop.extend(["-noDisplayDesktop", "-wait", "-log"])
676+
677+
mpa_flags = (
678+
mwi_env.Experimental.get_mpa_flags()
679+
if mwi_env.Experimental.is_mpa_enabled()
680+
else ""
681+
)
682+
683+
profile_matlab_startup = (
684+
"-timing" if mwi_env.Experimental.is_matlab_startup_profiling_enabled() else ""
685+
)
686+
687+
return [
688+
matlab_executable_path,
689+
"-nosplash",
690+
*flag_to_hide_desktop,
691+
"-softwareopengl",
692+
# " v=mvm ",
693+
*matlab_lic_mode,
694+
*mpa_flags,
695+
profile_matlab_startup,
696+
"-r",
697+
code_to_execute,
698+
]

0 commit comments

Comments
 (0)