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,536 @@
1
+ # Copyright 2024-2025 The MathWorks, Inc.
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ import signal
7
+ import sys
8
+ from collections import namedtuple
9
+ from typing import Optional
10
+
11
+ import aiohttp
12
+ from aiohttp import ClientSession, client_exceptions, web
13
+
14
+ import matlab_proxy.constants as mp_constants
15
+ import matlab_proxy.util.mwi.environment_variables as mwi_env
16
+ import matlab_proxy.util.system as mwi_sys
17
+ import matlab_proxy_manager.lib.api as mpm_lib
18
+ from matlab_proxy_manager.utils import constants, helpers, logger
19
+ from matlab_proxy_manager.utils import environment_variables as mpm_env
20
+ from matlab_proxy_manager.utils.auth import authenticate_access_decorator
21
+ from matlab_proxy_manager.utils.decorators import validate_incoming_request_decorator
22
+ from matlab_proxy_manager.web import watcher
23
+ from matlab_proxy_manager.web.monitor import OrphanedProcessMonitor
24
+
25
+ # List of public-facing APIs exported by this module.
26
+ # This list contains the names of functions or classes that are intended to be
27
+ # used by external code importing this module. Only items listed here will be
28
+ # directly accessible when using "from module import *".
29
+ __all__ = ["proxy"]
30
+
31
+ log = logger.get(init=True)
32
+
33
+
34
+ def init_app() -> web.Application:
35
+ """
36
+ Initialize and configure the aiohttp web application.
37
+
38
+ This function sets up the web application with necessary configurations,
39
+ including creating the proxy manager data directory, setting up an idle
40
+ timeout monitor, and configuring client sessions.
41
+
42
+ Returns:
43
+ web.Application: The configured aiohttp web application.
44
+ """
45
+ app = web.Application()
46
+ # Async event is utilized to signal app termination from this and other modules
47
+ app["shutdown_event"] = asyncio.Event()
48
+
49
+ # Tracks whether default matlab proxy is started or not
50
+ app["has_default_matlab_proxy_started"] = False
51
+
52
+ # Create and get the proxy manager data directory
53
+ try:
54
+ data_dir = helpers.create_and_get_proxy_manager_data_dir()
55
+ app["data_dir"] = data_dir
56
+ except Exception as ex:
57
+ raise RuntimeError(f"Failed to create or get data directory: {ex}") from ex
58
+
59
+ # Setup idle timeout monitor for the app
60
+ monitor = OrphanedProcessMonitor(app)
61
+
62
+ # Load existing matlab proxy servers into app state for consistency
63
+ app["servers"] = helpers.pre_load_from_state_file(app.get("data_dir"))
64
+ log.debug("Loaded existing matlab proxy servers into app state: %s", app["servers"])
65
+
66
+ async def start_idle_monitor(app):
67
+ """Start the idle timeout monitor."""
68
+ app["monitor_task"] = asyncio.create_task(monitor.start())
69
+
70
+ async def create_client_session(app):
71
+ """Create an aiohttp client session."""
72
+ app["session"] = ClientSession(
73
+ trust_env=True, connector=aiohttp.TCPConnector(ssl=False, ttl_dns_cache=600)
74
+ )
75
+
76
+ async def cleanup_client_session(app):
77
+ """Cleanup the aiohttp client session."""
78
+ await app["session"].close()
79
+
80
+ async def cleanup_monitor(app):
81
+ """Cancel the idle timeout monitor task."""
82
+ if "monitor_task" in app:
83
+ app["monitor_task"].cancel()
84
+ try:
85
+ await app["monitor_task"]
86
+ except asyncio.CancelledError:
87
+ pass
88
+
89
+ async def cleanup_watcher(app):
90
+ """Cleanup the filesystem watcher."""
91
+ if "observer" in app:
92
+ loop = asyncio.get_running_loop()
93
+ await loop.run_in_executor(None, watcher.stop_watcher, app)
94
+
95
+ if "watcher_future" in app:
96
+ app["watcher_future"].cancel()
97
+ try:
98
+ await app["watcher_future"]
99
+ except asyncio.CancelledError:
100
+ pass
101
+
102
+ app.on_startup.append(start_idle_monitor)
103
+ app.on_startup.append(create_client_session)
104
+ app.on_cleanup.append(helpers.poll_for_server_deletion)
105
+ app.on_cleanup.append(cleanup_client_session)
106
+ app.on_cleanup.append(cleanup_monitor)
107
+ app.on_cleanup.append(cleanup_watcher)
108
+
109
+ app.router.add_route("*", "/{tail:.*}", proxy)
110
+
111
+ return app
112
+
113
+
114
+ async def start_app(env_vars):
115
+ """
116
+ Initialize and start the web application.
117
+
118
+ This function sets up logging, initializes the web application, and starts
119
+ the default matlab proxy. It also sets up signal handlers for graceful shutdown
120
+ and starts a file watcher in a separate thread.
121
+
122
+ Raises:
123
+ Exception: If any error occurs during the application startup or runtime.
124
+ """
125
+ app = init_app()
126
+
127
+ app["port"] = env_vars.mpm_port
128
+ app["auth_token"] = env_vars.mpm_auth_token
129
+ app["parent_pid"] = env_vars.mpm_parent_pid
130
+ app["base_url_prefix"] = env_vars.base_url_prefix
131
+
132
+ web_logger = None if not mwi_env.is_web_logging_enabled() else log
133
+
134
+ # Run the app
135
+ runner = web.AppRunner(app, logger=web_logger, access_log=web_logger)
136
+ await runner.setup()
137
+ site = web.TCPSite(runner, port=env_vars.mpm_port)
138
+ await site.start()
139
+ log.debug("Proxy manager started at http://127.0.0.1:%d", site._port)
140
+
141
+ # Get the default event loop
142
+ loop = asyncio.get_running_loop()
143
+
144
+ # Run the observer in a separate thread and store the future
145
+ app["watcher_future"] = loop.run_in_executor(None, watcher.start_watcher, app)
146
+
147
+ # Register signal handler for graceful shutdown
148
+ _register_signal_handler(loop, app)
149
+
150
+ # Wait for receiving shutdown_event (set by interrupts or by monitoring process)
151
+ await app.get("shutdown_event").wait()
152
+
153
+ # After receiving the shutdown signal, perform cleanup by stopping the web server
154
+ await runner.cleanup()
155
+
156
+
157
+ def _register_signal_handler(loop, app):
158
+ """
159
+ Registers signal handlers for graceful shutdown of the application.
160
+
161
+ This function sets up handlers for supported termination signals to allow
162
+ the application to shut down gracefully. It uses different methods for
163
+ POSIX and non-POSIX systems to add the signal handlers.
164
+
165
+ Args:
166
+ loop (asyncio.AbstractEventLoop): The event loop to which the signal handlers
167
+ should be added.
168
+ app (aiohttp.web.Application): The web application instance.
169
+ """
170
+ signals = mwi_sys.get_supported_termination_signals()
171
+ for sig_name in signals:
172
+ if mwi_sys.is_posix():
173
+ loop.add_signal_handler(sig_name, lambda: _catch_signals(app))
174
+ else:
175
+ # loop.add_signal_handler() is not yet supported in Windows.
176
+ # Using the 'signal' package instead.
177
+ # signal module expects a handler function that takes two arguments:
178
+ # the signal number and the current stack frame
179
+ signal.signal(sig_name, lambda s, f: _catch_signals(app))
180
+
181
+
182
+ def _catch_signals(app):
183
+ """Handle termination signals for graceful shutdown."""
184
+ shutdown_event = app.get("shutdown_event")
185
+
186
+ # Only set if it exists and is not already set
187
+ if shutdown_event and not shutdown_event.is_set():
188
+ log.info("Signal caught, setting shutdown event")
189
+ shutdown_event.set()
190
+ else:
191
+ log.debug("Shutdown event already set or not available")
192
+
193
+
194
+ async def _start_default_proxy(app):
195
+ """
196
+ Starts the default MATLAB proxy and updates the application state.
197
+
198
+ Args:
199
+ app : The aiohttp web application.
200
+ """
201
+ server_process = await mpm_lib.start_matlab_proxy_for_jsp(
202
+ parent_id=app.get("parent_pid"),
203
+ is_shared_matlab=True,
204
+ mpm_auth_token=app.get("auth_token"),
205
+ base_url_prefix=app.get("base_url_prefix"),
206
+ )
207
+ errors = server_process.get("errors")
208
+
209
+ # Raising an exception if there was an error starting the default MATLAB proxy
210
+ if errors:
211
+ raise Exception(":".join(errors))
212
+
213
+ # Add the new/existing server to the app state
214
+ app["servers"][server_process.get("id")] = server_process
215
+
216
+
217
+ @authenticate_access_decorator
218
+ @validate_incoming_request_decorator
219
+ async def proxy(req):
220
+ """
221
+ Proxy incoming HTTP requests to the appropriate MATLAB proxy backend server.
222
+
223
+ This function handles requests by:
224
+ 1. Redirecting paths ending with '/matlab/' to '/matlab/default/'.
225
+ 2. Extracting client identifiers from the request path.
226
+ 3. Routing the request to the appropriate MATLAB backend server based on the client identifier.
227
+ 4. Handling various exceptions and providing appropriate HTTP responses.
228
+
229
+ Args:
230
+ req (aiohttp.web.Request): The incoming HTTP request.
231
+
232
+ Returns:
233
+ aiohttp.web.Response: The HTTP response from the backend server or an error page.
234
+
235
+ Raises:
236
+ aiohttp.web.HTTPFound: If the request path needs to be redirected.
237
+ aiohttp.web.HTTPServiceUnavailable: If the MATLAB proxy process is not running.
238
+ aiohttp.web.HTTPNotFound: If the request cannot be forwarded to the MATLAB proxy.
239
+ """
240
+ # Special keys for web socket requests
241
+ connection = "connection"
242
+ upgrade = "upgrade"
243
+ req_headers = req.headers.copy()
244
+ req_body = await req.read()
245
+
246
+ # Set content length in case of modification
247
+ req_headers["Content-Length"] = str(len(req_body))
248
+ req_headers["X-Forwarded-Proto"] = "http"
249
+ req_path = req.rel_url
250
+
251
+ # Redirect block to move /*/matlab to /*/matlab/default/
252
+ if str(req_path).endswith(f"{constants.MWI_BASE_URL_PREFIX}"):
253
+ return _redirect_to_default(req_path)
254
+
255
+ # Module-level pattern so it doesn't get recompiled on every request
256
+ match = constants.MATLAB_IN_REQ_PATH_PATTERN.match(str(req.rel_url))
257
+
258
+ if not match:
259
+ # Path doesn't contain /matlab/default|<id> in the request path
260
+ # redirect to error page
261
+ log.debug("Regex match not found, match: %s", match)
262
+ return helpers.render_error_page(
263
+ "Incorrect request path in the URL, please try with correct URL."
264
+ )
265
+
266
+ ident = match.group(1).rstrip("/")
267
+ backend_server = _get_backend_server(req, req.ctx, ident)
268
+
269
+ # Route to default matlab if the specified path doesn't exist
270
+ if not backend_server:
271
+ # Start default matlab, if not already started
272
+ await _start_default_matlab_proxy_if_needed(req)
273
+
274
+ # Redirect to default matlab
275
+ try:
276
+ new_path = re.sub(f"/matlab/{ident}/", "/matlab/default/", str(req_path))
277
+ log.debug(
278
+ "Backend server not found. Redirecting from %s to %s",
279
+ str(req_path),
280
+ str(new_path),
281
+ )
282
+ raise web.HTTPFound(new_path)
283
+ except TypeError:
284
+ log.exception("Failed to redirect to default matlab")
285
+ return helpers.render_error_page(
286
+ "Incorrect URL, please try with correct URL."
287
+ )
288
+
289
+ group_two_rel_url = match.group(2)
290
+ proxy_url = f"{backend_server.get('absolute_url')}/{group_two_rel_url}"
291
+ log.debug("Proxy URL: %s", proxy_url)
292
+
293
+ # TODO: Unify transport layer functionality between matlab-proxy and matlab-proxy-manager
294
+ if (
295
+ "upgrade" in req_headers.get(connection, "").lower()
296
+ and req_headers.get(upgrade, "").lower() == "websocket"
297
+ and req.method == "GET"
298
+ ):
299
+ return await _forward_websocket_request(req, proxy_url)
300
+ try:
301
+ return await _forward_http_request(
302
+ req, req_body, proxy_url, _collate_headers(req_headers, backend_server)
303
+ )
304
+ except web.HTTPFound:
305
+ log.debug("Redirection to path with /default")
306
+ raise
307
+
308
+ # Handles any pending HTTP requests from the browser when the MATLAB proxy process is
309
+ # terminated before responding to them.
310
+ except (
311
+ client_exceptions.ServerDisconnectedError,
312
+ client_exceptions.ClientConnectionError,
313
+ ) as ex:
314
+ log.debug("MATLAB proxy process may not be running.")
315
+ raise web.HTTPServiceUnavailable() from ex
316
+ except Exception as err:
317
+ log.error("Failed to forward HTTP request to MATLAB proxy with error: %s", err)
318
+ raise web.HTTPNotFound() from err
319
+
320
+
321
+ # Helper private functions
322
+
323
+
324
+ def _collate_headers(req_headers: dict, backend_server: dict) -> dict:
325
+ """Combines request headers with backend server (matlab-proxy) headers.
326
+
327
+ Args:
328
+ req_headers (dict): The headers from the incoming request.
329
+ backend_server (dict): The backend server configuration.
330
+
331
+ Returns:
332
+ dict: A new dictionary containing all headers from both sources.
333
+ """
334
+ return {**req_headers, **backend_server.get("headers")}
335
+
336
+
337
+ async def _forward_websocket_request(
338
+ req: web.Request, proxy_url: str
339
+ ) -> web.WebSocketResponse:
340
+ """Handles a websocket request to the backend matlab proxy server
341
+
342
+ Args:
343
+ req (web.Request): websocket request from the client
344
+ proxy_url (str): backend matlab proxy server URL
345
+
346
+ Raises:
347
+ ValueError: when an unexpected websocket message type is received
348
+ aiohttp.WebSocketError: For any exception raised while forwarding request from src to dest
349
+
350
+ Returns:
351
+ web.WebSocketResponse: The response from the backend server
352
+ """
353
+ ws_server = web.WebSocketResponse(
354
+ max_msg_size=mp_constants.MAX_WEBSOCKET_MESSAGE_SIZE_IN_MB, compress=True
355
+ )
356
+ await ws_server.prepare(req)
357
+
358
+ async with aiohttp.ClientSession(
359
+ trust_env=True,
360
+ cookies=req.cookies,
361
+ connector=aiohttp.TCPConnector(ssl=False),
362
+ ) as client_session:
363
+ try:
364
+ async with client_session.ws_connect(
365
+ proxy_url,
366
+ max_msg_size=mp_constants.MAX_WEBSOCKET_MESSAGE_SIZE_IN_MB, # max websocket message size from MATLAB to browser
367
+ compress=12, # enable websocket messages compression
368
+ ) as ws_client:
369
+
370
+ async def ws_forward(ws_src, ws_dest):
371
+ async for msg in ws_src:
372
+ msg_type = msg.type
373
+ msg_data = msg.data
374
+
375
+ # When a websocket is closed by the MATLAB JSD, it sends out a few
376
+ # http requests to the Embedded Connector about the events that had occurred
377
+ # (figureWindowClosed etc.) The Embedded Connector responds by sending a
378
+ # message of type 'Error' with close code as Abnormal closure. When this
379
+ # happens, matlab-proxy can safely exit out of the loop and close the
380
+ # websocket connection it has with the Embedded Connector (ws_client)
381
+ if (
382
+ msg_type == aiohttp.WSMsgType.ERROR
383
+ and ws_src.close_code
384
+ == aiohttp.WSCloseCode.ABNORMAL_CLOSURE
385
+ ):
386
+ log.debug(
387
+ "Src: %s, msg_type= %s, ws_src.close_code= %s",
388
+ ws_src,
389
+ msg_type,
390
+ ws_src.close_code,
391
+ )
392
+ break
393
+ if msg_type == aiohttp.WSMsgType.TEXT:
394
+ await ws_dest.send_str(msg_data)
395
+ elif msg_type == aiohttp.WSMsgType.BINARY:
396
+ await ws_dest.send_bytes(msg_data)
397
+ elif msg_type == aiohttp.WSMsgType.PING:
398
+ await ws_dest.ping()
399
+ elif msg_type == aiohttp.WSMsgType.PONG:
400
+ await ws_dest.pong()
401
+ elif ws_dest.closed:
402
+ log.debug("Destination: %s closed", ws_dest)
403
+ await ws_dest.close(
404
+ code=ws_dest.close_code, message=msg.extra
405
+ )
406
+ elif msg_type == aiohttp.WSMsgType.ERROR:
407
+ log.error(f"WebSocket error received: {msg}")
408
+ if "exceeds limit" in str(msg.data):
409
+ log.error(
410
+ f"Message too large: {msg.data}. Please refresh browser tab to reconnect."
411
+ )
412
+ break
413
+ else:
414
+ raise ValueError(f"Unexpected message type: {msg}")
415
+
416
+ await asyncio.wait(
417
+ [
418
+ asyncio.create_task(ws_forward(ws_server, ws_client)),
419
+ asyncio.create_task(ws_forward(ws_client, ws_server)),
420
+ ],
421
+ return_when=asyncio.FIRST_COMPLETED,
422
+ )
423
+
424
+ return ws_server
425
+ except Exception as err:
426
+ log.error("Failed to create web socket connection with error: %s", err)
427
+
428
+ code, message = (
429
+ aiohttp.WSCloseCode.INTERNAL_ERROR,
430
+ "Failed to establish websocket connection with the backend server",
431
+ )
432
+ await ws_server.close(code=code, message=message.encode("utf-8"))
433
+ raise aiohttp.WebSocketError(code=code, message=message)
434
+
435
+
436
+ async def _forward_http_request(
437
+ req: web.Request,
438
+ req_body: Optional[bytes],
439
+ proxy_url: str,
440
+ headers: dict,
441
+ ) -> web.Response:
442
+ """
443
+ Forwards an incoming HTTP request to a specified backend server.
444
+
445
+ Returns:
446
+ web.Response: The response from the backend server, including headers, status, and body.
447
+ """
448
+ client_session = req.app.get("session")
449
+ async with client_session.request(
450
+ req.method,
451
+ proxy_url,
452
+ allow_redirects=True,
453
+ data=req_body,
454
+ headers=headers,
455
+ ) as res:
456
+ headers = res.headers.copy()
457
+ body = await res.read()
458
+ return web.Response(headers=headers, status=res.status, body=body)
459
+
460
+
461
+ def _get_backend_server(req: web.Request, ctx: str, ident: str):
462
+ """
463
+ Retrieves the backend server configuration for a given client key.
464
+ """
465
+ client_key = f"{ctx}_{ident}"
466
+ return req.app["servers"].get(client_key)
467
+
468
+
469
+ async def _start_default_matlab_proxy_if_needed(req: web.Request):
470
+ # There could be a chance that the default matlab-proxy hasn't been running at this point
471
+ # considering which we would want to start it first
472
+ app = req.app
473
+ if app.get("has_default_matlab_proxy_started") is False:
474
+ try:
475
+ await _start_default_proxy(app)
476
+ app["has_default_matlab_proxy_started"] = True
477
+ except Exception as e:
478
+ log.error("Error starting default proxy: %s", e)
479
+ return helpers.render_error_page(f"Error during startup: {e}")
480
+
481
+
482
+ def _redirect_to_default(req_path) -> None:
483
+ """
484
+ Redirects the request to the default path.
485
+
486
+ This function constructs a new URL by appending '/default/' to the given request path
487
+ and raises an HTTPFound exception to redirect the client.
488
+
489
+ Raises:
490
+ web.HTTPFound: Redirects the client to the new URL.
491
+ """
492
+ new_redirect_url = f"{str(req_path).rstrip('/')}/default/"
493
+ log.info("Redirecting to %s", new_redirect_url)
494
+ raise web.HTTPFound(new_redirect_url)
495
+
496
+
497
+ def _fetch_and_validate_required_env_vars():
498
+ EnvVars = namedtuple(
499
+ "EnvVars", ["mpm_port", "mpm_auth_token", "mpm_parent_pid", "base_url_prefix"]
500
+ )
501
+
502
+ port = os.getenv(mpm_env.get_env_name_mwi_mpm_port())
503
+ mpm_auth_token = os.getenv(mpm_env.get_env_name_mwi_mpm_auth_token())
504
+ ctx = os.getenv(mpm_env.get_env_name_mwi_mpm_parent_pid())
505
+
506
+ if not ctx or not port or not mpm_auth_token:
507
+ log.error("Error: One or more required environment variables are missing.")
508
+ sys.exit(1)
509
+
510
+ try:
511
+ base_url_prefix = os.getenv(mpm_env.get_env_name_base_url_prefix(), "")
512
+ mwi_mpm_port: int = int(port)
513
+ return EnvVars(
514
+ mpm_port=mwi_mpm_port,
515
+ mpm_auth_token=mpm_auth_token,
516
+ mpm_parent_pid=ctx,
517
+ base_url_prefix=base_url_prefix,
518
+ )
519
+ except ValueError as ve:
520
+ log.error("Error: Invalid type for port: %s", ve)
521
+ sys.exit(1)
522
+
523
+
524
+ def main() -> None:
525
+ """
526
+ The main entry point of the application. Starts the app and run until the shutdown
527
+ signal to terminate the app is received.
528
+ """
529
+ env_vars = _fetch_and_validate_required_env_vars()
530
+ asyncio.run(start_app(env_vars))
531
+
532
+
533
+ if __name__ == "__main__":
534
+ # This ensures that the app is not created when the module is imported and
535
+ # is only started when the script is run directly or via executable invocation
536
+ main()
@@ -0,0 +1,45 @@
1
+ # Copyright 2024-2025 The MathWorks, Inc.
2
+ import asyncio
3
+
4
+ from matlab_proxy_manager.utils import helpers, logger
5
+
6
+ log = logger.get()
7
+
8
+
9
+ class OrphanedProcessMonitor:
10
+ """
11
+ Class that provides behavior to track the idle state of the proxy manager app.
12
+ It periodically checks if the parent process is alive and triggers a shutdown event if not.
13
+ """
14
+
15
+ def __init__(self, app, delay: int = 1) -> None:
16
+ self.app = app
17
+ self.delay = delay
18
+
19
+ async def start(self) -> None:
20
+ """
21
+ Starts the monitoring process. Periodically checks if the parent process is alive.
22
+ If the parent process is not alive, it triggers the shutdown process.
23
+ """
24
+ while True:
25
+ try:
26
+ if not helpers.does_process_exist(self.app.get("parent_pid")):
27
+ log.info("Parent doesn't exist, calling self-shutdown")
28
+ await self.shutdown()
29
+ break
30
+ except Exception as ex:
31
+ log.debug("Couldn't check for parent's liveness with err: %s", ex)
32
+ await asyncio.sleep(self.delay)
33
+
34
+ async def shutdown(self) -> None:
35
+ """
36
+ Triggers the shutdown process by setting the shutdown event.
37
+ """
38
+
39
+ try:
40
+ # Set the shutdown async event to signal app shutdown to the app runner
41
+ shutdown_event = self.app.get("shutdown_event")
42
+ if shutdown_event and not shutdown_event.is_set():
43
+ shutdown_event.set()
44
+ except Exception as ex:
45
+ log.debug("Unable to set proxy manager shutdown event, err: %s", ex)
@@ -0,0 +1,65 @@
1
+ # Copyright 2024 The MathWorks, Inc.
2
+ from aiohttp import web
3
+ from watchdog.events import FileSystemEventHandler
4
+ from watchdog.observers import Observer
5
+
6
+ from matlab_proxy_manager.utils import logger
7
+ from matlab_proxy_manager.storage.file_repository import FileRepository
8
+ from matlab_proxy_manager.utils import helpers
9
+
10
+ log = logger.get()
11
+
12
+
13
+ class FileWatcher(FileSystemEventHandler):
14
+ """
15
+ A class to watch for file system events and update the server state accordingly.
16
+ """
17
+
18
+ def __init__(self, app: web.Application, data_dir: str) -> None:
19
+ """
20
+ Initialize the FileWatcher with the application and directory to watch.
21
+ """
22
+ self.app = app
23
+ self.data_dir = data_dir
24
+ super().__init__()
25
+
26
+ def on_created(self, event) -> None:
27
+ """
28
+ Handle the event when a file or directory is created.
29
+ """
30
+ try:
31
+ self.update_server_state()
32
+ except Exception:
33
+ log.error("Error handling created event:", exc_info=True)
34
+
35
+ def update_server_state(self) -> None:
36
+ """Update the server state from the repository."""
37
+ current_servers = {}
38
+ storage = FileRepository(self.data_dir)
39
+ servers = storage.get_all()
40
+ current_servers = {server.id: server.as_dict() for server in servers.values()}
41
+ self.app["servers"] = current_servers
42
+
43
+
44
+ def start_watcher(app: web.Application):
45
+ """
46
+ Start a file system watcher to monitor changes in the proxy manager data directory.
47
+ """
48
+ path_to_watch = helpers.create_and_get_proxy_manager_data_dir()
49
+ log.debug("Watching dir: %s", path_to_watch)
50
+ event_handler = FileWatcher(app, path_to_watch)
51
+ observer = Observer()
52
+ observer.schedule(event_handler, path_to_watch, recursive=True)
53
+ observer.start()
54
+ app["observer"] = observer
55
+ return observer
56
+
57
+
58
+ def stop_watcher(app):
59
+ """
60
+ Stop the file system watcher associated with the application.
61
+ This function stops and joins the observer thread if it exists in the application.
62
+ """
63
+ if "observer" in app:
64
+ app["observer"].stop()
65
+ app["observer"].join()
@@ -1,23 +0,0 @@
1
- {
2
- "files": {
3
- "main.css": "./static/css/main.d890078a.chunk.css",
4
- "main.js": "./static/js/main.c311d854.chunk.js",
5
- "main.js.map": "./static/js/main.c311d854.chunk.js.map",
6
- "runtime-main.js": "./static/js/runtime-main.f70e4d5f.js",
7
- "runtime-main.js.map": "./static/js/runtime-main.f70e4d5f.js.map",
8
- "static/js/2.13be6544.chunk.js": "./static/js/2.13be6544.chunk.js",
9
- "static/js/2.13be6544.chunk.js.map": "./static/js/2.13be6544.chunk.js.map",
10
- "index.html": "./index.html",
11
- "static/css/main.d890078a.chunk.css.map": "./static/css/main.d890078a.chunk.css.map",
12
- "static/js/2.13be6544.chunk.js.LICENSE.txt": "./static/js/2.13be6544.chunk.js.LICENSE.txt",
13
- "static/media/OverlayTrigger.css": "./static/media/trigger-ok.7b9c238b.svg",
14
- "static/media/Controls.css": "./static/media/terminate.7ea1363e.svg",
15
- "static/media/App.css": "./static/media/mathworks.c422935b.ttf"
16
- },
17
- "entrypoints": [
18
- "static/js/runtime-main.f70e4d5f.js",
19
- "static/js/2.13be6544.chunk.js",
20
- "static/css/main.d890078a.chunk.css",
21
- "static/js/main.c311d854.chunk.js"
22
- ]
23
- }