matlab-proxy 0.5.3__py3-none-any.whl → 0.30.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matlab_proxy/app.py +578 -205
- matlab_proxy/app_state.py +1061 -431
- matlab_proxy/constants.py +37 -0
- matlab_proxy/default_configuration.py +39 -4
- matlab_proxy/devel.py +18 -22
- matlab_proxy/gui/index.html +20 -1
- matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
- matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
- matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
- matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
- matlab_proxy/matlab/startup.m +3 -28
- matlab_proxy/settings.py +543 -112
- matlab_proxy/util/__init__.py +187 -59
- matlab_proxy/util/cookie_jar.py +72 -0
- matlab_proxy/util/event_loop.py +28 -10
- matlab_proxy/util/list_servers.py +71 -26
- matlab_proxy/util/mw.py +16 -15
- matlab_proxy/util/mwi/download.py +136 -0
- matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
- matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
- matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
- matlab_proxy/util/mwi/environment_variables.py +120 -27
- matlab_proxy/util/mwi/exceptions.py +63 -9
- matlab_proxy/util/mwi/logger.py +141 -27
- matlab_proxy/util/mwi/session_name.py +28 -0
- matlab_proxy/util/mwi/token_auth.py +264 -121
- matlab_proxy/util/mwi/validators.py +231 -88
- matlab_proxy/util/system.py +9 -0
- matlab_proxy/util/windows.py +32 -6
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
- matlab_proxy-0.30.1.dist-info/RECORD +88 -0
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
- matlab_proxy_manager/README.md +85 -0
- matlab_proxy_manager/__init__.py +6 -0
- matlab_proxy_manager/lib/README.md +53 -0
- matlab_proxy_manager/lib/__init__.py +1 -0
- matlab_proxy_manager/lib/api.py +419 -0
- matlab_proxy_manager/storage/README.md +54 -0
- matlab_proxy_manager/storage/__init__.py +1 -0
- matlab_proxy_manager/storage/file_repository.py +144 -0
- matlab_proxy_manager/storage/interface.py +62 -0
- matlab_proxy_manager/storage/server.py +172 -0
- matlab_proxy_manager/utils/__init__.py +1 -0
- matlab_proxy_manager/utils/auth.py +77 -0
- matlab_proxy_manager/utils/constants.py +8 -0
- matlab_proxy_manager/utils/decorators.py +37 -0
- matlab_proxy_manager/utils/environment_variables.py +51 -0
- matlab_proxy_manager/utils/exceptions.py +45 -0
- matlab_proxy_manager/utils/helpers.py +314 -0
- matlab_proxy_manager/utils/logger.py +76 -0
- matlab_proxy_manager/web/README.md +37 -0
- matlab_proxy_manager/web/__init__.py +1 -0
- matlab_proxy_manager/web/app.py +536 -0
- matlab_proxy_manager/web/monitor.py +45 -0
- matlab_proxy_manager/web/watcher.py +65 -0
- matlab_proxy/gui/asset-manifest.json +0 -23
- matlab_proxy/gui/authorization.html +0 -115
- matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
- matlab_proxy/gui/navbar.css +0 -8
- matlab_proxy/gui/signin.css +0 -42
- matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
- matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
- matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
- matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
- matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
- matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
- matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
- matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
- matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
- matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
- matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
- matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
- matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
- matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
- matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
- matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
- matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
- matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
- matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
- matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
- matlab_proxy/gui/token.html +0 -123
- matlab_proxy-0.5.3.dist-info/RECORD +0 -84
- matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
- /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
- /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
- /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
- /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
- /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info/licenses}/LICENSE.md +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Copyright 2024-2025 The MathWorks, Inc.
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import secrets
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
import matlab_proxy
|
|
9
|
+
import matlab_proxy.util.mwi.environment_variables as mwi_env
|
|
10
|
+
import matlab_proxy.util.system as mwi_sys
|
|
11
|
+
from matlab_proxy_manager.storage.file_repository import FileRepository
|
|
12
|
+
from matlab_proxy_manager.storage.server import ServerProcess
|
|
13
|
+
from matlab_proxy_manager.utils import constants, exceptions, helpers, logger
|
|
14
|
+
|
|
15
|
+
# Used to list all the public-facing APIs exported by this module.
|
|
16
|
+
__all__ = ["shutdown", "start_matlab_proxy_for_kernel", "start_matlab_proxy_for_jsp"]
|
|
17
|
+
|
|
18
|
+
log = logger.get()
|
|
19
|
+
shutdown_lock = asyncio.Lock()
|
|
20
|
+
log = logger.get(init=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def start_matlab_proxy_for_kernel(
|
|
24
|
+
caller_id: str,
|
|
25
|
+
parent_id: str,
|
|
26
|
+
is_shared_matlab: bool,
|
|
27
|
+
base_url_prefix: str = "",
|
|
28
|
+
**kwargs,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Starts a MATLAB proxy server specifically for MATLAB Kernel.
|
|
32
|
+
|
|
33
|
+
This function is a wrapper around the `start_matlab_proxy` function, with mpm_auth_token
|
|
34
|
+
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.
|
|
48
|
+
"""
|
|
49
|
+
return await _start_matlab_proxy(
|
|
50
|
+
caller_id=caller_id,
|
|
51
|
+
ctx=parent_id,
|
|
52
|
+
is_shared_matlab=is_shared_matlab,
|
|
53
|
+
base_url_prefix=base_url_prefix,
|
|
54
|
+
**kwargs,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def start_matlab_proxy_for_jsp(
|
|
59
|
+
parent_id: str,
|
|
60
|
+
is_shared_matlab: bool,
|
|
61
|
+
mpm_auth_token: str,
|
|
62
|
+
base_url_prefix: str = "",
|
|
63
|
+
**kwargs,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Starts a MATLAB proxy server specifically for Jupyter Server Proxy (JSP) - Open MATLAB launcher.
|
|
67
|
+
|
|
68
|
+
This function is a wrapper around the `start_matlab_proxy` function, providing
|
|
69
|
+
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.
|
|
83
|
+
"""
|
|
84
|
+
return await _start_matlab_proxy(
|
|
85
|
+
caller_id="jsp",
|
|
86
|
+
ctx=parent_id,
|
|
87
|
+
is_shared_matlab=is_shared_matlab,
|
|
88
|
+
mpm_auth_token=mpm_auth_token,
|
|
89
|
+
base_url_prefix=base_url_prefix,
|
|
90
|
+
**kwargs,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def _start_matlab_proxy(**options) -> dict:
|
|
95
|
+
"""
|
|
96
|
+
Starts a MATLAB proxy server with the specified options.
|
|
97
|
+
|
|
98
|
+
This function validates the provided options, checks for existing server instances,
|
|
99
|
+
and either returns an existing server process or starts a new MATLAB proxy server.
|
|
100
|
+
It ensures that required arguments are present, handles token generation, and manages
|
|
101
|
+
server readiness and error handling.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
**options: Arbitrary keyword arguments containing the following keys:
|
|
105
|
+
- caller_id (str): The identifier for the caller (kernel id for kernels, "jsp" for JSP).
|
|
106
|
+
- ctx (str): The context in which the server is being started (parent pid).
|
|
107
|
+
- is_shared_matlab (bool): Flag indicating if the MATLAB proxy is shared.
|
|
108
|
+
- mpm_auth_token (Optional[str]): Authentication token for the MATLAB proxy manager.
|
|
109
|
+
- base_url_prefix (Optional[str]): Custom URL path which gets added to mwi_base_url
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
dict: A dictionary representation of the server process, including any errors encountered.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: If `caller_id` is "default" and `is_shared_matlab` is False.
|
|
116
|
+
"""
|
|
117
|
+
_validate_required_arguments(options)
|
|
118
|
+
|
|
119
|
+
caller_id: str = options["caller_id"]
|
|
120
|
+
ctx: str = options["ctx"]
|
|
121
|
+
is_shared_matlab: bool = options.get("is_shared_matlab", True)
|
|
122
|
+
mpm_auth_token = options.get("mpm_auth_token", None) or secrets.token_hex(32)
|
|
123
|
+
|
|
124
|
+
if not is_shared_matlab and caller_id == "default":
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"Caller id cannot be default when matlab proxy is not shareable"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Cleanup stale entries before starting new instance of matlab proxy server
|
|
130
|
+
helpers._are_orphaned_servers_deleted(ctx)
|
|
131
|
+
|
|
132
|
+
client_id = caller_id if not is_shared_matlab else "default"
|
|
133
|
+
matlab_session_dir = f"{ctx}_{client_id}"
|
|
134
|
+
filename = f"{ctx}_{caller_id}"
|
|
135
|
+
proxy_manager_root_dir = helpers.create_and_get_proxy_manager_data_dir()
|
|
136
|
+
existing_matlab_proxy_process = ServerProcess.find_existing_server(
|
|
137
|
+
proxy_manager_root_dir, matlab_session_dir
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if existing_matlab_proxy_process:
|
|
141
|
+
log.debug("Found existing server for aliasing")
|
|
142
|
+
|
|
143
|
+
# Create a backend file for this caller for reference tracking
|
|
144
|
+
helpers.create_state_file(
|
|
145
|
+
proxy_manager_root_dir, existing_matlab_proxy_process, filename
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return existing_matlab_proxy_process.as_dict()
|
|
149
|
+
|
|
150
|
+
# Create a new matlab proxy server
|
|
151
|
+
try:
|
|
152
|
+
base_url_prefix = options.get("base_url_prefix", "")
|
|
153
|
+
|
|
154
|
+
# Prepare matlab proxy command and required environment variables
|
|
155
|
+
matlab_proxy_cmd, matlab_proxy_env = _prepare_cmd_and_env_for_matlab_proxy(
|
|
156
|
+
client_id, base_url_prefix
|
|
157
|
+
)
|
|
158
|
+
|
|
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
|
+
|
|
164
|
+
log.debug(
|
|
165
|
+
"Starting new matlab proxy server using ctx=%s, client_id=%s, is_shared_matlab=%s",
|
|
166
|
+
ctx,
|
|
167
|
+
client_id,
|
|
168
|
+
is_shared_matlab,
|
|
169
|
+
)
|
|
170
|
+
# Start the matlab proxy process
|
|
171
|
+
process_id, url = await _start_subprocess(matlab_proxy_cmd, matlab_proxy_env)
|
|
172
|
+
log.debug("MATLAB proxy process url: %s", url)
|
|
173
|
+
|
|
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(
|
|
178
|
+
matlab_proxy_env, "MWI"
|
|
179
|
+
),
|
|
180
|
+
pid=str(process_id),
|
|
181
|
+
parent_pid=ctx,
|
|
182
|
+
id=matlab_session_dir,
|
|
183
|
+
type="shared" if is_shared_matlab else "isolated",
|
|
184
|
+
mpm_auth_token=mpm_auth_token,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
await _check_for_process_readiness(matlab_proxy_process)
|
|
188
|
+
|
|
189
|
+
# Store the newly created server into filesystem
|
|
190
|
+
helpers.create_state_file(
|
|
191
|
+
proxy_manager_root_dir, matlab_proxy_process, filename
|
|
192
|
+
)
|
|
193
|
+
return matlab_proxy_process.as_dict()
|
|
194
|
+
|
|
195
|
+
# Return a server process instance with the errors information set
|
|
196
|
+
except exceptions.ProcessStartError as pse:
|
|
197
|
+
return ServerProcess(errors=[str(pse)]).as_dict()
|
|
198
|
+
except exceptions.ServerReadinessError as sre:
|
|
199
|
+
return ServerProcess(errors=[str(sre)]).as_dict()
|
|
200
|
+
except Exception as e:
|
|
201
|
+
log.error("Error starting matlab proxy server: %s", str(e))
|
|
202
|
+
return ServerProcess(errors=[str(e)]).as_dict()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _validate_required_arguments(options):
|
|
206
|
+
# Validates that all required arguments are present in the supplied values
|
|
207
|
+
required_args: List[str] = ["caller_id", "ctx", "is_shared_matlab"]
|
|
208
|
+
missing_args: List[str] = [arg for arg in required_args if arg not in options]
|
|
209
|
+
|
|
210
|
+
if missing_args:
|
|
211
|
+
raise ValueError(f"Missing required arguments: {', '.join(missing_args)}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def _check_for_process_readiness(matlab_proxy_process: ServerProcess):
|
|
215
|
+
"""
|
|
216
|
+
Checks if the MATLAB proxy server is ready.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
matlab_proxy_process (ServerProcess): Deserialized matlab-proxy process
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
ServerReadinessError: If the MATLAB proxy server is not ready after retries.
|
|
223
|
+
"""
|
|
224
|
+
# Check for the matlab proxy server readiness - with retries
|
|
225
|
+
if not helpers.is_server_ready(
|
|
226
|
+
url=matlab_proxy_process.absolute_url, retries=7, backoff_factor=0.5
|
|
227
|
+
):
|
|
228
|
+
log.error(
|
|
229
|
+
"MATLAB Proxy Server unavailable: matlab-proxy-app failed to start or has timed out."
|
|
230
|
+
)
|
|
231
|
+
matlab_proxy_process.shutdown()
|
|
232
|
+
raise exceptions.ServerReadinessError()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _prepare_cmd_and_env_for_matlab_proxy(client_id: str, base_url_prefix: str):
|
|
236
|
+
"""
|
|
237
|
+
Prepare the command and environment variables for starting the MATLAB proxy.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tuple: A tuple containing the MATLAB proxy command and environment variables.
|
|
241
|
+
"""
|
|
242
|
+
# Get config from matlab_proxy module if jupyter_matlab_proxy module is not available
|
|
243
|
+
try:
|
|
244
|
+
from jupyter_matlab_proxy import config
|
|
245
|
+
except ImportError:
|
|
246
|
+
from matlab_proxy.default_configuration import config
|
|
247
|
+
|
|
248
|
+
# Get the command to start matlab-proxy
|
|
249
|
+
matlab_proxy_cmd: list = [
|
|
250
|
+
matlab_proxy.get_executable_name(),
|
|
251
|
+
"--config",
|
|
252
|
+
config.get("extension_name"),
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
mwi_base_url = _construct_mwi_base_url(base_url_prefix, client_id)
|
|
256
|
+
log.info("MWI_BASE_URL : %s", mwi_base_url)
|
|
257
|
+
|
|
258
|
+
input_env: dict = {
|
|
259
|
+
"MWI_AUTH_TOKEN": secrets.token_urlsafe(32),
|
|
260
|
+
"MWI_BASE_URL": mwi_base_url,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
matlab_proxy_env: dict = os.environ.copy()
|
|
264
|
+
matlab_proxy_env.update(input_env)
|
|
265
|
+
|
|
266
|
+
return matlab_proxy_cmd, matlab_proxy_env
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _construct_mwi_base_url(base_url_prefix: str, client_id: str):
|
|
270
|
+
# Converts to correct base url (e.g. /jupyter/, default to /jupyter/matlab/default)
|
|
271
|
+
log.debug(
|
|
272
|
+
"base_url_prefix_from_client: %s, client_id: %s", base_url_prefix, client_id
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if base_url_prefix:
|
|
276
|
+
base_url_prefix = base_url_prefix.rstrip("/")
|
|
277
|
+
prefix = constants.MWI_BASE_URL_PREFIX.strip("/")
|
|
278
|
+
client_id = client_id.strip("/")
|
|
279
|
+
return "/".join([base_url_prefix, prefix, client_id])
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def _start_subprocess(cmd: list, env: dict) -> Tuple[int, str]:
|
|
283
|
+
"""
|
|
284
|
+
Initializes and starts a subprocess using the specified command and provided environment.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
cmd (list): The command to execute the subprocess.
|
|
288
|
+
env (dict): The environment variables to set for the subprocess.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Optional[Tuple[int, str]]: A tuple containing the process ID, the URL
|
|
292
|
+
of the server, or None if the process fails to start.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
process = None
|
|
296
|
+
url = None
|
|
297
|
+
|
|
298
|
+
# Get a free port and corresponding bound socket
|
|
299
|
+
with helpers.find_free_port() as (port, _):
|
|
300
|
+
log.debug("Allocated port %s", port)
|
|
301
|
+
|
|
302
|
+
env.update(
|
|
303
|
+
{
|
|
304
|
+
"MWI_APP_PORT": port,
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Using loopback address so that DNS resolution doesn't add latency in Windows
|
|
309
|
+
url = f"http://127.0.0.1:{port}"
|
|
310
|
+
process = await _initialize_process_based_on_os_type(cmd, env)
|
|
311
|
+
process_pid = process.pid
|
|
312
|
+
log.debug(
|
|
313
|
+
"MATLAB proxy info: pid = %s, returncode = %s",
|
|
314
|
+
process_pid,
|
|
315
|
+
process.returncode,
|
|
316
|
+
)
|
|
317
|
+
return process_pid, url
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def _initialize_process_based_on_os_type(cmd, env):
|
|
321
|
+
"""
|
|
322
|
+
Initializes and starts a subprocess based on the operating system.
|
|
323
|
+
|
|
324
|
+
This function attempts to create a subprocess using the provided command and
|
|
325
|
+
environment variables. It handles both POSIX and Windows systems differently.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
cmd (list): The command to execute the subprocess.
|
|
329
|
+
env (dict): The environment variables to set for the subprocess.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
subprocess.Popen or asyncio.subprocess.Process: The process object for the started subprocess.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
exceptions.ProcessStartError: If the subprocess fails to start.
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
if mwi_sys.is_posix():
|
|
339
|
+
log.debug("Starting matlab proxy subprocess for posix")
|
|
340
|
+
return await asyncio.create_subprocess_exec(
|
|
341
|
+
*cmd,
|
|
342
|
+
env=env,
|
|
343
|
+
# kernel sporadically ends up cleaning the child matlab-proxy process during the
|
|
344
|
+
# restart workflow. This is a workaround to handle that race condition which leads
|
|
345
|
+
# to starting matlab-proxy in a new process group and is not counted for deletion.
|
|
346
|
+
# https://github.com/ipython/ipykernel/blob/main/ipykernel/kernelbase.py#L1283
|
|
347
|
+
start_new_session=True,
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
log.debug("Starting matlab proxy subprocess for windows")
|
|
351
|
+
return subprocess.Popen(
|
|
352
|
+
cmd,
|
|
353
|
+
env=env,
|
|
354
|
+
)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
log.error("Failed to create matlab-proxy subprocess: %s", e)
|
|
357
|
+
raise exceptions.ProcessStartError(extra_info=str(e)) from e
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def shutdown(parent_pid: str, caller_id: str, mpm_auth_token: str):
|
|
361
|
+
"""
|
|
362
|
+
Shutdown the MATLAB proxy server if the provided authentication token is valid.
|
|
363
|
+
|
|
364
|
+
This function attempts to shut down the MATLAB proxy server identified by the
|
|
365
|
+
given context and ID, provided the correct authentication token is supplied.
|
|
366
|
+
It ensures that the shutdown process is thread-safe using an asyncio lock.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
parent_pid (str): The context identifier for the server.
|
|
370
|
+
caller_id (str): The unique identifier for the server.
|
|
371
|
+
mpm_auth_token (str): The authentication token for proxy manager and client communication.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Optional[None]: Returns None if the shutdown process is successful or if
|
|
375
|
+
required arguments are missing.
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
FileNotFoundError: If the state file for the server does not exist.
|
|
379
|
+
ValueError: If the authentication token is invalid.
|
|
380
|
+
Exception: If any other error occurs during the shutdown process.
|
|
381
|
+
"""
|
|
382
|
+
if not parent_pid or not caller_id or not mpm_auth_token:
|
|
383
|
+
log.debug(
|
|
384
|
+
"Required arguments (parent_pid | caller_id | mpm_auth_token) for shutdown missing"
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
filename = f"{parent_pid}_{caller_id}"
|
|
389
|
+
try:
|
|
390
|
+
data_dir = helpers.create_and_get_proxy_manager_data_dir()
|
|
391
|
+
storage = FileRepository(data_dir)
|
|
392
|
+
full_file_path, server = storage.get(filename)
|
|
393
|
+
|
|
394
|
+
if not server:
|
|
395
|
+
log.debug("State file for this server not found, filename: %s", filename)
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
if mpm_auth_token != server.mpm_auth_token:
|
|
399
|
+
raise ValueError("Invalid authentication token")
|
|
400
|
+
|
|
401
|
+
# Using asyncio lock to ensure thread-safe shutdown: clicking shutdown
|
|
402
|
+
# all on Kernel UI sends shutdown request in parallel which could lead
|
|
403
|
+
# to a scenario where the kernels' shutdown just cleans the files from
|
|
404
|
+
# filesystem and doesn't shut down the backend matlab proxy server.
|
|
405
|
+
async with shutdown_lock:
|
|
406
|
+
if helpers.is_only_reference(full_file_path):
|
|
407
|
+
server.shutdown()
|
|
408
|
+
|
|
409
|
+
# Delete the file for this server
|
|
410
|
+
storage.delete(f"{filename}.info")
|
|
411
|
+
except FileNotFoundError as e:
|
|
412
|
+
log.error("State file for server %s not found: %s", filename, e)
|
|
413
|
+
return
|
|
414
|
+
except ValueError as e:
|
|
415
|
+
log.error("Authentication error for server %s: %s", filename, e)
|
|
416
|
+
return
|
|
417
|
+
except Exception as e:
|
|
418
|
+
log.error("Error during shutdown of server %s: %s", filename, e)
|
|
419
|
+
raise
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# MATLAB Proxy Manager - Storage
|
|
2
|
+
|
|
3
|
+
This README is intended for MathWorks® developers only.
|
|
4
|
+
The storage module is a critical part of the `matlab-proxy-manager`, responsible for managing the persistence of metadata related to MATLAB proxy instances. It employs a repository pattern to provide a clean and consistent interface for performing CRD (Create, Read, Delete) operations on the file system.
|
|
5
|
+
|
|
6
|
+
## Key Features
|
|
7
|
+
|
|
8
|
+
### Repository Pattern:
|
|
9
|
+
|
|
10
|
+
The storage module is designed using the repository pattern, which abstracts the data layer and provides a straightforward API for interacting with stored metadata. This pattern ensures that the underlying data storage mechanism can be modified or replaced with minimal impact on the rest of the application.
|
|
11
|
+
|
|
12
|
+
### File System-Based Storage:
|
|
13
|
+
|
|
14
|
+
Currently, the storage operations are performed directly on the file system. Each MATLAB proxy instance's metadata is stored in a separate file, making it easy to manage and access individual instances.
|
|
15
|
+
|
|
16
|
+
### CRD Operations:
|
|
17
|
+
|
|
18
|
+
The storage module provides a set of APIs to perform CRD operations:
|
|
19
|
+
|
|
20
|
+
1. add(...): Create a new metadata file for a MATLAB proxy instance.
|
|
21
|
+
2. get(...): Retrieve metadata for a specific instance.
|
|
22
|
+
3. get_all(): Retrieve metadata for all instances.
|
|
23
|
+
4. delete(...): Remove the metadata file for a specific instance.
|
|
24
|
+
|
|
25
|
+
Usage
|
|
26
|
+
To use the storage APIs, clients can import the relevant module and invoke the provided functions. Here’s an example of how to perform basic CRUD operations:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
|
|
30
|
+
from matlab_proxy_manager.storage.file_repository import FileRepository
|
|
31
|
+
|
|
32
|
+
storage = FileRepository(data_dir)
|
|
33
|
+
|
|
34
|
+
# Add a new MATLAB proxy instance metadata
|
|
35
|
+
filename = '1234.info'
|
|
36
|
+
server_process = <instance of ServerProcess class>
|
|
37
|
+
storage.add(server=server_process, filename=filename)
|
|
38
|
+
|
|
39
|
+
# Retrieve metadata for a specific instance
|
|
40
|
+
filename = f"{parent_pid}_{caller_id}"
|
|
41
|
+
full_file_path, server = storage.get(filename)
|
|
42
|
+
|
|
43
|
+
# Retrieve metadata for all instances
|
|
44
|
+
servers = storage.get_all()
|
|
45
|
+
|
|
46
|
+
# Delete metadata for a specific instance
|
|
47
|
+
storage.delete(f"{filename}.info")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
Copyright 2024 The MathWorks, Inc.
|
|
53
|
+
|
|
54
|
+
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Copyright 2024 The MathWorks, Inc.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright 2024 The MathWorks, Inc.
|
|
2
|
+
import glob
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from matlab_proxy_manager.utils import logger
|
|
9
|
+
|
|
10
|
+
from .interface import IRepository
|
|
11
|
+
|
|
12
|
+
log = logger.get()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileRepository(IRepository):
|
|
16
|
+
"""
|
|
17
|
+
A repository for managing MATLAB proxy server processes using the file system.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, data_dir) -> None:
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.data_dir = data_dir
|
|
23
|
+
self.encoding = "utf-8"
|
|
24
|
+
|
|
25
|
+
def get_all(self):
|
|
26
|
+
"""Retrieves all server processes from the repository.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A dictionary mapping file paths to server process instances.
|
|
30
|
+
"""
|
|
31
|
+
from matlab_proxy_manager.storage.server import ServerProcess
|
|
32
|
+
|
|
33
|
+
servers = {}
|
|
34
|
+
|
|
35
|
+
# Read all the files in data_dir
|
|
36
|
+
all_files = glob.glob(f"{self.data_dir}/**/*.info", recursive=True)
|
|
37
|
+
|
|
38
|
+
for file in all_files:
|
|
39
|
+
try:
|
|
40
|
+
with open(file, "r", encoding=self.encoding) as f:
|
|
41
|
+
data = f.read().strip()
|
|
42
|
+
|
|
43
|
+
# Convert the content of each file to ServerProcess
|
|
44
|
+
if data:
|
|
45
|
+
server_process = ServerProcess.instantiate_from_string(data)
|
|
46
|
+
servers[file] = server_process
|
|
47
|
+
except Exception as ex:
|
|
48
|
+
log.debug("ServerProcess instantiation failed for %s: %s", file, ex)
|
|
49
|
+
return servers
|
|
50
|
+
|
|
51
|
+
def get(self, name) -> tuple:
|
|
52
|
+
"""
|
|
53
|
+
Retrieves a server process from the repository by its filename.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
name (str): The name of the server process file.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tuple[Optional[str], Optional[ServerProcess]]: A tuple containing the file path
|
|
60
|
+
and the server process instance.
|
|
61
|
+
"""
|
|
62
|
+
from matlab_proxy_manager.storage.server import ServerProcess
|
|
63
|
+
|
|
64
|
+
server_process = None
|
|
65
|
+
full_file_path: Optional[str] = None
|
|
66
|
+
current_files = glob.glob(f"{self.data_dir}/**/{name}.info", recursive=True)
|
|
67
|
+
if current_files:
|
|
68
|
+
full_file_path = current_files[0]
|
|
69
|
+
with open(full_file_path, "r", encoding=self.encoding) as f:
|
|
70
|
+
try:
|
|
71
|
+
data = f.read().strip()
|
|
72
|
+
if data:
|
|
73
|
+
server_process = ServerProcess.instantiate_from_string(data)
|
|
74
|
+
except Exception as ex:
|
|
75
|
+
log.debug(
|
|
76
|
+
"ServerProcess instantiation failed for %s: %s",
|
|
77
|
+
full_file_path,
|
|
78
|
+
ex,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return full_file_path, server_process
|
|
82
|
+
|
|
83
|
+
def add(self, server, filename: str) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Adds a server process to the repository.
|
|
86
|
+
Creates a directory like <ctx>_default|<kernel_id> and then creates a file as
|
|
87
|
+
default|<kernel_id>.info in that dir
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
server (ServerProcess): The server process instance to add.
|
|
91
|
+
filename (str): The filename to associate with the server process.
|
|
92
|
+
"""
|
|
93
|
+
# Creates a child dir under the data_dir
|
|
94
|
+
server_dir = Path(f"{self.data_dir}", f"{server.id}")
|
|
95
|
+
Path.mkdir(server_dir, parents=True, exist_ok=True)
|
|
96
|
+
server_dict = {}
|
|
97
|
+
|
|
98
|
+
server_file = Path(server_dir, f"{filename}.info")
|
|
99
|
+
with open(server_file, "w", encoding=self.encoding) as f:
|
|
100
|
+
server_dict[server.id] = server.as_dict()
|
|
101
|
+
file_content = json.dumps(server_dict)
|
|
102
|
+
f.write(file_content)
|
|
103
|
+
|
|
104
|
+
def delete(self, filename: str) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Deletes a server process from the repository by its filename.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
filename (str): The filename associated with the server process to delete.
|
|
110
|
+
"""
|
|
111
|
+
# <path to proxy manager dir>/<parent_pid>_<name>/<name>.info
|
|
112
|
+
full_file_path, parent_dir = FileRepository._find_file_and_get_parent(
|
|
113
|
+
self.data_dir, filename
|
|
114
|
+
)
|
|
115
|
+
if full_file_path:
|
|
116
|
+
Path(full_file_path).unlink(missing_ok=True)
|
|
117
|
+
log.debug("Deleted file: %s", filename)
|
|
118
|
+
|
|
119
|
+
# delete the sub-directory (<parent_pid>_<id>) only if it is empty
|
|
120
|
+
if parent_dir and not len(os.listdir(parent_dir)):
|
|
121
|
+
os.rmdir(parent_dir)
|
|
122
|
+
log.debug("Deleted dir: %s", parent_dir)
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _find_file_and_get_parent(data_dir: str, filename: str):
|
|
126
|
+
"""
|
|
127
|
+
Finds the file and its parent directory in the given data directory.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
data_dir (str): The base directory to search within.
|
|
131
|
+
filename (str): The filename to search for.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tuple[Optional[str], Optional[str]]: A tuple containing the full file path and the parent directory.
|
|
135
|
+
"""
|
|
136
|
+
for dirpath, _, filenames in os.walk(data_dir):
|
|
137
|
+
# Check if target file is in the current directory's files
|
|
138
|
+
if filename in filenames:
|
|
139
|
+
full_path = os.path.join(dirpath, filename)
|
|
140
|
+
parent_dir = os.path.dirname(full_path)
|
|
141
|
+
return full_path, parent_dir
|
|
142
|
+
|
|
143
|
+
# Return None if the file was not found
|
|
144
|
+
return None, None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright 2024 The MathWorks, Inc.
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class IRepository(Protocol):
|
|
6
|
+
"""
|
|
7
|
+
Protocol for a repository that manages MATLAB proxy server processes.
|
|
8
|
+
This protocol defines the required methods for adding, retrieving, and deleting
|
|
9
|
+
server process instances to the storage system (files).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def add(self, server, filename: str):
|
|
13
|
+
"""
|
|
14
|
+
Adds a server process to the repository.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
server (server.ServerProcess): The server process instance to add.
|
|
18
|
+
filename (str): The filename to associate with the server process.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
NotImplementedError
|
|
22
|
+
"""
|
|
23
|
+
raise NotImplementedError("add not implemented")
|
|
24
|
+
|
|
25
|
+
def get(self, name: str) -> tuple:
|
|
26
|
+
"""
|
|
27
|
+
Retrieves a server process from the repository by its filename.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
filename (str): The filename associated with the server process.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
tuple (str, server.ServerProcess): Full file path and the retrieved server process instance.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
NotImplementedError
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError("get not implemented")
|
|
39
|
+
|
|
40
|
+
def get_all(self):
|
|
41
|
+
"""
|
|
42
|
+
Retrieves all server processes from the repository.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dict[str, server.ServerProcess]: Dict with filename as key and corresponding server process as value.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
NotImplementedError
|
|
49
|
+
"""
|
|
50
|
+
raise NotImplementedError("get_all not implemented")
|
|
51
|
+
|
|
52
|
+
def delete(self, filename: str) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Deletes a server process from the repository by its filename.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
filename (str): The filename associated with the server process to delete.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
NotImplementedError
|
|
61
|
+
"""
|
|
62
|
+
raise NotImplementedError("delete not implemented")
|