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.
Files changed (104) hide show
  1. matlab_proxy/app.py +578 -205
  2. matlab_proxy/app_state.py +1061 -431
  3. matlab_proxy/constants.py +37 -0
  4. matlab_proxy/default_configuration.py +39 -4
  5. matlab_proxy/devel.py +18 -22
  6. matlab_proxy/gui/index.html +20 -1
  7. matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
  8. matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
  9. matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
  10. matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
  11. matlab_proxy/matlab/startup.m +3 -28
  12. matlab_proxy/settings.py +543 -112
  13. matlab_proxy/util/__init__.py +187 -59
  14. matlab_proxy/util/cookie_jar.py +72 -0
  15. matlab_proxy/util/event_loop.py +28 -10
  16. matlab_proxy/util/list_servers.py +71 -26
  17. matlab_proxy/util/mw.py +16 -15
  18. matlab_proxy/util/mwi/download.py +136 -0
  19. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  20. matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
  21. matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
  22. matlab_proxy/util/mwi/environment_variables.py +120 -27
  23. matlab_proxy/util/mwi/exceptions.py +63 -9
  24. matlab_proxy/util/mwi/logger.py +141 -27
  25. matlab_proxy/util/mwi/session_name.py +28 -0
  26. matlab_proxy/util/mwi/token_auth.py +264 -121
  27. matlab_proxy/util/mwi/validators.py +231 -88
  28. matlab_proxy/util/system.py +9 -0
  29. matlab_proxy/util/windows.py +32 -6
  30. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
  31. matlab_proxy-0.30.1.dist-info/RECORD +88 -0
  32. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
  33. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
  34. matlab_proxy_manager/README.md +85 -0
  35. matlab_proxy_manager/__init__.py +6 -0
  36. matlab_proxy_manager/lib/README.md +53 -0
  37. matlab_proxy_manager/lib/__init__.py +1 -0
  38. matlab_proxy_manager/lib/api.py +419 -0
  39. matlab_proxy_manager/storage/README.md +54 -0
  40. matlab_proxy_manager/storage/__init__.py +1 -0
  41. matlab_proxy_manager/storage/file_repository.py +144 -0
  42. matlab_proxy_manager/storage/interface.py +62 -0
  43. matlab_proxy_manager/storage/server.py +172 -0
  44. matlab_proxy_manager/utils/__init__.py +1 -0
  45. matlab_proxy_manager/utils/auth.py +77 -0
  46. matlab_proxy_manager/utils/constants.py +8 -0
  47. matlab_proxy_manager/utils/decorators.py +37 -0
  48. matlab_proxy_manager/utils/environment_variables.py +51 -0
  49. matlab_proxy_manager/utils/exceptions.py +45 -0
  50. matlab_proxy_manager/utils/helpers.py +314 -0
  51. matlab_proxy_manager/utils/logger.py +76 -0
  52. matlab_proxy_manager/web/README.md +37 -0
  53. matlab_proxy_manager/web/__init__.py +1 -0
  54. matlab_proxy_manager/web/app.py +536 -0
  55. matlab_proxy_manager/web/monitor.py +45 -0
  56. matlab_proxy_manager/web/watcher.py +65 -0
  57. matlab_proxy/gui/asset-manifest.json +0 -23
  58. matlab_proxy/gui/authorization.html +0 -115
  59. matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
  60. matlab_proxy/gui/navbar.css +0 -8
  61. matlab_proxy/gui/signin.css +0 -42
  62. matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
  63. matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
  64. matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
  65. matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
  66. matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
  67. matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
  68. matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
  69. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
  70. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
  71. matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
  72. matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
  73. matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
  74. matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
  75. matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
  76. matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
  77. matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
  78. matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
  79. matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
  80. matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
  81. matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
  82. matlab_proxy/gui/token.html +0 -123
  83. matlab_proxy-0.5.3.dist-info/RECORD +0 -84
  84. matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
  85. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
  86. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
  87. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
  88. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
  89. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
  90. /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
  91. /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
  92. /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
  93. /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
  94. /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
  95. /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
  96. /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
  97. /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
  98. /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
  99. /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
  100. /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
  101. /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
  102. /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
  103. /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
  104. {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")