Skip to content

Commit 8e41745

Browse files
krisctlprabhakk-mw
authored andcommitted
Fixes issues related to base url configurations from jupyter environments.
1 parent 563e714 commit 8e41745

File tree

5 files changed

+150
-117
lines changed

5 files changed

+150
-117
lines changed

matlab_proxy_manager/lib/api.py

Lines changed: 106 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import os
44
import secrets
55
import subprocess
6-
from typing import List, Optional, Tuple
6+
from typing import List, Tuple
77

88
import matlab_proxy
9-
import matlab_proxy.util.system as mwi_sys
109
import matlab_proxy.util.mwi.environment_variables as mwi_env
10+
import matlab_proxy.util.system as mwi_sys
1111
from matlab_proxy_manager.storage.file_repository import FileRepository
1212
from matlab_proxy_manager.storage.server import ServerProcess
13-
from matlab_proxy_manager.utils import constants, helpers, logger, exceptions
13+
from matlab_proxy_manager.utils import constants, exceptions, helpers, logger
1414

1515
# Used to list all the public-facing APIs exported by this module.
1616
__all__ = ["shutdown", "start_matlab_proxy_for_kernel", "start_matlab_proxy_for_jsp"]
@@ -21,7 +21,7 @@
2121

2222

2323
async def start_matlab_proxy_for_kernel(
24-
caller_id: str, parent_id: str, is_shared_matlab: bool
24+
caller_id: str, parent_id: str, is_shared_matlab: bool, base_url_prefix: str = ""
2525
):
2626
"""
2727
Starts a MATLAB proxy server specifically for MATLAB Kernel.
@@ -30,12 +30,18 @@ async def start_matlab_proxy_for_kernel(
3030
set to None, for starting the MATLAB proxy server via proxy manager.
3131
"""
3232
return await _start_matlab_proxy(
33-
caller_id=caller_id, ctx=parent_id, is_shared_matlab=is_shared_matlab
33+
caller_id=caller_id,
34+
ctx=parent_id,
35+
is_shared_matlab=is_shared_matlab,
36+
base_url_prefix=base_url_prefix,
3437
)
3538

3639

3740
async def start_matlab_proxy_for_jsp(
38-
parent_id: str, is_shared_matlab: bool, mpm_auth_token: str
41+
parent_id: str,
42+
is_shared_matlab: bool,
43+
mpm_auth_token: str,
44+
base_url_prefix: str = "",
3945
):
4046
"""
4147
Starts a MATLAB proxy server specifically for Jupyter Server Proxy (JSP) - Open MATLAB launcher.
@@ -48,6 +54,7 @@ async def start_matlab_proxy_for_jsp(
4854
ctx=parent_id,
4955
is_shared_matlab=is_shared_matlab,
5056
mpm_auth_token=mpm_auth_token,
57+
base_url_prefix=base_url_prefix,
5158
)
5259

5360

@@ -66,122 +73,116 @@ async def _start_matlab_proxy(**options) -> dict:
6673
- ctx (str): The context in which the server is being started (parent pid).
6774
- is_shared_matlab (bool): Flag indicating if the MATLAB proxy is shared.
6875
- mpm_auth_token (Optional[str]): Authentication token for the MATLAB proxy manager.
76+
- base_url_prefix (Optional[str]): Custom URL path which gets added to mwi_base_url
6977
7078
Returns:
7179
dict: A dictionary representation of the server process, including any errors encountered.
7280
7381
Raises:
7482
ValueError: If `caller_id` is "default" and `is_shared_matlab` is False.
7583
"""
76-
# Validate arguments
77-
required_args: List[str] = ["caller_id", "ctx", "is_shared_matlab"]
78-
missing_args: List[str] = [arg for arg in required_args if arg not in options]
79-
80-
if missing_args:
81-
raise ValueError(f"Missing required arguments: {', '.join(missing_args)}")
84+
_validate_required_arguments(options)
8285

8386
caller_id: str = options["caller_id"]
8487
ctx: str = options["ctx"]
8588
is_shared_matlab: bool = options.get("is_shared_matlab", True)
86-
mpm_auth_token: Optional[str] = options.get("mpm_auth_token", None)
89+
mpm_auth_token = options.get("mpm_auth_token", None) or secrets.token_hex(32)
8790

8891
if not is_shared_matlab and caller_id == "default":
8992
raise ValueError(
9093
"Caller id cannot be default when matlab proxy is not shareable"
9194
)
9295

93-
mpm_auth_token = mpm_auth_token or secrets.token_hex(32)
94-
9596
# Cleanup stale entries before starting new instance of matlab proxy server
9697
helpers._are_orphaned_servers_deleted(ctx)
9798

