Skip to content

Commit 77a83fa

Browse files
krisctlprabhakk-mw
authored andcommitted
Adds support for notebook magic %matlab [OPTIONS] in jupyter-matlab-proxy.
1 parent a7509d2 commit 77a83fa

File tree

10 files changed

+195
-108
lines changed

10 files changed

+195
-108
lines changed

matlab_proxy/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ async def get_env_config(req):
207207
config["matlab"] = {
208208
"version": state.settings["matlab_version"],
209209
"supportedVersions": constants.SUPPORTED_MATLAB_VERSIONS,
210+
"rootPath": str(state.settings.get("matlab_path", "")),
210211
}
211212

212213
config["browserTitle"] = state.settings["browser_title"]
@@ -432,8 +433,12 @@ async def shutdown_integration_delete(req):
432433
req (HTTPRequest): HTTPRequest Object
433434
"""
434435
state = req.app["state"]
435-
logger.info(f"Shutting down {state.settings['integration_name']}...")
436+
if state.is_shutting_down:
437+
logger.debug("Shutdown already in progress")
438+
return create_status_response(req.app, "../")
436439

440+
logger.info(f"Shutting down {state.settings['integration_name']}...")
441+
state.is_shutting_down = True
437442
res = create_status_response(req.app, "../")
438443

439444
# Schedule the shutdown to happen after the response is sent

matlab_proxy/app_state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ def __init__(self, settings):
154154
self.__decrement_idle_timer()
155155
)
156156

157+
# Flag to track if matlab-proxy is in the process of shutting down
158+
self.is_shutting_down: bool = False
159+
157160
def set_remaining_idle_timeout(self, new_timeout):
158161
"""Sets the remaining IDLE timeout after the validating checks.
159162

matlab_proxy_manager/lib/api.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,37 @@
2121

2222

2323
async def start_matlab_proxy_for_kernel(
24-
caller_id: str, parent_id: str, is_shared_matlab: bool, base_url_prefix: str = ""
24+
caller_id: str,
25+
parent_id: str,
26+
is_shared_matlab: bool,
27+
base_url_prefix: str = "",
28+
**kwargs,
2529
):
2630
"""
2731
Starts a MATLAB proxy server specifically for MATLAB Kernel.
2832
2933
This function is a wrapper around the `start_matlab_proxy` function, with mpm_auth_token
3034
set to None, for starting the MATLAB proxy server via proxy manager.
35+
36+
Args:
37+
caller_id (str): The identifier for the caller (kernel id).
38+
parent_id (str): The context in which the server is being started (parent pid).
39+
is_shared_matlab (bool): Flag indicating if the MATLAB proxy is shared.
40+
base_url_prefix (str, optional): Custom URL path which gets added to mwi_base_url. Defaults to "".
41+
**kwargs: Additional keyword arguments:
42+
env (Dict[str, str], optional): Dictionary of environment variables to set for the
43+
MATLAB proxy process. These variables can control various aspects of the MATLAB proxy
44+
behavior. Defaults to None.
45+
46+
Returns:
47+
dict: A dictionary representation of the server process, including any errors encountered.
3148
"""
3249
return await _start_matlab_proxy(
3350
caller_id=caller_id,
3451
ctx=parent_id,
3552
is_shared_matlab=is_shared_matlab,
3653
base_url_prefix=base_url_prefix,
54+
**kwargs,
3755
)
3856

3957

@@ -42,19 +60,34 @@ async def start_matlab_proxy_for_jsp(
4260
is_shared_matlab: bool,
4361
mpm_auth_token: str,
4462
base_url_prefix: str = "",
63+
**kwargs,
4564
):
4665
"""
4766
Starts a MATLAB proxy server specifically for Jupyter Server Proxy (JSP) - Open MATLAB launcher.
4867
4968
This function is a wrapper around the `start_matlab_proxy` function, providing
5069
a more specific context (mpm_auth_token) for starting the MATLAB proxy server via proxy manager.
70+
71+
Args:
72+
caller_id (str): The identifier for the caller (kernel id).
73+
parent_id (str): The context in which the server is being started (parent pid).
74+
is_shared_matlab (bool): Flag indicating if the MATLAB proxy is shared.
75+
base_url_prefix (str, optional): Custom URL path which gets added to mwi_base_url. Defaults to "".
76+
**kwargs: Additional keyword arguments:
77+
env (Dict[str, str], optional): Dictionary of environment variables to set for the
78+
MATLAB proxy process. These variables can control various aspects of the MATLAB proxy
79+
behavior. Defaults to None.
80+
81+
Returns:
82+
dict: A dictionary representation of the server process, including any errors encountered.
5183
"""
5284
return await _start_matlab_proxy(
5385
caller_id="jsp",
5486
ctx=parent_id,
5587
is_shared_matlab=is_shared_matlab,
5688
mpm_auth_token=mpm_auth_token,
5789
base_url_prefix=base_url_prefix,
90+
**kwargs,
5891
)
5992

