Skip to content

Commit

Permalink
Introduces environment variable MWI_MATLAB_STARTUP_SCRIPT to specify …
Browse files Browse the repository at this point in the history
…MATLAB code to run at startup.

fixes mathworks/jupyter-matlab-proxy#20
  • Loading branch information
SirajPersonal authored and prabhakk-mw committed May 24, 2024
1 parent 4aeb25c commit 0a02c95
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 7 deletions.
28 changes: 28 additions & 0 deletions Advanced-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The following table describes all the environment variables that you can set to
| **MWI_USE_EXISTING_LICENSE** | string (optional) | `"True"` | When set to True, matlab-proxy will not ask you for additional licensing information and will try to launch an already activated MATLAB on your system PATH.
| **MWI_CUSTOM_MATLAB_ROOT** | string (optional) | `"/path/to/matlab/root/"` | Optionally, provide a custom path to MATLAB root. For more information see [Adding MATLAB to System Path](#adding-matlab-to-system-path) |
| **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) |

## Adding MATLAB to System Path

Expand Down Expand Up @@ -134,6 +135,33 @@ Replace `your.machine.fqdn.com` with the FQDN for the machine on which the `ubun

The logs from the SQUID container terminal should show activity when attempting to login to MATLAB through matlab-proxy.

### Run Custom MATLAB Startup Code

Use the environment variable `MWI_MATLAB_STARTUP_SCRIPT` to specify MATLAB code to run at startup.

When you start MATLAB using `matlab-proxy`, MATLAB will first run a `startup.m` file, if one exists on your path. For details, see [User-defined startup script for MATLAB](https://www.mathworks.com/help/matlab/ref/startup.html). MATLAB will then run any code you have provided as a string to the `MWI_MATLAB_STARTUP_SCRIPT` environment variable.


You might want to run code at startup to:
1. Add a folder to the MATLAB search path before you run a script.
2. Set a constant in the workspace

For example, to set variables `c1` and `c2`, with values `124` and `'xyz'`, respectively, and to add the folder `C:\Windows\Temp` to the MATLAB search path, run the command:
```bash
env MWI_MATLAB_STARTUP_SCRIPT="c1=124, c2='xyz', addpath('C:\Windows\Temp')" matlab-proxy-app
```
To specify a script to run at startup, use the `run` command and provide the path to your script.
```bash
env MWI_MATLAB_STARTUP_SCRIPT="run('path/to/startup_script.m')" matlab-proxy-app
```

If the code you specify throws an error, then after MATLAB starts, you see a variable `MATLABCustomStartupCodeError` of type `MException` in the workspace. To see the error message, run `disp(MATLABCustomStartupCodeError.message)` in the command window.

Note: Restarting MATLAB from within `matlab-proxy` will run the specified code again.

#### Limitations

* Commands that require user input or open MATLAB editor windows are not supported. Using commands such as `keyboard`, `openExample` or `edit` will render `matlab-proxy` unresponsive.

----

Expand Down
6 changes: 6 additions & 0 deletions MATLAB-Licensing-Info.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ Based on the license that you want to use, you can follow the procedures outline
| ------ | ------ | ------ |
| You want your users to use online licensing | Campus-Wide, Individual | These licenses are already configured for use with MATLAB Proxy. When starting MATLAB, an end user will need to log in to their MathWorks account to use the license linked to it. |
| You want your users to use licenses administered using a network license manager | Campus-Wide, Concurrent, or Network Named User | You will need to embed the address of your network license manager using the `MLM_LICENSE_FILE` environment variable. See [Advanced-Usage.md](./Advanced-Usage.md). Otherwise, an end user will need to manually enter the address to the network license manager when they start MATLAB. Each instance of MATLAB will consume a license seat. Using network named user licenses is possible but is *not recommended*. All named users will need to be explicitly specified in the license manager options file. Named users may not use MathWorks products on more than two computers simultaneously - specifying a single, generic named user for the integration will not be viable. |

----

Copyright 2020-2024 The MathWorks, Inc.

----
25 changes: 22 additions & 3 deletions matlab_proxy/app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
CONNECTOR_SECUREPORT_FILENAME,
MATLAB_LOGS_FILE_NAME,
IS_CONCURRENCY_CHECK_ENABLED,
USER_CODE_OUTPUT_FILE_NAME,
)
from matlab_proxy.settings import (
get_process_startup_timeout,
)

from matlab_proxy.settings import get_process_startup_timeout

from matlab_proxy.util import mw, mwi, system, windows
from matlab_proxy.util.mwi import environment_variables as mwi_env
from matlab_proxy.util.mwi import token_auth
Expand Down Expand Up @@ -594,6 +595,24 @@ def create_logs_dir_for_MATLAB(self):
self.matlab_session_files["matlab_ready_file"] = matlab_ready_file

logger.debug(f"matlab_session_files:{self.matlab_session_files}")

# check if the user has provided any code or not
if self.settings.get("has_custom_code_to_execute"):
# Keep a reference to the user code output file in the matlab_session_files for cleanup
user_code_output_file = mwi_logs_dir / USER_CODE_OUTPUT_FILE_NAME
self.matlab_session_files["startup_code_output_file"] = (
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', ' ')}",
],
)
)

return

def create_server_info_file(self):
Expand Down
1 change: 1 addition & 0 deletions matlab_proxy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
VERSION_INFO_FILE_NAME: Final[str] = "VersionInfo.xml"
MAX_HTTP_REQUEST_SIZE: Final[int] = 500_000_000 # 500MB
MATLAB_LOGS_FILE_NAME: Final[str] = "matlab_logs.txt"
USER_CODE_OUTPUT_FILE_NAME: Final[str] = "startup_code_output.txt"

# Max startup duration in seconds for processes launched by matlab-proxy
# This constant is meant for internal use within matlab-proxy
Expand Down
51 changes: 51 additions & 0 deletions matlab_proxy/matlab/evaluateUserMatlabCode.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
% Copyright 2024 The MathWorks, Inc.

% Note:
% Any extra variable we are creating begins with `mwiInternal` to prevent
% potential conflicts with variables created by user code evaluated using evalc.
% Since evalc("user code") is executed in the base workspace, it might create
% variables that could overwrite our internal variables. To avoid polluting the
% user's workspace when MATLAB starts, we ensure to clear any internal variable
% that we create in the base workspace. We do not need to be concerned about
% variables in the function's workspace.

if ~isempty(getenv('MWI_MATLAB_STARTUP_SCRIPT')) && ~all(isspace(getenv('MWI_MATLAB_STARTUP_SCRIPT')))
try
% Evaluate the code from the environment variable and capture the output
mwiInternalResults = evalc(getenv('MWI_MATLAB_STARTUP_SCRIPT'));
% Write the results to the file
logOutputOrError(mwiInternalResults);
clear mwiInternalResults;
catch mwiInternalException
% Log the error message to the file
logOutputOrError(" ", mwiInternalException);
clear mwiInternalResults mwiInternalException;
error("Unable to run the startup code you specified. For details of the error, see the output file at " + fullfile(getenv('MATLAB_LOG_DIR'), "startup_code_output.txt"));
end

end

function logOutputOrError(userCodeResults, mwiInternalException)
% Logs the results of the user code execution if successful, otherwise logs the
% error information. It then closes the file handle.
%
% Inputs:
% userCodeResults - String containing the output from the user code.
% mwiInternalException - (Optional) MException object containing error details.
filePath = fullfile(getenv('MATLAB_LOG_DIR'), "startup_code_output.txt");
[fileHandle, ~] = fopen(filePath, 'w');
if nargin < 2
% Log the successful output of the user code
fprintf(fileHandle, " ");
fprintf(fileHandle, userCodeResults);
else
% Log the error information
fprintf(fileHandle, 'An error occurred in the following code:\n');
fprintf(fileHandle, getenv('MWI_MATLAB_STARTUP_SCRIPT'));
fprintf(fileHandle, '\n\nMessage: %s\n', mwiInternalException.message);
fprintf(fileHandle, '\nError Identifier: %s\n', mwiInternalException.identifier);
end
% Close the file handle
fclose(fileHandle);
end

4 changes: 2 additions & 2 deletions matlab_proxy/matlab/startup.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
% Copyright (c) 2020-2023 The MathWorks, Inc.
% Copyright 2020-2024 The MathWorks, Inc.

% Configure logged in user if possible
if ~isempty(getenv('MW_LOGIN_USER_ID'))
Expand Down Expand Up @@ -34,4 +34,4 @@
matlab_settings.matlab.addons.explorer.isExplorerSupported.TemporaryValue = false;

clear
clc
clc
20 changes: 18 additions & 2 deletions matlab_proxy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,10 @@ def get_matlab_settings():
flag_to_hide_desktop = ["-nodesktop"]
if system.is_windows():
flag_to_hide_desktop.extend(["-noDisplayDesktop", "-wait", "-log"])
matlab_startup_file = str(Path(__file__).resolve().parent / "matlab" / "startup.m")

matlab_code_dir = Path(__file__).resolve().parent / "matlab"
matlab_startup_file = str(matlab_code_dir / "startup.m")
matlab_code_file = str(matlab_code_dir / "evaluateUserMatlabCode.m")

matlab_version = get_matlab_version(matlab_root_path)

Expand All @@ -388,6 +391,18 @@ def get_matlab_settings():
profile_matlab_startup = (
"-timing" if mwi_env.Experimental.is_matlab_startup_profiling_enabled() else ""
)

has_custom_code_to_execute = (
len(os.getenv(mwi_env.get_env_name_custom_matlab_code(), "").strip()) > 0
)
mp_code_to_execute = f"try; run('{matlab_startup_file}'); catch MATLABProxyInitializationError; disp(MATLABProxyInitializationError.message); end;"
custom_code_to_execute = f"try; run('{matlab_code_file}'); catch MATLABCustomStartupCodeError; disp(MATLABCustomStartupCodeError.message); end;"
code_to_execute = (
mp_code_to_execute + custom_code_to_execute
if has_custom_code_to_execute
else mp_code_to_execute
)

return {
"matlab_path": matlab_root_path,
"matlab_version": matlab_version,
Expand All @@ -402,13 +417,14 @@ def get_matlab_settings():
*mpa_flags,
profile_matlab_startup,
"-r",
f"try; run('{matlab_startup_file}'); catch ME; disp(ME.message); end;",
code_to_execute,
],
"ws_env": ws_env,
"mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
"mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
"mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
"nlm_conn_str": nlm_conn_str,
"has_custom_code_to_execute": has_custom_code_to_execute,
}


Expand Down
5 changes: 5 additions & 0 deletions matlab_proxy/util/mwi/environment_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ def get_env_name_process_startup_timeout():
return "MWI_PROCESS_START_TIMEOUT"


def get_env_name_custom_matlab_code():
"""User specified MATLAB code that will be executed by matlab-proxy upon its start"""
return "MWI_MATLAB_STARTUP_SCRIPT"


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
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/test_app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
from typing import Optional

import pytest
from matlab_proxy import settings

from matlab_proxy import settings
from matlab_proxy.app_state import AppState
from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
from matlab_proxy.util.mwi.exceptions import LicensingError, MatlabError
from tests.unit.util import MockResponse

from matlab_proxy.constants import (
CONNECTOR_SECUREPORT_FILENAME,
USER_CODE_OUTPUT_FILE_NAME,
)


@pytest.fixture
def sample_settings_fixture(tmp_path):
Expand All @@ -35,6 +41,7 @@ def sample_settings_fixture(tmp_path):
"mwi_logs_root_dir": Path(settings.get_mwi_config_folder(dev=True)),
"app_port": 12345,
"mwapikey": "asdf",
"has_custom_code_to_execute": False,
}


Expand Down Expand Up @@ -662,3 +669,30 @@ async def test_detect_active_client_status_can_reset_active_client(app_state_fix
assert (
app_state_fixture.active_client == None
), f"Expected the active_client to be None"


@pytest.mark.parametrize(
"session_file_count, has_custom_code_to_execute", [(2, True), (1, False)]
)
def test_create_logs_dir_for_MATLAB(
app_state_fixture, session_file_count, has_custom_code_to_execute
):
"""Test to check create_logs_dir_for_MATLAB()
Args:
app_state_fixture (AppState): Object of AppState class with defaults set
"""
# Arrange
app_state_fixture.settings["has_custom_code_to_execute"] = (
has_custom_code_to_execute
)

# Act
app_state_fixture.create_logs_dir_for_MATLAB()

# Assert
for _, session_file_path in app_state_fixture.matlab_session_files.items():
# Check session files are present in mwi logs directory
assert app_state_fixture.mwi_logs_dir == Path(session_file_path).parent

assert len(app_state_fixture.matlab_session_files) == session_file_count
35 changes: 35 additions & 0 deletions tests/unit/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,38 @@ def test_get_ssl_context_with_invalid_custom_ssl_files_raises_exception(

with pytest.raises(Exception, match=exception_msg):
settings._validate_ssl_files_and_get_ssl_context(mwi_certs_dir)


@pytest.mark.parametrize(
"expected_value_for_has_custom_code, custom_code, has_custom_code_exception_matlab_cmd",
[(False, "", False), (True, "run(disp('MATLAB'))", True)],
ids=["No custom code to execute", "Has custom code to execute"],
)
def test_get_matlab_settings_custom_code(
monkeypatch,
mocker,
expected_value_for_has_custom_code,
custom_code,
has_custom_code_exception_matlab_cmd,
):
# Arrange
monkeypatch.setenv(mwi_env.get_env_name_custom_matlab_code(), custom_code)
mocker.patch(
"matlab_proxy.settings.get_matlab_executable_and_root_path",
return_value=("matlab", None),
)

# Act
matlab_settings = settings.get_matlab_settings()
exception_present_in_matlab_cmd = (
"MATLABCustomStartupCodeError" in matlab_settings["matlab_cmd"][-1]
)
print(matlab_settings)

# Assert
assert (
matlab_settings["has_custom_code_to_execute"]
== expected_value_for_has_custom_code
)

assert exception_present_in_matlab_cmd == has_custom_code_exception_matlab_cmd

0 comments on commit 0a02c95

Please sign in to comment.