matlab-proxy 0.20.0__py3-none-any.whl → 0.22.0__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.

Potentially problematic release.


This version of matlab-proxy might be problematic. Click here for more details.

Files changed (39) hide show
  1. matlab_proxy/app.py +1 -1
  2. matlab_proxy/constants.py +1 -0
  3. matlab_proxy/gui/asset-manifest.json +6 -6
  4. matlab_proxy/gui/index.html +1 -1
  5. matlab_proxy/gui/static/css/main.6cd0caba.css +13 -0
  6. matlab_proxy/gui/static/css/main.6cd0caba.css.map +1 -0
  7. matlab_proxy/gui/static/js/main.77e6cbaf.js +3 -0
  8. matlab_proxy/gui/static/js/{main.9c68c75c.js.LICENSE.txt → main.77e6cbaf.js.LICENSE.txt} +0 -2
  9. matlab_proxy/gui/static/js/main.77e6cbaf.js.map +1 -0
  10. matlab_proxy/settings.py +1 -1
  11. {matlab_proxy-0.20.0.dist-info → matlab_proxy-0.22.0.dist-info}/METADATA +2 -1
  12. {matlab_proxy-0.20.0.dist-info → matlab_proxy-0.22.0.dist-info}/RECORD +34 -17
  13. {matlab_proxy-0.20.0.dist-info → matlab_proxy-0.22.0.dist-info}/entry_points.txt +1 -0
  14. matlab_proxy-0.22.0.dist-info/top_level.txt +3 -0
  15. matlab_proxy_manager/__init__.py +6 -0
  16. matlab_proxy_manager/lib/__init__.py +1 -0
  17. matlab_proxy_manager/lib/api.py +295 -0
  18. matlab_proxy_manager/storage/__init__.py +1 -0
  19. matlab_proxy_manager/storage/file_repository.py +144 -0
  20. matlab_proxy_manager/storage/interface.py +62 -0
  21. matlab_proxy_manager/storage/server.py +144 -0
  22. matlab_proxy_manager/utils/__init__.py +1 -0
  23. matlab_proxy_manager/utils/auth.py +77 -0
  24. matlab_proxy_manager/utils/constants.py +5 -0
  25. matlab_proxy_manager/utils/environment_variables.py +46 -0
  26. matlab_proxy_manager/utils/helpers.py +286 -0
  27. matlab_proxy_manager/utils/logger.py +73 -0
  28. matlab_proxy_manager/web/__init__.py +1 -0
  29. matlab_proxy_manager/web/app.py +447 -0
  30. matlab_proxy_manager/web/monitor.py +45 -0
  31. matlab_proxy_manager/web/watcher.py +54 -0
  32. tests/unit/test_app.py +1 -1
  33. matlab_proxy/gui/static/css/main.da9c4eb8.css +0 -13
  34. matlab_proxy/gui/static/css/main.da9c4eb8.css.map +0 -1
  35. matlab_proxy/gui/static/js/main.9c68c75c.js +0 -3
  36. matlab_proxy/gui/static/js/main.9c68c75c.js.map +0 -1
  37. matlab_proxy-0.20.0.dist-info/top_level.txt +0 -2
  38. {matlab_proxy-0.20.0.dist-info → matlab_proxy-0.22.0.dist-info}/LICENSE.md +0 -0
  39. {matlab_proxy-0.20.0.dist-info → matlab_proxy-0.22.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,447 @@
1
+ # Copyright 2024 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.util.mwi.environment_variables as mwi_env
15
+ import matlab_proxy.util.system as mwi_sys
16
+ import matlab_proxy_manager.lib.api as mpm_lib
17
+ from matlab_proxy.util.event_loop import get_event_loop
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.web import watcher
22
+ from matlab_proxy_manager.web.monitor import OrphanedProcessMonitor
23
+
24
+ # We use __all__ to list down all the public-facing APIs exported by this module
25
+ __all__ = ["proxy", "SHUTDOWN_EVENT"]
26
+
27
+ SHUTDOWN_EVENT = None
28
+ log = logger.get(init=True)
29
+
30
+
31
+ def init_app() -> web.Application:
32
+ """
33
+ Initialize and configure the aiohttp web application.
34
+
35
+ This function sets up the web application with necessary configurations,
36
+ including creating the proxy manager data directory, setting up an idle
37
+ timeout monitor, and configuring client sessions.
38
+
39
+ Returns:
40
+ web.Application: The configured aiohttp web application.
41
+ """
42
+ app = web.Application()
43
+
44
+ # Create and get the proxy manager data directory
45
+ try:
46
+ data_dir = helpers.create_and_get_proxy_manager_data_dir()
47
+ app["data_dir"] = data_dir
48
+ except Exception as ex:
49
+ raise RuntimeError(f"Failed to create or get data directory: {ex}") from ex
50
+
51
+ # Setup idle timeout monitor for the app
52
+ monitor = OrphanedProcessMonitor(app)
53
+
54
+ async def start_idle_monitor(app):
55
+ """Start the idle timeout monitor."""
56
+ asyncio.create_task(monitor.start())
57
+
58
+ async def create_client_session(app):
59
+ """Create an aiohttp client session."""
60
+ app["session"] = ClientSession(
61
+ trust_env=True, connector=aiohttp.TCPConnector(ssl=False, ttl_dns_cache=600)
62
+ )
63
+
64
+ async def cleanup_client_session(app):
65
+ """Cleanup the aiohttp client session."""
66
+ await app["session"].close()
67
+
68
+ app.on_startup.append(start_idle_monitor)
69
+ app.on_startup.append(create_client_session)
70
+ app.on_cleanup.append(helpers.delete_dangling_servers)
71
+ app.on_cleanup.append(cleanup_client_session)
72
+
73
+ app.router.add_route("*", "/{tail:.*}", proxy)
74
+
75
+ return app
76
+
77
+
78
+ async def start_app(env_vars: namedtuple):
79
+ """
80
+ Initialize and start the web application.
81
+
82
+ This function sets up logging, initializes the web application, and starts
83
+ the default matlab proxy. It also sets up signal handlers for graceful shutdown
84
+ and starts a file watcher in a separate thread.
85
+
86
+ Raises:
87
+ Exception: If any error occurs during the application startup or runtime.
88
+ """
89
+ # Async events are utilized to signal app termination from other modules,
90
+ # necessitating the use of a global variable. To avoid potential issues with variable attachment
91
+ # to other event loops in Python versions prior to 3.10, the variable is initialized locally
92
+ # rather than globally.
93
+ global SHUTDOWN_EVENT
94
+ app = init_app()
95
+
96
+ app["port"] = env_vars.mpm_port
97
+ app["auth_token"] = env_vars.mpm_auth_token
98
+ app["parent_pid"] = env_vars.mpm_parent_pid
99
+
100
+ # Start default matlab proxy
101
+ await _start_default_proxy(app)
102
+
103
+ web_logger = None if not mwi_env.is_web_logging_enabled() else log
104
+
105
+ # Run the app
106
+ runner = web.AppRunner(app, logger=web_logger, access_log=web_logger)
107
+ await runner.setup()
108
+ site = web.TCPSite(runner, port=env_vars.mpm_port)
109
+ await site.start()
110
+ log.debug("Proxy manager started at http://127.0.0.1:%d", site._port)
111
+
112
+ # Get the default event loop
113
+ loop = get_event_loop()
114
+
115
+ # Run the observer in a separate thread
116
+ loop.run_in_executor(None, watcher.start_watcher, app)
117
+
118
+ # Register signal handler for graceful shutdown
119
+ _register_signal_handler(loop)
120
+
121
+ SHUTDOWN_EVENT = asyncio.Event()
122
+
123
+ # Wait for receiving shutdown_event (set by interrupts or by monitoring process)
124
+ await SHUTDOWN_EVENT.wait()
125
+
126
+ # After receiving the shutdown signal, perform cleanup by stopping the web server
127
+ await runner.cleanup()
128
+
129
+
130
+ def _register_signal_handler(loop):
131
+ """
132
+ Registers signal handlers for supported termination signals to allow for graceful shutdown
133
+ of the application.
134
+
135
+ Args:
136
+ loop (asyncio.AbstractEventLoop): The event loop to which the signal handlers
137
+ should be added.
138
+ """
139
+ signals = mwi_sys.get_supported_termination_signals()
140
+ for sig_name in signals:
141
+ if mwi_sys.is_posix():
142
+ loop.add_signal_handler(sig_name, catch_signals)
143
+ else:
144
+ # loop.add_signal_handler() is not yet supported in Windows.
145
+ # Using the 'signal' package instead.
146
+ signal.signal(sig_name, catch_signals)
147
+
148
+
149
+ def catch_signals(*args):
150
+ """Handle termination signals for graceful shutdown."""
151
+ # Poll for parent process to clean up to avoid race conditions in cleanup of matlab proxies
152
+ helpers.poll_for_server_deletion()
153
+ SHUTDOWN_EVENT.set()
154
+
155
+
156
+ async def _start_default_proxy(app):
157
+ """
158
+ Starts the default MATLAB proxy and updates the application state.
159
+
160
+ Args:
161
+ app : The aiohttp web application.
162
+ """
163
+ server_process = await mpm_lib.start_matlab_proxy_for_jsp(
164
+ parent_id=app.get("parent_pid"),
165
+ is_isolated_matlab=False,
166
+ mpm_auth_token=app.get("auth_token"),
167
+ )
168
+ if not server_process:
169
+ log.error("Failed to start default matlab proxy using Jupyter")
170
+ return
171
+
172
+ # Load existing matlab proxy servers into app state for consistency
173
+ app["servers"] = helpers.pre_load_from_state_file(app.get("data_dir"))
174
+
175
+ # Add the new/existing server to the app state
176
+ app["servers"][server_process.get("id")] = server_process
177
+
178
+
179
+ @authenticate_access_decorator
180
+ async def proxy(req):
181
+ """
182
+ Proxy incoming HTTP requests to the appropriate MATLAB proxy backend server.
183
+
184
+ This function handles requests by:
185
+ 1. Redirecting paths ending with '/matlab/' to '/matlab/default/'.
186
+ 2. Extracting client identifiers from the request path.
187
+ 3. Routing the request to the appropriate MATLAB backend server based on the client identifier.
188
+ 4. Handling various exceptions and providing appropriate HTTP responses.
189
+
190
+ Args:
191
+ req (aiohttp.web.Request): The incoming HTTP request.
192
+
193
+ Returns:
194
+ aiohttp.web.Response: The HTTP response from the backend server or an error page.
195
+
196
+ Raises:
197
+ aiohttp.web.HTTPFound: If the request path needs to be redirected.
198
+ aiohttp.web.HTTPServiceUnavailable: If the MATLAB proxy process is not running.
199
+ aiohttp.web.HTTPNotFound: If the request cannot be forwarded to the MATLAB proxy.
200
+ """
201
+ # Special keys for web socket requests
202
+ connection = "connection"
203
+ upgrade = "upgrade"
204
+ req_headers = req.headers.copy()
205
+ req_body = await req.read()
206
+
207
+ # Set content length in case of modification
208
+ req_headers["Content-Length"] = str(len(req_body))
209
+ req_headers["x-forwarded-proto"] = "http"
210
+ req_path = req.rel_url
211
+
212
+ # Redirect block to move /*/matlab to /*/matlab/default/
213
+ if str(req_path).endswith(f"{constants.MWI_BASE_URL_PREFIX}"):
214
+ return _redirect_to_default(req_path)
215
+
216
+ match = re.compile(r".*?/matlab/([^/]+)/(.*)").match(str(req.rel_url))
217
+
218
+ if not match:
219
+ # Path doesn't contain /matlab/default|<id> in the request path
220
+ # redirect to error page
221
+ log.debug("Regex match not found, match: %s", match)
222
+ return _render_error_page(
223
+ "Incorrect request path in the URL, please try with correct URL."
224
+ )
225
+
226
+ ident = match.group(1).rstrip("/")
227
+ log.debug("Client identifier for proxy: %s", ident)
228
+
229
+ ctx = req_headers.get(constants.HEADER_MWI_MPM_CONTEXT)
230
+ if not ctx:
231
+ log.debug("MPM Context header not found in the request")
232
+ return _render_error_page(
233
+ "Required header (MWI-MPM-CONTEXT) not found in the request"
234
+ )
235
+
236
+ client_key = f"{ctx}_{ident}"
237
+ default_key = f"{ctx}_default"
238
+ group_two_rel_url = match.group(2)
239
+
240
+ backend_server = _get_backend_server(req, client_key, default_key)
241
+ proxy_url = f'{backend_server.get("absolute_url")}/{group_two_rel_url}'
242
+ log.debug("Proxy URL: %s", proxy_url)
243
+
244
+ if (
245
+ req_headers.get(connection, "").lower() == upgrade
246
+ and req_headers.get(upgrade, "").lower() == "websocket"
247
+ and req.method == "GET"
248
+ ):
249
+ return await _handle_websocket_request(req, proxy_url)
250
+ try:
251
+ return await _handle_http_request(req, req_body, proxy_url, backend_server)
252
+ except web.HTTPFound:
253
+ log.debug("Redirection to path with /default")
254
+ raise
255
+
256
+ # Handles any pending HTTP requests from the browser when the MATLAB proxy process is
257
+ # terminated before responding to them.
258
+ except (
259
+ client_exceptions.ServerDisconnectedError,
260
+ client_exceptions.ClientConnectionError,
261
+ ) as ex:
262
+ log.debug("MATLAB proxy process may not be running.")
263
+ raise web.HTTPServiceUnavailable() from ex
264
+ except Exception as err:
265
+ log.error("Failed to forward HTTP request to MATLAB proxy with error: %s", err)
266
+ raise web.HTTPNotFound() from err
267
+
268
+
269
+ async def _handle_websocket_request(
270
+ req: web.Request, proxy_url: str
271
+ ) -> web.WebSocketResponse:
272
+ """Handles a websocket request to the backend matlab proxy server
273
+
274
+ Args:
275
+ req (web.Request): websocket request from the client
276
+ proxy_url (str): backend matlab proxy server URL
277
+
278
+ Raises:
279
+ ValueError: when an unexpected websocket message type is received
280
+ aiohttp.WebSocketError: For any exception raised while forwarding request from src to dest
281
+
282
+ Returns:
283
+ web.WebSocketResponse: The response from the backend server
284
+ """
285
+ ws_server = web.WebSocketResponse()
286
+ await ws_server.prepare(req)
287
+
288
+ async with aiohttp.ClientSession(
289
+ trust_env=True,
290
+ cookies=req.cookies,
291
+ connector=aiohttp.TCPConnector(ssl=False),
292
+ ) as client_session:
293
+ try:
294
+ async with client_session.ws_connect(proxy_url) as ws_client:
295
+
296
+ async def ws_forward(ws_src, ws_dest):
297
+ async for msg in ws_src:
298
+ msg_type = msg.type
299
+ msg_data = msg.data
300
+
301
+ # When a websocket is closed by the MATLAB JSD, it sends out a few
302
+ # http requests to the Embedded Connector about the events that had occurred
303
+ # (figureWindowClosed etc.) The Embedded Connector responds by sending a
304
+ # message of type 'Error' with close code as Abnormal closure. When this
305
+ # happens, matlab-proxy can safely exit out of the loop and close the
306
+ # websocket connection it has with the Embedded Connector (ws_client)
307
+ if (
308
+ msg_type == aiohttp.WSMsgType.ERROR
309
+ and ws_src.close_code
310
+ == aiohttp.WSCloseCode.ABNORMAL_CLOSURE
311
+ ):
312
+ log.debug(
313
+ "Src: %s, msg_type= %s, ws_src.close_code= %s",
314
+ ws_src,
315
+ msg_type,
316
+ ws_src.close_code,
317
+ )
318
+ break
319
+ if msg_type == aiohttp.WSMsgType.TEXT:
320
+ await ws_dest.send_str(msg_data)
321
+ elif msg_type == aiohttp.WSMsgType.BINARY:
322
+ await ws_dest.send_bytes(msg_data)
323
+ elif msg_type == aiohttp.WSMsgType.PING:
324
+ await ws_dest.ping()
325
+ elif msg_type == aiohttp.WSMsgType.PONG:
326
+ await ws_dest.pong()
327
+ elif ws_dest.closed:
328
+ log.debug("Destination: %s closed", ws_dest)
329
+ await ws_dest.close(
330
+ code=ws_dest.close_code, message=msg.extra
331
+ )
332
+ else:
333
+ raise ValueError(f"Unexpected message type: {msg}")
334
+
335
+ await asyncio.wait(
336
+ [
337
+ asyncio.create_task(ws_forward(ws_server, ws_client)),
338
+ asyncio.create_task(ws_forward(ws_client, ws_server)),
339
+ ],
340
+ return_when=asyncio.FIRST_COMPLETED,
341
+ )
342
+
343
+ return ws_server
344
+ except Exception as err:
345
+ log.error("Failed to create web socket connection with error: %s", err)
346
+
347
+ code, message = (
348
+ aiohttp.WSCloseCode.INTERNAL_ERROR,
349
+ "Failed to establish websocket connection with the backend server",
350
+ )
351
+ await ws_server.close(code=code, message=message.encode("utf-8"))
352
+ raise aiohttp.WebSocketError(code=code, message=message)
353
+
354
+
355
+ # Helper private functions
356
+
357
+
358
+ async def _handle_http_request(
359
+ req: web.Request, req_body: Optional[bytes], proxy_url: str, backend_server: dict
360
+ ) -> web.Response:
361
+ """
362
+ Forwards an incoming HTTP request to a specified backend server.
363
+
364
+ Returns:
365
+ web.Response: The response from the backend server, including headers, status, and body.
366
+ """
367
+ client_session = req.app.get("session")
368
+ async with client_session.request(
369
+ req.method,
370
+ proxy_url,
371
+ allow_redirects=True,
372
+ data=req_body,
373
+ headers=backend_server.get("headers"),
374
+ ) as res:
375
+ headers = res.headers.copy()
376
+ body = await res.read()
377
+ return web.Response(headers=headers, status=res.status, body=body)
378
+
379
+
380
+ def _get_backend_server(req: web.Request, client_key: str, default_key: str) -> dict:
381
+ """
382
+ Retrieves the backend server configuration for a given client key.
383
+ """
384
+ app = req.app
385
+ backend_server = app["servers"].get(client_key)
386
+ # Route to default matlab if the specified path doesn't exist
387
+ if not backend_server:
388
+ log.debug("Client not found in the current servers, using default matlab proxy")
389
+ backend_server = app["servers"].get(default_key)
390
+ return backend_server
391
+
392
+
393
+ def _redirect_to_default(req_path) -> None:
394
+ """
395
+ Redirects the request to the default path.
396
+
397
+ This function constructs a new URL by appending '/default/' to the given request path
398
+ and raises an HTTPFound exception to redirect the client.
399
+
400
+ Raises:
401
+ web.HTTPFound: Redirects the client to the new URL.
402
+ """
403
+ new_redirect_url = f'{str(req_path).rstrip("/")}/default/'
404
+ log.info("Redirecting to %s", new_redirect_url)
405
+ raise web.HTTPFound(new_redirect_url)
406
+
407
+
408
+ def _render_error_page(error_msg: str) -> web.Response:
409
+ """Returns 503 with error text"""
410
+ return web.HTTPServiceUnavailable(text=f"Error: {error_msg}")
411
+
412
+
413
+ def main() -> None:
414
+ """
415
+ The main entry point of the application. Starts the app and run until the shutdown
416
+ signal to terminate the app is received.
417
+ """
418
+ env_vars: namedtuple = _fetch_and_validate_required_env_vars()
419
+ loop: asyncio.AbstractEventLoop | asyncio.ProactorEventLoop = get_event_loop()
420
+ loop.run_until_complete(start_app(env_vars))
421
+
422
+
423
+ def _fetch_and_validate_required_env_vars() -> namedtuple:
424
+ EnvVars = namedtuple("EnvVars", ["mpm_port", "mpm_auth_token", "mpm_parent_pid"])
425
+
426
+ port = os.getenv(mpm_env.get_env_name_mwi_mpm_port())
427
+ mpm_auth_token = os.getenv(mpm_env.get_env_name_mwi_mpm_auth_token())
428
+ ctx = os.getenv(mpm_env.get_env_name_mwi_mpm_parent_pid())
429
+
430
+ if not ctx or not port or not mpm_auth_token:
431
+ print("Error: One or more required environment variables are missing.")
432
+ sys.exit(1)
433
+
434
+ try:
435
+ mwi_mpm_port: int = int(port)
436
+ return EnvVars(
437
+ mpm_port=mwi_mpm_port, mpm_auth_token=mpm_auth_token, mpm_parent_pid=ctx
438
+ )
439
+ except ValueError as ve:
440
+ print("Error: Invalid type for port: ", ve)
441
+ sys.exit(1)
442
+
443
+
444
+ if __name__ == "__main__":
445
+ # This ensures that the app is not created when the module is imported and
446
+ # is only started when the script is run directly or via executable invocation
447
+ main()
@@ -0,0 +1,45 @@
1
+ # Copyright 2024 The MathWorks, Inc.
2
+ import asyncio
3
+
4
+ from matlab_proxy_manager.utils import logger
5
+ from matlab_proxy_manager.utils import helpers
6
+
7
+ log = logger.get()
8
+
9
+
10
+ class OrphanedProcessMonitor:
11
+ """
12
+ Class that provides behavior to track the idle state of the proxy manager app.
13
+ It periodically checks if the parent process is alive and triggers a shutdown event if not.
14
+ """
15
+
16
+ def __init__(self, app, delay: int = 1) -> None:
17
+ self.app = app
18
+ self.delay = delay
19
+
20
+ async def start(self) -> None:
21
+ """
22
+ Starts the monitoring process. Periodically checks if the parent process is alive.
23
+ If the parent process is not alive, it triggers the shutdown process.
24
+ """
25
+ while True:
26
+ try:
27
+ if not helpers.does_process_exist(self.app.get("parent_pid")):
28
+ log.info("Parent doesn't exist, calling self-shutdown")
29
+ await self.shutdown()
30
+ break
31
+ except Exception as ex:
32
+ log.debug("Couldn't check for parent's liveness with err: %s", ex)
33
+ await asyncio.sleep(self.delay)
34
+
35
+ async def shutdown(self) -> None:
36
+ """
37
+ Triggers the shutdown process by setting the shutdown event.
38
+ """
39
+ from matlab_proxy_manager.web.app import SHUTDOWN_EVENT
40
+
41
+ try:
42
+ # Set the shutdown async event to signal app shutdown to the app runner
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,54 @@
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
+ return observer
tests/unit/test_app.py CHANGED
@@ -447,7 +447,7 @@ async def test_matlab_proxy_404(proxy_payload, test_server):
447
447
  count = 0
448
448
  while True:
449
449
  resp = await test_server.post(
450
- "./1234.html", data=json.dumps(proxy_payload), headers=headers
450
+ "/1234.html", data=json.dumps(proxy_payload), headers=headers
451
451
  )
452
452
  if resp.status == HTTPStatus.SERVICE_UNAVAILABLE:
453
453
  time.sleep(test_constants.ONE_SECOND_DELAY)