6093

@@ -123,6 +156,11 @@ async def _start_matlab_proxy(**options) -> dict:
123156
client_id, base_url_prefix
124157
)
125158

159+
# Use client-provided environment variables, if available
160+
client_env_variables = options.get("env")
161+
if client_env_variables and isinstance(client_env_variables, dict):
162+
matlab_proxy_env.update(client_env_variables)
163+
126164
log.debug(
127165
"Starting new matlab proxy server using ctx=%s, client_id=%s, is_shared_matlab=%s",
128166
ctx,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright 2024-2025 The MathWorks, Inc.
2+
import re
23

34
MWI_BASE_URL_PREFIX = "/matlab/"
45
MWI_DEFAULT_MATLAB_PATH = MWI_BASE_URL_PREFIX + "default"
56
HEADER_MWI_MPM_CONTEXT = "MWI-MPM-CONTEXT"
67
HEADER_MWI_MPM_AUTH_TOKEN = "MWI-MPM-AUTH-TOKEN"
8+
MATLAB_IN_REQ_PATH_PATTERN = re.compile(r".*?/matlab/([^/]+)/(.*)")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2025 The MathWorks, Inc.
2+
3+
import functools
4+
from typing import Callable
5+
6+
from matlab_proxy_manager.utils.constants import (
7+
HEADER_MWI_MPM_CONTEXT,
8+
)
9+
from matlab_proxy_manager.utils.helpers import render_error_page
10+
11+
12+
def validate_incoming_request_decorator(endpoint: Callable):
13+
"""Decorator to validate incoming requests.
14+
15+
This decorator checks if the request contains the required MWI_MPM_CONTEXT header.
16+
If the header is not found, it returns an error page. Otherwise, it adds the context
17+
to the request object and proceeds with the endpoint execution.
18+
19+
Args:
20+
endpoint (Callable): The endpoint function to be decorated.
21+
22+
Returns:
23+
Callable: The wrapped function that validates the request before calling the endpoint.
24+
"""
25+
26+
@functools.wraps(endpoint)
27+
async def wrapper(req):
28+
ctx = req.headers.get(HEADER_MWI_MPM_CONTEXT)
29+
if not ctx:
30+
return render_error_page(
31+
f"Required header: ${HEADER_MWI_MPM_CONTEXT} not found in the request"
32+
)
33+
# Add ctx to the request object
34+
req.ctx = ctx
35+
return await endpoint(req)
36+
37+
return wrapper

matlab_proxy_manager/utils/helpers.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2024-2025 The MathWorks, Inc.
2+
import asyncio
23
import http
34
import os
45
import socket
@@ -194,7 +195,7 @@ def _delete_server_and_file(storage, servers) -> bool:
194195
return is_server_deleted
195196

196197

197-
def poll_for_server_deletion() -> None:
198+
async def poll_for_server_deletion(app: web.Application) -> None:
198199
"""
199200
Poll for server deletion for a specified timeout period.
200201
@@ -207,14 +208,17 @@ def poll_for_server_deletion() -> None:
207208
log.debug("Interrupt/termination signal caught, cleaning up resources")
208209
start_time = time.time()
209210

210-
while time.time() - start_time < timeout_in_seconds:
211-
is_server_deleted = _are_orphaned_servers_deleted()
212-
if is_server_deleted:
213-
log.debug("Servers deleted, breaking out of loop")
214-
break
215-
log.debug("Servers not deleted, waiting")
216-
# Sleep for a short interval before polling again
217-
time.sleep(0.5)
211+
try:
212+
while time.time() - start_time < timeout_in_seconds:
213+
is_server_deleted = _are_orphaned_servers_deleted()
214+
if is_server_deleted:
215+
log.debug("Servers deleted, breaking out of loop")
216+
break
217+
log.debug("Servers not deleted, waiting")
218+
# Sleep for a short interval before polling again
219+
await asyncio.sleep(0.5)
220+
except Exception as ex:
221+
log.debug("Error while polling for server deletion: %s", ex)
218222

219223

220224
@contextmanager
@@ -301,3 +305,10 @@ def create_state_file(data_dir, server_process, filename: str):
301305
raise IOError(
302306
f"Failed to create state file {filename} in {data_dir}, error: {e}"
303307
) from e
308+
309+
310+
def render_error_page(error_msg: str) -> web.Response:
311+
"""Returns 503 with error text"""
312+
return web.HTTPServiceUnavailable(
313+
text=f'<p style="color: red;">{error_msg}</p>', content_type="text/html"
314+
)

0 commit comments

Comments
 (0)