33import os
44import secrets
55import subprocess
6- from typing import List , Optional , Tuple
6+ from typing import List , Tuple
77
88import matlab_proxy
9- import matlab_proxy .util .system as mwi_sys
109import matlab_proxy .util .mwi .environment_variables as mwi_env
10+ import matlab_proxy .util .system as mwi_sys
1111from matlab_proxy_manager .storage .file_repository import FileRepository
1212from 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" ]
2121
2222
2323async 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
3740async 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+
230244async 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 :
0 commit comments