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,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
|
-
}
|