98-
ident = caller_id if not is_shared_matlab else "default"
99-
key = f"{ctx}_{ident}"
100-
log.debug(
101-
"Starting matlab proxy using ctx=%s, ident=%s, is_shared_matlab=%s",
102-
ctx,
103-
ident,
104-
is_shared_matlab,
99+
client_id = caller_id if not is_shared_matlab else "default"
100+
matlab_session_dir = f"{ctx}_{client_id}"
101+
filename = f"{ctx}_{caller_id}"
102+
proxy_manager_root_dir = helpers.create_and_get_proxy_manager_data_dir()
103+
existing_matlab_proxy_process = ServerProcess.find_existing_server(
104+
proxy_manager_root_dir, matlab_session_dir
105105
)
106106

107-
data_dir = helpers.create_and_get_proxy_manager_data_dir()
108-
server_process = ServerProcess.find_existing_server(data_dir, key)
109-
110-
if server_process:
107+
if existing_matlab_proxy_process:
111108
log.debug("Found existing server for aliasing")
112109

113110
# Create a backend file for this caller for reference tracking
114-
helpers.create_state_file(data_dir, server_process, f"{ctx}_{caller_id}")
111+
helpers.create_state_file(
112+
proxy_manager_root_dir, existing_matlab_proxy_process, filename
113+
)
115114

116-
return server_process.as_dict()
115+
return existing_matlab_proxy_process.as_dict()
117116

118117
# Create a new matlab proxy server
119-
else:
120-
try:
121-
server_process = await _start_subprocess_and_check_for_readiness(
122-
ident, ctx, key, is_shared_matlab, mpm_auth_token
123-
)
124-
# Store the newly created server into filesystem
125-
helpers.create_state_file(data_dir, server_process, f"{ctx}_{caller_id}")
126-
return server_process.as_dict()
127-
128-
# Return a server process instance with the errors information set
129-
except exceptions.ProcessStartError as pse:
130-
return ServerProcess(errors=[str(pse)]).as_dict()
131-
except exceptions.ServerReadinessError as sre:
132-
return ServerProcess(errors=[str(sre)]).as_dict()
133-
except Exception as e:
134-
log.error("Error starting matlab proxy server: %s", str(e))
135-
return ServerProcess(errors=[str(e)]).as_dict()
136-
137-
138-
async def _start_subprocess_and_check_for_readiness(
139-
server_id: str, ctx: str, key: str, is_shared_matlab: bool, mpm_auth_token: str
140-
) -> ServerProcess:
141-
"""
142-
Starts a MATLAB proxy server.
118+
try:
119+
base_url_prefix = options.get("base_url_prefix", "")
143120

144-
This function performs the following steps:
145-
1. Prepares the command and environment variables required to start the MATLAB proxy server.
146-
2. Initializes the MATLAB proxy process.
147-
3. Checks if the MATLAB proxy server is ready.
148-
4. Creates and returns a ServerProcess instance if the server is ready.
121+
# Prepare matlab proxy command and required environment variables
122+
matlab_proxy_cmd, matlab_proxy_env = _prepare_cmd_and_env_for_matlab_proxy(
123+
client_id, base_url_prefix
124+
)
149125

150-
Args:
151-
server_id (str): Unique identifier for the server.
152-
ctx (str): Parent process ID.
153-
key (str): Unique key for identifying the server process.
154-
is_shared_matlab (bool): Indicates if the MATLAB instance is shared.
155-
mpm_auth_token (str): Authentication token for MATLAB proxy manager.
126+
log.debug(
127+
"Starting new matlab proxy server using ctx=%s, client_id=%s, is_shared_matlab=%s",
128+
ctx,
129+
client_id,
130+
is_shared_matlab,
131+
)
132+
# Start the matlab proxy process
133+
process_id, url = await _start_subprocess(matlab_proxy_cmd, matlab_proxy_env)
134+
log.debug("MATLAB proxy process url: %s", url)
135+
136+
matlab_proxy_process = ServerProcess(
137+
server_url=url,
138+
mwi_base_url=matlab_proxy_env.get(mwi_env.get_env_name_base_url()),
139+
headers=helpers.convert_mwi_env_vars_to_header_format(
140+
matlab_proxy_env, "MWI"
141+
),
142+
pid=str(process_id),
143+
parent_pid=ctx,
144+
id=matlab_session_dir,
145+
type="shared" if is_shared_matlab else "isolated",
146+
mpm_auth_token=mpm_auth_token,
147+
)
156148

157-
Returns:
158-
ServerProcess: An instance representing the MATLAB proxy server process.
149+
await _check_for_process_readiness(matlab_proxy_process)
159150

160-
Raises:
161-
ServerReadinessError: If the MATLAB proxy server is not ready after retries.
162-
"""
163-
log.debug("Starting new matlab proxy server")
151+
# Store the newly created server into filesystem
152+
helpers.create_state_file(
153+
proxy_manager_root_dir, matlab_proxy_process, filename
154+
)
155+
return matlab_proxy_process.as_dict()
164156

165-
# Prepare matlab proxy command and required environment variables
166-
matlab_proxy_cmd, matlab_proxy_env = _prepare_cmd_and_env_for_matlab_proxy(
167-
server_id
168-
)
157+
# Return a server process instance with the errors information set
158+
except exceptions.ProcessStartError as pse:
159+
return ServerProcess(errors=[str(pse)]).as_dict()
160+
except exceptions.ServerReadinessError as sre:
161+
return ServerProcess(errors=[str(sre)]).as_dict()
162+
except Exception as e:
163+
log.error("Error starting matlab proxy server: %s", str(e))
164+
return ServerProcess(errors=[str(e)]).as_dict()
169165

170-
# Start the matlab proxy process
171-
process_id, url = await _start_subprocess(matlab_proxy_cmd, matlab_proxy_env)
172-
173-
log.debug("MATLAB proxy process url: %s", url)
174-
matlab_proxy_process = ServerProcess(
175-
server_url=url,
176-
mwi_base_url=matlab_proxy_env.get(mwi_env.get_env_name_base_url()),
177-
headers=helpers.convert_mwi_env_vars_to_header_format(matlab_proxy_env, "MWI"),
178-
pid=str(process_id),
179-
parent_pid=ctx,
180-
id=key,
181-
type="shared" if is_shared_matlab else "isolated",
182-
mpm_auth_token=mpm_auth_token,
183-
)
184166

167+
def _validate_required_arguments(options):
168+
# Validates that all required arguments are present in the supplied values
169+
required_args: List[str] = ["caller_id", "ctx", "is_shared_matlab"]
170+
missing_args: List[str] = [arg for arg in required_args if arg not in options]
171+
172+
if missing_args:
173+
raise ValueError(f"Missing required arguments: {', '.join(missing_args)}")
174+
175+
176+
async def _check_for_process_readiness(matlab_proxy_process: ServerProcess):
177+
"""
178+
Checks if the MATLAB proxy server is ready.
179+
180+
Args:
181+
matlab_proxy_process (ServerProcess): Deserialized matlab-proxy process
182+
183+
Raises:
184+
ServerReadinessError: If the MATLAB proxy server is not ready after retries.
185+
"""
185186
# Check for the matlab proxy server readiness - with retries
186187
if not helpers.is_server_ready(
187188
url=matlab_proxy_process.absolute_url, retries=7, backoff_factor=0.5
@@ -192,10 +193,8 @@ async def _start_subprocess_and_check_for_readiness(
192193
matlab_proxy_process.shutdown()
193194
raise exceptions.ServerReadinessError()
194195

195-
return matlab_proxy_process
196-
197196

198-
def _prepare_cmd_and_env_for_matlab_proxy(server_id: str):
197+
def _prepare_cmd_and_env_for_matlab_proxy(client_id: str, base_url_prefix: str):
199198
"""
200199
Prepare the command and environment variables for starting the MATLAB proxy.
201200
@@ -215,7 +214,9 @@ def _prepare_cmd_and_env_for_matlab_proxy(server_id: str):
215214
config.get("extension_name"),
216215
]
217216

218-
mwi_base_url: str = f"{constants.MWI_BASE_URL_PREFIX}{server_id}"
217+
mwi_base_url = _construct_mwi_base_url(base_url_prefix, client_id)
218+
log.info("MWI_BASE_URL : %s", mwi_base_url)
219+
219220
input_env: dict = {
220221
"MWI_AUTH_TOKEN": secrets.token_urlsafe(32),
221222
"MWI_BASE_URL": mwi_base_url,
@@ -227,6 +228,19 @@ def _prepare_cmd_and_env_for_matlab_proxy(server_id: str):
227228
return matlab_proxy_cmd, matlab_proxy_env
228229

229230

231+
def _construct_mwi_base_url(base_url_prefix: str, client_id: str):
232+
# Converts to correct base url (e.g. /jupyter/, default to /jupyter/matlab/default)
233+
log.debug(
234+
"base_url_prefix_from_client: %s, client_id: %s", base_url_prefix, client_id
235+
)
236+
237+
if base_url_prefix:
238+
base_url_prefix = base_url_prefix.rstrip("/")
239+
prefix = constants.MWI_BASE_URL_PREFIX.strip("/")
240+
client_id = client_id.strip("/")
241+
return "/".join([base_url_prefix, prefix, client_id])
242+
243+
230244
async def _start_subprocess(cmd: list, env: dict) -> Tuple[int, str]:
231245
"""
232246
Initializes and starts a subprocess using the specified command and provided environment.
@@ -333,10 +347,10 @@ async def shutdown(parent_pid: str, caller_id: str, mpm_auth_token: str):
333347
)
334348
return
335349

350+
filename = f"{parent_pid}_{caller_id}"
336351
try:
337352
data_dir = helpers.create_and_get_proxy_manager_data_dir()
338353
storage = FileRepository(data_dir)
339-
filename = f"{parent_pid}_{caller_id}"
340354
full_file_path, server = storage.get(filename)
341355

342356
if not server:

matlab_proxy_manager/utils/environment_variables.py

Lines changed: 6 additions & 1 deletion
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
"""This file lists and exposes the environment variables which are used by proxy manager."""
33

44
import os
@@ -41,6 +41,11 @@ def get_env_name_mwi_mpm_parent_pid():
4141
return "MWI_MPM_PARENT_PID"
4242

4343

44+
def get_env_name_base_url_prefix():
45+
"""Used to specify the base url prefix for setting base url on matlab (e.g. Jupyter base url)"""
46+
return "MWI_MPM_BASE_URL_PREFIX"
47+
48+
4449
def is_web_logging_enabled():
4550
"""Returns true if the web logging is required to be enabled"""
4651
return _is_env_set_to_true(get_env_name_enable_web_logging())

matlab_proxy_manager/web/app.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async def cleanup_watcher(app):
109109
return app
110110

111111

112-
async def start_app(env_vars: namedtuple):
112+
async def start_app(env_vars):
113113
"""
114114
Initialize and start the web application.
115115
@@ -125,6 +125,7 @@ async def start_app(env_vars: namedtuple):
125125
app["port"] = env_vars.mpm_port
126126
app["auth_token"] = env_vars.mpm_auth_token
127127
app["parent_pid"] = env_vars.mpm_parent_pid
128+
app["base_url_prefix"] = env_vars.base_url_prefix
128129

129130
web_logger = None if not mwi_env.is_web_logging_enabled() else log
130131

@@ -196,8 +197,9 @@ async def _start_default_proxy(app):
196197
parent_id=app.get("parent_pid"),
197198
is_shared_matlab=True,
198199
mpm_auth_token=app.get("auth_token"),
200+
base_url_prefix=app.get("base_url_prefix"),
199201
)
200-
errors: list = server_process.get("errors")
202+
errors = server_process.get("errors")
201203

202204
# Raising an exception if there was an error starting the default MATLAB proxy
203205
if errors:
@@ -484,8 +486,10 @@ def _render_error_page(error_msg: str) -> web.Response:
484486
)
485487

486488

487-
def _fetch_and_validate_required_env_vars() -> namedtuple:
488-
EnvVars = namedtuple("EnvVars", ["mpm_port", "mpm_auth_token", "mpm_parent_pid"])
489+
def _fetch_and_validate_required_env_vars():
490+
EnvVars = namedtuple(
491+
"EnvVars", ["mpm_port", "mpm_auth_token", "mpm_parent_pid", "base_url_prefix"]
492+
)
489493

490494
port = os.getenv(mpm_env.get_env_name_mwi_mpm_port())
491495
mpm_auth_token = os.getenv(mpm_env.get_env_name_mwi_mpm_auth_token())
@@ -496,9 +500,13 @@ def _fetch_and_validate_required_env_vars() -> namedtuple:
496500
sys.exit(1)
497501

498502
try:
503+
base_url_prefix = os.getenv(mpm_env.get_env_name_base_url_prefix(), "")
499504
mwi_mpm_port: int = int(port)
500505
return EnvVars(
501-
mpm_port=mwi_mpm_port, mpm_auth_token=mpm_auth_token, mpm_parent_pid=ctx
506+
mpm_port=mwi_mpm_port,
507+
mpm_auth_token=mpm_auth_token,
508+
mpm_parent_pid=ctx,
509+
base_url_prefix=base_url_prefix,
502510
)
503511
except ValueError as ve:
504512
log.error("Error: Invalid type for port: %s", ve)
@@ -510,7 +518,7 @@ def main() -> None:
510518
The main entry point of the application. Starts the app and run until the shutdown
511519
signal to terminate the app is received.
512520
"""
513-
env_vars: namedtuple = _fetch_and_validate_required_env_vars()
521+
env_vars = _fetch_and_validate_required_env_vars()
514522
asyncio.run(start_app(env_vars))
515523

516524

0 commit comments

Comments
 (0)