matlab-proxy 0.23.1__py3-none-any.whl → 0.23.4__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.
- matlab_proxy/app_state.py +55 -45
- matlab_proxy/settings.py +1 -1
- matlab_proxy/util/__init__.py +1 -1
- matlab_proxy/util/mwi/validators.py +1 -1
- {matlab_proxy-0.23.1.dist-info → matlab_proxy-0.23.4.dist-info}/METADATA +1 -1
- {matlab_proxy-0.23.1.dist-info → matlab_proxy-0.23.4.dist-info}/RECORD +17 -17
- {matlab_proxy-0.23.1.dist-info → matlab_proxy-0.23.4.dist-info}/WHEEL +1 -1
- matlab_proxy_manager/lib/api.py +90 -49
- matlab_proxy_manager/storage/server.py +2 -2
- matlab_proxy_manager/utils/helpers.py +38 -21
- matlab_proxy_manager/web/app.py +92 -49
- matlab_proxy_manager/web/monitor.py +1 -2
- matlab_proxy_manager/web/watcher.py +11 -0
- tests/unit/test_app_state.py +79 -4
- {matlab_proxy-0.23.1.dist-info → matlab_proxy-0.23.4.dist-info}/LICENSE.md +0 -0
- {matlab_proxy-0.23.1.dist-info → matlab_proxy-0.23.4.dist-info}/entry_points.txt +0 -0
- {matlab_proxy-0.23.1.dist-info → matlab_proxy-0.23.4.dist-info}/top_level.txt +0 -0
matlab_proxy/app_state.py
CHANGED
|
@@ -58,13 +58,13 @@ class AppState:
|
|
|
58
58
|
self.settings = settings
|
|
59
59
|
self.processes = {"matlab": None, "xvfb": None}
|
|
60
60
|
|
|
61
|
-
# Timeout for processes
|
|
61
|
+
# Timeout for processes started by matlab-proxy
|
|
62
62
|
self.PROCESS_TIMEOUT = get_process_startup_timeout()
|
|
63
63
|
|
|
64
|
-
# The port on which MATLAB(
|
|
64
|
+
# The port on which MATLAB(started by this matlab-proxy process) starts on.
|
|
65
65
|
self.matlab_port = None
|
|
66
66
|
|
|
67
|
-
# The directory in which the instance of MATLAB (
|
|
67
|
+
# The directory in which the instance of MATLAB (started by this matlab-proxy process) will write logs to.
|
|
68
68
|
self.mwi_logs_dir = None
|
|
69
69
|
|
|
70
70
|
# Dictionary of all files used to manage the MATLAB session.
|
|
@@ -99,7 +99,7 @@ class AppState:
|
|
|
99
99
|
self.embedded_connector_start_time = None
|
|
100
100
|
|
|
101
101
|
# Keep track of the state of the Embedded Connector.
|
|
102
|
-
# If there is some problem with
|
|
102
|
+
# If there is some problem with starting the Embedded Connector(say an issue with licensing),
|
|
103
103
|
# the state of MATLAB process in app_state will continue to be in a 'starting' indefinitely.
|
|
104
104
|
# This variable can be either "up" or "down"
|
|
105
105
|
self.embedded_connector_state = "down"
|
|
@@ -222,7 +222,7 @@ class AppState:
|
|
|
222
222
|
def __delete_cached_config_file(self):
|
|
223
223
|
"""Deletes the cached config file"""
|
|
224
224
|
try:
|
|
225
|
-
logger.
|
|
225
|
+
logger.debug(f"Deleting any cached config files!")
|
|
226
226
|
os.remove(self.__get_cached_config_file())
|
|
227
227
|
except FileNotFoundError:
|
|
228
228
|
# The file being absent is acceptable.
|
|
@@ -230,7 +230,7 @@ class AppState:
|
|
|
230
230
|
|
|
231
231
|
def __reset_and_delete_cached_config(self):
|
|
232
232
|
"""Reset licensing variable of the class and removes the cached config file."""
|
|
233
|
-
logger.
|
|
233
|
+
logger.debug(f"Resetting cached config information...")
|
|
234
234
|
self.licensing = None
|
|
235
235
|
self.__delete_cached_config_file()
|
|
236
236
|
|
|
@@ -259,14 +259,14 @@ class AppState:
|
|
|
259
259
|
# Default value
|
|
260
260
|
self.licensing = None
|
|
261
261
|
|
|
262
|
-
# If MWI_USE_EXISTING_LICENSE is set in environment, try
|
|
262
|
+
# If MWI_USE_EXISTING_LICENSE is set in environment, try starting MATLAB directly
|
|
263
263
|
if self.settings["mwi_use_existing_license"]:
|
|
264
264
|
self.licensing = {"type": "existing_license"}
|
|
265
265
|
logger.debug(
|
|
266
266
|
f"{mwi_env.get_env_name_mwi_use_existing_license()} variable set in environment"
|
|
267
267
|
)
|
|
268
268
|
logger.info(
|
|
269
|
-
f"!!!
|
|
269
|
+
f"!!! Starting MATLAB without providing any additional licensing information. This requires MATLAB to have been activated on the machine from which its being started !!!"
|
|
270
270
|
)
|
|
271
271
|
|
|
272
272
|
# Delete old config info from cache to ensure its wiped out first before persisting new info.
|
|
@@ -276,7 +276,7 @@ class AppState:
|
|
|
276
276
|
elif self.settings.get("nlm_conn_str", None) is not None:
|
|
277
277
|
nlm_licensing_str = self.settings.get("nlm_conn_str")
|
|
278
278
|
logger.debug(f"Found NLM:[{nlm_licensing_str}] set in environment")
|
|
279
|
-
logger.
|
|
279
|
+
logger.info(f"Using NLM:{nlm_licensing_str} to connect...")
|
|
280
280
|
self.licensing = {
|
|
281
281
|
"type": "nlm",
|
|
282
282
|
"conn_str": nlm_licensing_str,
|
|
@@ -307,7 +307,7 @@ class AppState:
|
|
|
307
307
|
"type": "nlm",
|
|
308
308
|
"conn_str": licensing["conn_str"],
|
|
309
309
|
}
|
|
310
|
-
logger.
|
|
310
|
+
logger.debug("Using cached NLM licensing to start MATLAB")
|
|
311
311
|
|
|
312
312
|
elif licensing["type"] == "mhlm":
|
|
313
313
|
self.licensing = {
|
|
@@ -335,12 +335,12 @@ class AppState:
|
|
|
335
335
|
)
|
|
336
336
|
if successful_update:
|
|
337
337
|
logger.debug(
|
|
338
|
-
"Using cached Online Licensing to
|
|
338
|
+
"Using cached Online Licensing to start MATLAB."
|
|
339
339
|
)
|
|
340
340
|
else:
|
|
341
341
|
self.__reset_and_delete_cached_config()
|
|
342
342
|
elif licensing["type"] == "existing_license":
|
|
343
|
-
logger.
|
|
343
|
+
logger.debug("Using cached existing license to start MATLAB")
|
|
344
344
|
self.licensing = licensing
|
|
345
345
|
else:
|
|
346
346
|
# Somethings wrong, licensing is neither NLM or MHLM
|
|
@@ -920,14 +920,14 @@ class AppState:
|
|
|
920
920
|
try:
|
|
921
921
|
for session_file in self.mwi_server_session_files.items():
|
|
922
922
|
if session_file[1] is not None:
|
|
923
|
-
logger.
|
|
923
|
+
logger.debug(f"Deleting:{session_file[1]}")
|
|
924
924
|
session_file[1].unlink()
|
|
925
925
|
except FileNotFoundError:
|
|
926
926
|
# Files may not exist if cleanup is called before they are created
|
|
927
927
|
pass
|
|
928
928
|
|
|
929
929
|
async def __setup_env_for_matlab(self) -> dict:
|
|
930
|
-
"""Configure the environment variables required for
|
|
930
|
+
"""Configure the environment variables required for starting MATLAB by matlab-proxy.
|
|
931
931
|
|
|
932
932
|
Returns:
|
|
933
933
|
[dict]: Containing keys as the Env variable names and values are its corresponding values.
|
|
@@ -979,17 +979,17 @@ class AppState:
|
|
|
979
979
|
if system.is_linux():
|
|
980
980
|
if self.settings.get("matlab_display", None):
|
|
981
981
|
matlab_env["DISPLAY"] = self.settings["matlab_display"]
|
|
982
|
-
logger.
|
|
983
|
-
f"Using the display number supplied by Xvfb process'{matlab_env['DISPLAY']}' for
|
|
982
|
+
logger.debug(
|
|
983
|
+
f"Using the display number supplied by Xvfb process'{matlab_env['DISPLAY']}' for starting MATLAB"
|
|
984
984
|
)
|
|
985
985
|
else:
|
|
986
986
|
if "DISPLAY" in matlab_env:
|
|
987
|
-
logger.
|
|
988
|
-
f"Using the existing DISPLAY environment variable with value:{matlab_env['DISPLAY']} for
|
|
987
|
+
logger.debug(
|
|
988
|
+
f"Using the existing DISPLAY environment variable with value:{matlab_env['DISPLAY']} for starting MATLAB"
|
|
989
989
|
)
|
|
990
990
|
else:
|
|
991
|
-
logger.
|
|
992
|
-
"No DISPLAY environment variable found.
|
|
991
|
+
logger.debug(
|
|
992
|
+
"No DISPLAY environment variable found. Starting MATLAB without it."
|
|
993
993
|
)
|
|
994
994
|
|
|
995
995
|
# The matlab ready file is written into this location(self.mwi_logs_dir) by MATLAB
|
|
@@ -1080,11 +1080,11 @@ class AppState:
|
|
|
1080
1080
|
|
|
1081
1081
|
return xvfb
|
|
1082
1082
|
|
|
1083
|
-
# If something went wrong ie. exception is raised in
|
|
1083
|
+
# If something went wrong ie. exception is raised in starting Xvfb process, capture error for logging
|
|
1084
1084
|
# and for showing the error on the frontend.
|
|
1085
1085
|
|
|
1086
1086
|
# FileNotFoundError: is thrown if Xvfb is not found on System Path.
|
|
1087
|
-
# XvfbError: is thrown if something went wrong when
|
|
1087
|
+
# XvfbError: is thrown if something went wrong when starting Xvfb process.
|
|
1088
1088
|
except (FileNotFoundError, XvfbError) as err:
|
|
1089
1089
|
self.error = XvfbError(
|
|
1090
1090
|
"""Unable to start the Xvfb process. Ensure Xvfb is installed and is available on the System Path. See https://github.com/mathworks/matlab-proxy#requirements for information on Xvfb"""
|
|
@@ -1178,7 +1178,7 @@ class AppState:
|
|
|
1178
1178
|
else:
|
|
1179
1179
|
time_diff = time.time() - self.embedded_connector_start_time
|
|
1180
1180
|
if time_diff > self.PROCESS_TIMEOUT:
|
|
1181
|
-
# Since max allowed startup time has elapsed, it means that MATLAB is
|
|
1181
|
+
# Since max allowed startup time has elapsed, it means that MATLAB is stuck and is unable to start.
|
|
1182
1182
|
# Set the error and stop matlab.
|
|
1183
1183
|
user_visible_error = "Unable to start MATLAB.\nTry again by clicking Start MATLAB."
|
|
1184
1184
|
|
|
@@ -1187,32 +1187,27 @@ class AppState:
|
|
|
1187
1187
|
# So, raise a generic error wherever appropriate
|
|
1188
1188
|
generic_error = f"MATLAB did not start in {int(self.PROCESS_TIMEOUT)} seconds. Use Windows Remote Desktop to check for any errors."
|
|
1189
1189
|
logger.error(f":{this_task}: {generic_error}")
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
break
|
|
1197
|
-
else:
|
|
1198
|
-
# Do not stop the MATLAB process or break from the loop (the error type is unknown)
|
|
1199
|
-
self.error = MatlabError(generic_error)
|
|
1200
|
-
await asyncio.sleep(5)
|
|
1201
|
-
continue
|
|
1190
|
+
|
|
1191
|
+
# Stopping the MATLAB process would remove the UI window displaying the error too.
|
|
1192
|
+
# Do not stop the MATLAB or break from the loop (as the error is still unknown)
|
|
1193
|
+
self.error = MatlabError(generic_error)
|
|
1194
|
+
await asyncio.sleep(5)
|
|
1195
|
+
continue
|
|
1202
1196
|
|
|
1203
1197
|
else:
|
|
1204
|
-
# If there are no logs after the max startup time has elapsed, it means that MATLAB is
|
|
1198
|
+
# If there are no logs after the max startup time has elapsed, it means that MATLAB is stuck and is unable to start.
|
|
1205
1199
|
# Set the error and stop matlab.
|
|
1206
1200
|
logger.error(
|
|
1207
1201
|
f":{this_task}: MATLAB did not start in {int(self.PROCESS_TIMEOUT)} seconds!"
|
|
1208
1202
|
)
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1203
|
+
# MATLAB can be stopped on posix systems because the stderr pipe of the MATLAB process is
|
|
1204
|
+
# read (by __matlab_stderr_reader_posix() task) and is logged by matlab-proxy appropriately.
|
|
1205
|
+
await self.__force_stop_matlab(
|
|
1206
|
+
user_visible_error, this_task
|
|
1207
|
+
)
|
|
1208
|
+
# Breaking out of the loop to end this task as matlab-proxy was unable to start MATLAB successfully
|
|
1209
|
+
# even after waiting for self.PROCESS_TIMEOUT
|
|
1210
|
+
break
|
|
1216
1211
|
|
|
1217
1212
|
else:
|
|
1218
1213
|
logger.debug(
|
|
@@ -1410,7 +1405,7 @@ class AppState:
|
|
|
1410
1405
|
if session_file_path is not None:
|
|
1411
1406
|
self.matlab_session_files[session_file_name] = None
|
|
1412
1407
|
with contextlib.suppress(FileNotFoundError):
|
|
1413
|
-
logger.
|
|
1408
|
+
logger.debug(f"Deleting:{session_file_path}")
|
|
1414
1409
|
session_file_path.unlink()
|
|
1415
1410
|
|
|
1416
1411
|
# In posix systems, variable matlab is an instance of asyncio.subprocess.Process()
|
|
@@ -1432,11 +1427,26 @@ class AppState:
|
|
|
1432
1427
|
else:
|
|
1433
1428
|
logger.debug("Sending HTTP request to stop the MATLAB process...")
|
|
1434
1429
|
try:
|
|
1430
|
+
import sys
|
|
1431
|
+
|
|
1435
1432
|
# Send HTTP request
|
|
1436
1433
|
await self.__send_stop_request_to_matlab()
|
|
1437
1434
|
|
|
1435
|
+
# Close the stderr stream to prevent indefinite hanging on it due to a child
|
|
1436
|
+
# process inheriting it, fixes https://github.com/mathworks/matlab-proxy/issues/44
|
|
1437
|
+
stderr_stream = matlab._transport.get_pipe_transport(
|
|
1438
|
+
sys.stderr.fileno()
|
|
1439
|
+
)
|
|
1440
|
+
if stderr_stream:
|
|
1441
|
+
logger.debug(
|
|
1442
|
+
"Closing matlab process stderr stream: %s",
|
|
1443
|
+
stderr_stream,
|
|
1444
|
+
)
|
|
1445
|
+
stderr_stream.close()
|
|
1446
|
+
|
|
1438
1447
|
# Wait for matlab to shutdown gracefully
|
|
1439
1448
|
await matlab.wait()
|
|
1449
|
+
|
|
1440
1450
|
assert (
|
|
1441
1451
|
matlab.returncode == 0
|
|
1442
1452
|
), "Failed to gracefully shutdown MATLAB via the embedded connector"
|
|
@@ -1496,7 +1506,7 @@ class AppState:
|
|
|
1496
1506
|
if system.is_posix():
|
|
1497
1507
|
xvfb = self.processes["xvfb"]
|
|
1498
1508
|
if xvfb is not None and xvfb.returncode is None:
|
|
1499
|
-
logger.
|
|
1509
|
+
logger.debug(f"Terminating Xvfb (PID={xvfb.pid})")
|
|
1500
1510
|
xvfb.terminate()
|
|
1501
1511
|
waiters.append(xvfb.wait())
|
|
1502
1512
|
|
matlab_proxy/settings.py
CHANGED
|
@@ -103,7 +103,7 @@ def get_matlab_executable_and_root_path():
|
|
|
103
103
|
# Note, error messages are formatted as multi-line strings and the front end displays them as is.
|
|
104
104
|
error_message = "Unable to find MATLAB on the system PATH. Add MATLAB to the system PATH, and restart matlab-proxy."
|
|
105
105
|
|
|
106
|
-
logger.
|
|
106
|
+
logger.error(error_message)
|
|
107
107
|
raise MatlabInstallError(error_message)
|
|
108
108
|
|
|
109
109
|
|
matlab_proxy/util/__init__.py
CHANGED
|
@@ -326,7 +326,7 @@ def validate_matlab_root_path(matlab_root: Path, is_custom_matlab_root: bool):
|
|
|
326
326
|
|
|
327
327
|
try:
|
|
328
328
|
__validate_if_paths_exist([matlab_root])
|
|
329
|
-
logger.
|
|
329
|
+
logger.debug(
|
|
330
330
|
f"MATLAB root path: {matlab_root} exists, continuing to verify its validity..."
|
|
331
331
|
)
|
|
332
332
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
matlab_proxy/__init__.py,sha256=6cwi8buKCMtw9OeWaOYUHEoqwl5MyJ_s6GxgNuqPuNg,1673
|
|
2
2
|
matlab_proxy/app.py,sha256=7BsHSM3PxTjN6eN10HLj74Y1wtu-Gl92g1RLbaTGz9U,34698
|
|
3
|
-
matlab_proxy/app_state.py,sha256=
|
|
3
|
+
matlab_proxy/app_state.py,sha256=x8rXfAA-TIvHARI4BLhP00VxYnrvhOLw6hKs_HTUyTE,71700
|
|
4
4
|
matlab_proxy/constants.py,sha256=L9fhXdcGH7Eu56h0aB_TNm3d6aNFr2nP6-HyQJ96TAA,1175
|
|
5
5
|
matlab_proxy/default_configuration.py,sha256=tBHaEq_bYsX2uC9pPA9mi_8M6o94ij-rxq8mbvcYpFc,1874
|
|
6
6
|
matlab_proxy/devel.py,sha256=nR6XPVBUEdQ-RZGtYvX1YHTp8gj9cuw5Hp8ahasMBc8,14310
|
|
7
|
-
matlab_proxy/settings.py,sha256=
|
|
7
|
+
matlab_proxy/settings.py,sha256=ni-9zNbWF3cy4v0X8vAwAiM5WcC8U_t3a-s7GXIkyBA,26723
|
|
8
8
|
matlab_proxy/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
matlab_proxy/gui/asset-manifest.json,sha256=VW67P_Jg7RPTCNDkMCWmphyXKJvv-KE-DjFQvQxm8aM,3516
|
|
10
10
|
matlab_proxy/gui/favicon.ico,sha256=7w7Ki1uQP2Rgwc64dOV4-NrTu97I3WsZw8OvRSoY1A0,130876
|
|
@@ -54,7 +54,7 @@ matlab_proxy/gui/static/media/trigger-ok.7b9c238be42f685c4fa7.svg,sha256=mD-7N9c
|
|
|
54
54
|
matlab_proxy/icons/matlab.svg,sha256=xh5uYebQd8I-ISvenjU9A-PkClzW_lU9wvm3doXOFKM,13366
|
|
55
55
|
matlab_proxy/matlab/evaluateUserMatlabCode.m,sha256=R8w6nPdGtadR4UUFJaspcrGQL7cJwUItdrfc531w3bM,2420
|
|
56
56
|
matlab_proxy/matlab/startup.m,sha256=Rgb1Y3F2pFgByKAaXxWWcYOA2594D7V2HXuyuVmGYjs,653
|
|
57
|
-
matlab_proxy/util/__init__.py,sha256=
|
|
57
|
+
matlab_proxy/util/__init__.py,sha256=JkVIsTOae5giDK0cQ7jcxQSHa8zo1umdq-1C0grDZwk,11712
|
|
58
58
|
matlab_proxy/util/event_loop.py,sha256=sX_0tKlirCY5ImLxkss_XO4Ksj65u6JHtwMj25oGL94,1816
|
|
59
59
|
matlab_proxy/util/list_servers.py,sha256=M93coVZjyQCdIvCCxsNOU_XDWNjBSysOJ5tWXaTjP8Y,1369
|
|
60
60
|
matlab_proxy/util/mw.py,sha256=dLGSdfcTZiwKR1MMZA-39o-8na13IEPZOGBqcaHmKVI,11086
|
|
@@ -67,27 +67,27 @@ matlab_proxy/util/mwi/environment_variables.py,sha256=sOfmL8PjQONgkJdegdotLbsqHs
|
|
|
67
67
|
matlab_proxy/util/mwi/exceptions.py,sha256=3jklFU6br2_pSSsATCRDY3A5fTzk6ekJ4M69sunwxBk,5114
|
|
68
68
|
matlab_proxy/util/mwi/logger.py,sha256=EHHr6OWlXe6yVX0RPh57HSE7lz6MhWmwLQIpe_SlsC0,3803
|
|
69
69
|
matlab_proxy/util/mwi/token_auth.py,sha256=UbIWqo7qADaZdijFvorLYsZbxzaB8TycGP8nk305ru0,9997
|
|
70
|
-
matlab_proxy/util/mwi/validators.py,sha256=
|
|
70
|
+
matlab_proxy/util/mwi/validators.py,sha256=iXPR7b3jtnwx2i15YxKQgmOm2pt3ujJeMGYNxY0oGGw,12633
|
|
71
71
|
matlab_proxy/util/mwi/embedded_connector/__init__.py,sha256=Vfl2hNC7V1IwoK9_wrwfENs4BC8P-Mvvqh4BNGi2n48,119
|
|
72
72
|
matlab_proxy/util/mwi/embedded_connector/helpers.py,sha256=aOn-AvcDy6jBQJIffiv_agIa4UVldAIl3--QnDpXWDM,3656
|
|
73
73
|
matlab_proxy/util/mwi/embedded_connector/request.py,sha256=-IzTDjy3qViHfLJpK3OnFtEyV7dgwJKPQAfav9lqILc,4317
|
|
74
74
|
matlab_proxy_manager/__init__.py,sha256=CMqm2aSYUWo5sxV3vyqWudrQU31muouSqZRDesJNJSA,178
|
|
75
75
|
matlab_proxy_manager/lib/__init__.py,sha256=KfwQxxM5a1kMRtNbhz8tb7YfHp8e2d0tNLB55wYvDS8,37
|
|
76
|
-
matlab_proxy_manager/lib/api.py,sha256=
|
|
76
|
+
matlab_proxy_manager/lib/api.py,sha256=_ukL8ZeQg8E5s9uPeNjYAuCc_1BR08GSlom0VQclt84,12549
|
|
77
77
|
matlab_proxy_manager/storage/__init__.py,sha256=KfwQxxM5a1kMRtNbhz8tb7YfHp8e2d0tNLB55wYvDS8,37
|
|
78
78
|
matlab_proxy_manager/storage/file_repository.py,sha256=U4FAw0zFN9z7YNlaMsYZXWm5ccs3rp3bzZL-W2BNhxA,5187
|
|
79
79
|
matlab_proxy_manager/storage/interface.py,sha256=pnRRD0Ku3gzbruAOM3J3NI2Kk8do3-_yRw9Pag1IqnE,1883
|
|
80
|
-
matlab_proxy_manager/storage/server.py,sha256=
|
|
80
|
+
matlab_proxy_manager/storage/server.py,sha256=MjTw5-CRb3jF57wUXnVJmOyezM6peBOAyBfMfEqSzBc,4848
|
|
81
81
|
matlab_proxy_manager/utils/__init__.py,sha256=KfwQxxM5a1kMRtNbhz8tb7YfHp8e2d0tNLB55wYvDS8,37
|
|
82
82
|
matlab_proxy_manager/utils/auth.py,sha256=60vi16eQ7LWp3I4CNv2easTjObw50irEm518fiMA5YI,2526
|
|
83
83
|
matlab_proxy_manager/utils/constants.py,sha256=pyg-bkk6wWfmy60nvhroZDMZt__FcbZbuvU-b9m2Fkg,163
|
|
84
84
|
matlab_proxy_manager/utils/environment_variables.py,sha256=rbDeWnyJp77Yr6btK3eXKZQ5thwiwhOGZcvDetGPOH8,1436
|
|
85
|
-
matlab_proxy_manager/utils/helpers.py,sha256=
|
|
85
|
+
matlab_proxy_manager/utils/helpers.py,sha256=vgbWDTGmHpI_IMEy67UMfExxmbjMskhdEadCmbbvav8,9983
|
|
86
86
|
matlab_proxy_manager/utils/logger.py,sha256=GSRGD-yf518o-2b1BxEeJYuNiEz2eEqpl0Solqbwpb4,1869
|
|
87
87
|
matlab_proxy_manager/web/__init__.py,sha256=KfwQxxM5a1kMRtNbhz8tb7YfHp8e2d0tNLB55wYvDS8,37
|
|
88
|
-
matlab_proxy_manager/web/app.py,sha256=
|
|
89
|
-
matlab_proxy_manager/web/monitor.py,sha256=
|
|
90
|
-
matlab_proxy_manager/web/watcher.py,sha256=
|
|
88
|
+
matlab_proxy_manager/web/app.py,sha256=a_fCQS1rDGCuQ7iE1J2oaFrKSeBUyk_9lYeuyZo-MVc,18174
|
|
89
|
+
matlab_proxy_manager/web/monitor.py,sha256=PWkwV0kP3XHCxDRHpurPh74Zg-SgaIXnCnX2xZSW_R8,1541
|
|
90
|
+
matlab_proxy_manager/web/watcher.py,sha256=89JHjBAQtOrllstaJFxqrjHwckpRmu3qfUqeqPLmH2Q,2130
|
|
91
91
|
tests/integration/__init__.py,sha256=ttzJ8xKWGxOJZz56qOiWOn6sp5LGomkZMn_w4KJLRMU,42
|
|
92
92
|
tests/integration/integration_tests_with_license/__init__.py,sha256=vVYZCur-QhmIGCxUmn-WZjIywtDQidaLDmlmrRHRlgY,37
|
|
93
93
|
tests/integration/integration_tests_with_license/conftest.py,sha256=sCaIXB8d4vf05C7JWSVA7g5gnPjbpRq3dftuBpWyp1s,1599
|
|
@@ -101,7 +101,7 @@ tests/integration/utils/licensing.py,sha256=rEBjvMXO8R3mL6KnePu2lojmOsjD4GXl9frf
|
|
|
101
101
|
tests/unit/__init__.py,sha256=KfwQxxM5a1kMRtNbhz8tb7YfHp8e2d0tNLB55wYvDS8,37
|
|
102
102
|
tests/unit/conftest.py,sha256=Hfxq3h8IZuLJkRMh5jdEFiq78CIAdKvm-6KryRDZ0FY,1918
|
|
103
103
|
tests/unit/test_app.py,sha256=pM8zVyXWLx8nP8RePusXH4admEBYKc5JC8eE2t8xIQE,38487
|
|
104
|
-
tests/unit/test_app_state.py,sha256=
|
|
104
|
+
tests/unit/test_app_state.py,sha256=mq0qDWY3k2-_8job9g3BY52aT3ciNxji5_HVG1VhVBw,37112
|
|
105
105
|
tests/unit/test_constants.py,sha256=2nXxTmDP8utr8krsfZ4c_Bh4_mWPcDO5uI8MXeq4Usg,158
|
|
106
106
|
tests/unit/test_ddux.py,sha256=a2J2iM8j_nnfJVuMI38p5AjwrRdoMj3N88gFgS2I4hg,713
|
|
107
107
|
tests/unit/test_devel.py,sha256=A-1iVhSSwmywaW65QIRcUS2Fk7nJxceCcCm7CJtNdEc,7982
|
|
@@ -121,9 +121,9 @@ tests/unit/util/mwi/embedded_connector/test_helpers.py,sha256=vYTWNUTuDeaygo16JG
|
|
|
121
121
|
tests/unit/util/mwi/embedded_connector/test_request.py,sha256=PR-jddnXDEiip-lD7A_QSvRwEkwo3eQ8owZlk-r9vnk,1867
|
|
122
122
|
tests/utils/__init__.py,sha256=ttzJ8xKWGxOJZz56qOiWOn6sp5LGomkZMn_w4KJLRMU,42
|
|
123
123
|
tests/utils/logging_util.py,sha256=VBy_NRvwau3C_CVTBjK5RMROrQimnJYHO2U0aKSZiRw,2234
|
|
124
|
-
matlab_proxy-0.23.
|
|
125
|
-
matlab_proxy-0.23.
|
|
126
|
-
matlab_proxy-0.23.
|
|
127
|
-
matlab_proxy-0.23.
|
|
128
|
-
matlab_proxy-0.23.
|
|
129
|
-
matlab_proxy-0.23.
|
|
124
|
+
matlab_proxy-0.23.4.dist-info/LICENSE.md,sha256=oF0h3UdSF-rlUiMGYwi086ZHqelzz7yOOk9HFDv9ELo,2344
|
|
125
|
+
matlab_proxy-0.23.4.dist-info/METADATA,sha256=KBooTQm1Jo4GgGtZsuGm2l5JoIR937BA5MQ6VpAHBXE,10199
|
|
126
|
+
matlab_proxy-0.23.4.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
127
|
+
matlab_proxy-0.23.4.dist-info/entry_points.txt,sha256=ZAlCUsgKzGcAeQaMZOq31FrTB5tQ8Ypq8Op_8U600-A,305
|
|
128
|
+
matlab_proxy-0.23.4.dist-info/top_level.txt,sha256=KF-347aoRGsfHTpiSqfIPUZ95bzK5-oMIu8S_TUcu-w,40
|
|
129
|
+
matlab_proxy-0.23.4.dist-info/RECORD,,
|
matlab_proxy_manager/lib/api.py
CHANGED
|
@@ -3,7 +3,7 @@ import asyncio
|
|
|
3
3
|
import os
|
|
4
4
|
import secrets
|
|
5
5
|
import subprocess
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
import matlab_proxy
|
|
9
9
|
import matlab_proxy.util.system as mwi_sys
|
|
@@ -20,7 +20,7 @@ log = logger.get(init=True)
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
async def start_matlab_proxy_for_kernel(
|
|
23
|
-
caller_id: str, parent_id: str,
|
|
23
|
+
caller_id: str, parent_id: str, is_shared_matlab: bool
|
|
24
24
|
):
|
|
25
25
|
"""
|
|
26
26
|
Starts a MATLAB proxy server specifically for MATLAB Kernel.
|
|
@@ -29,12 +29,12 @@ async def start_matlab_proxy_for_kernel(
|
|
|
29
29
|
set to None, for starting the MATLAB proxy server via proxy manager.
|
|
30
30
|
"""
|
|
31
31
|
return await _start_matlab_proxy(
|
|
32
|
-
caller_id=caller_id, ctx=parent_id,
|
|
32
|
+
caller_id=caller_id, ctx=parent_id, is_shared_matlab=is_shared_matlab
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
async def start_matlab_proxy_for_jsp(
|
|
37
|
-
parent_id: str,
|
|
37
|
+
parent_id: str, is_shared_matlab: bool, mpm_auth_token: str
|
|
38
38
|
):
|
|
39
39
|
"""
|
|
40
40
|
Starts a MATLAB proxy server specifically for Jupyter Server Proxy (JSP) - Open MATLAB launcher.
|
|
@@ -45,7 +45,7 @@ async def start_matlab_proxy_for_jsp(
|
|
|
45
45
|
return await _start_matlab_proxy(
|
|
46
46
|
caller_id="jsp",
|
|
47
47
|
ctx=parent_id,
|
|
48
|
-
|
|
48
|
+
is_shared_matlab=is_shared_matlab,
|
|
49
49
|
mpm_auth_token=mpm_auth_token,
|
|
50
50
|
)
|
|
51
51
|
|
|
@@ -60,7 +60,7 @@ async def _start_matlab_proxy(**options) -> Optional[dict]:
|
|
|
60
60
|
Args (keyword arguments):
|
|
61
61
|
- caller_id (str): The identifier for the caller (kernel id for kernels, "jsp" for JSP).
|
|
62
62
|
- ctx (str): The context in which the server is being started (parent pid).
|
|
63
|
-
-
|
|
63
|
+
- is_shared_matlab (bool, optional): Whether to start a shared MATLAB proxy instance.
|
|
64
64
|
Defaults to False.
|
|
65
65
|
- mpm_auth_token (str, optional): The MATLAB proxy manager token. If not provided,
|
|
66
66
|
a new token is generated. Defaults to None.
|
|
@@ -69,16 +69,23 @@ async def _start_matlab_proxy(**options) -> Optional[dict]:
|
|
|
69
69
|
ServerProcess: The process representing the MATLAB proxy server.
|
|
70
70
|
|
|
71
71
|
Raises:
|
|
72
|
-
ValueError: If `caller_id` is "default" and `
|
|
72
|
+
ValueError: If `caller_id` is "default" and `is_shared_matlab` is False.
|
|
73
73
|
"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
# Validate arguments
|
|
75
|
+
required_args: List[str] = ["caller_id", "ctx", "is_shared_matlab"]
|
|
76
|
+
missing_args: List[str] = [arg for arg in required_args if arg not in options]
|
|
77
|
+
|
|
78
|
+
if missing_args:
|
|
79
|
+
raise ValueError(f"Missing required arguments: {', '.join(missing_args)}")
|
|
80
|
+
|
|
81
|
+
caller_id: str = options["caller_id"]
|
|
82
|
+
ctx: str = options["ctx"]
|
|
83
|
+
is_shared_matlab: bool = options.get("is_shared_matlab", True)
|
|
77
84
|
mpm_auth_token: Optional[str] = options.get("mpm_auth_token", None)
|
|
78
85
|
|
|
79
|
-
if
|
|
86
|
+
if not is_shared_matlab and caller_id == "default":
|
|
80
87
|
raise ValueError(
|
|
81
|
-
"Caller id cannot be default when
|
|
88
|
+
"Caller id cannot be default when matlab proxy is not shareable"
|
|
82
89
|
)
|
|
83
90
|
|
|
84
91
|
mpm_auth_token = mpm_auth_token or secrets.token_hex(32)
|
|
@@ -86,9 +93,9 @@ async def _start_matlab_proxy(**options) -> Optional[dict]:
|
|
|
86
93
|
# Cleanup stale entries before starting new instance of matlab proxy server
|
|
87
94
|
helpers._are_orphaned_servers_deleted(ctx)
|
|
88
95
|
|
|
89
|
-
ident = caller_id if
|
|
96
|
+
ident = caller_id if not is_shared_matlab else "default"
|
|
90
97
|
key = f"{ctx}_{ident}"
|
|
91
|
-
log.debug("Starting matlab proxy using %s, %s, %s", ctx, ident,
|
|
98
|
+
log.debug("Starting matlab proxy using %s, %s, %s", ctx, ident, is_shared_matlab)
|
|
92
99
|
|
|
93
100
|
data_dir = helpers.create_and_get_proxy_manager_data_dir()
|
|
94
101
|
server_process = ServerProcess.find_existing_server(data_dir, key)
|
|
@@ -101,10 +108,8 @@ async def _start_matlab_proxy(**options) -> Optional[dict]:
|
|
|
101
108
|
|
|
102
109
|
# Create a new matlab proxy server
|
|
103
110
|
else:
|
|
104
|
-
server_process
|
|
105
|
-
|
|
106
|
-
ident, ctx, key, is_isolated_matlab, mpm_auth_token
|
|
107
|
-
)
|
|
111
|
+
server_process = await _start_subprocess_and_check_for_readiness(
|
|
112
|
+
ident, ctx, key, is_shared_matlab, mpm_auth_token
|
|
108
113
|
)
|
|
109
114
|
|
|
110
115
|
# Store the newly created server into filesystem
|
|
@@ -115,7 +120,7 @@ async def _start_matlab_proxy(**options) -> Optional[dict]:
|
|
|
115
120
|
|
|
116
121
|
|
|
117
122
|
async def _start_subprocess_and_check_for_readiness(
|
|
118
|
-
server_id: str, ctx: str, key: str,
|
|
123
|
+
server_id: str, ctx: str, key: str, is_shared_matlab: bool, mpm_auth_token: str
|
|
119
124
|
) -> Optional[ServerProcess]:
|
|
120
125
|
"""
|
|
121
126
|
Starts a MATLAB proxy server.
|
|
@@ -136,10 +141,12 @@ async def _start_subprocess_and_check_for_readiness(
|
|
|
136
141
|
matlab_proxy_cmd, matlab_proxy_env = _prepare_cmd_and_env_for_matlab_proxy()
|
|
137
142
|
|
|
138
143
|
# Start the matlab proxy process
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
result = await _start_subprocess(matlab_proxy_cmd, matlab_proxy_env, server_id)
|
|
145
|
+
if not result:
|
|
146
|
+
log.error("Could not start matlab proxy")
|
|
147
|
+
return None
|
|
142
148
|
|
|
149
|
+
process_id, url, mwi_base_url = result
|
|
143
150
|
server_process = None
|
|
144
151
|
|
|
145
152
|
# Check for the matlab proxy server readiness
|
|
@@ -151,14 +158,14 @@ async def _start_subprocess_and_check_for_readiness(
|
|
|
151
158
|
headers=helpers.convert_mwi_env_vars_to_header_format(
|
|
152
159
|
matlab_proxy_env, "MWI"
|
|
153
160
|
),
|
|
154
|
-
pid=process_id,
|
|
161
|
+
pid=str(process_id),
|
|
155
162
|
parent_pid=ctx,
|
|
156
163
|
id=key,
|
|
157
|
-
type="
|
|
164
|
+
type="shared" if is_shared_matlab else "named",
|
|
158
165
|
mpm_auth_token=mpm_auth_token,
|
|
159
166
|
)
|
|
160
167
|
else:
|
|
161
|
-
log.error("
|
|
168
|
+
log.error("matlab-proxy server never became ready")
|
|
162
169
|
|
|
163
170
|
return server_process
|
|
164
171
|
|
|
@@ -189,7 +196,7 @@ def _prepare_cmd_and_env_for_matlab_proxy():
|
|
|
189
196
|
return matlab_proxy_cmd, matlab_proxy_env
|
|
190
197
|
|
|
191
198
|
|
|
192
|
-
async def _start_subprocess(cmd, env, server_id) -> Optional[int]:
|
|
199
|
+
async def _start_subprocess(cmd, env, server_id) -> Optional[Tuple[int, str, str]]:
|
|
193
200
|
"""
|
|
194
201
|
Initializes and starts a subprocess using the specified command and provided environment.
|
|
195
202
|
|
|
@@ -199,33 +206,22 @@ async def _start_subprocess(cmd, env, server_id) -> Optional[int]:
|
|
|
199
206
|
process = None
|
|
200
207
|
mwi_base_url: str = f"{constants.MWI_BASE_URL_PREFIX}{server_id}"
|
|
201
208
|
|
|
202
|
-
# Get a free port
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
# Get a free port and corresponding bound socket
|
|
210
|
+
with helpers.find_free_port() as (port, _):
|
|
211
|
+
env.update(
|
|
212
|
+
{
|
|
213
|
+
"MWI_APP_PORT": port,
|
|
214
|
+
"MWI_BASE_URL": mwi_base_url,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
210
217
|
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
# Using loopback address so that DNS resolution doesn't add latency in Windows
|
|
219
|
+
url: str = f"http://127.0.0.1:{port}"
|
|
213
220
|
|
|
214
|
-
|
|
215
|
-
process = await asyncio.create_subprocess_exec(
|
|
216
|
-
*cmd,
|
|
217
|
-
env=env,
|
|
218
|
-
)
|
|
219
|
-
log.debug("Started matlab proxy subprocess for posix")
|
|
220
|
-
else:
|
|
221
|
-
process = subprocess.Popen(
|
|
222
|
-
cmd,
|
|
223
|
-
env=env,
|
|
224
|
-
)
|
|
225
|
-
log.debug("Started matlab proxy subprocess for windows")
|
|
221
|
+
process = await _initialize_process_based_on_os_type(cmd, env)
|
|
226
222
|
|
|
227
223
|
if not process:
|
|
228
|
-
log.error("Matlab proxy process not created
|
|
224
|
+
log.error("Matlab proxy process not created due to some error")
|
|
229
225
|
return None
|
|
230
226
|
|
|
231
227
|
process_pid = process.pid
|
|
@@ -233,6 +229,51 @@ async def _start_subprocess(cmd, env, server_id) -> Optional[int]:
|
|
|
233
229
|
return process_pid, url, mwi_base_url
|
|
234
230
|
|
|
235
231
|
|
|
232
|
+
async def _initialize_process_based_on_os_type(cmd, env):
|
|
233
|
+
"""
|
|
234
|
+
Initializes and starts a subprocess based on the operating system.
|
|
235
|
+
|
|
236
|
+
This function attempts to create a subprocess using the provided command and
|
|
237
|
+
environment variables. It handles both POSIX and Windows systems differently.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
cmd (List[str]): The command to execute in the subprocess.
|
|
241
|
+
env (Dict[str, str]): The environment variables for the subprocess.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Union[Process, None, Popen[bytes]]: The created subprocess object if successful,
|
|
245
|
+
or None if an error occurs during subprocess creation.
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
Exception: If there's an error creating the subprocess (caught and logged).
|
|
249
|
+
"""
|
|
250
|
+
if mwi_sys.is_posix():
|
|
251
|
+
log.debug("Starting matlab proxy subprocess for posix")
|
|
252
|
+
try:
|
|
253
|
+
return await asyncio.create_subprocess_exec(
|
|
254
|
+
*cmd,
|
|
255
|
+
env=env,
|
|
256
|
+
# kernel sporadically ends up cleaning the child matlab-proxy process during the
|
|
257
|
+
# restart workflow. This is a workaround to handle that race condition which leads
|
|
258
|
+
# to starting matlab-proxy in a new process group and is not counted for deletion.
|
|
259
|
+
# https://github.com/ipython/ipykernel/blob/main/ipykernel/kernelbase.py#L1283
|
|
260
|
+
start_new_session=True,
|
|
261
|
+
)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
log.error("Failed to create posix subprocess: %s", e)
|
|
264
|
+
return None
|
|
265
|
+
else:
|
|
266
|
+
try:
|
|
267
|
+
log.debug("Starting matlab proxy subprocess for windows")
|
|
268
|
+
return subprocess.Popen(
|
|
269
|
+
cmd,
|
|
270
|
+
env=env,
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
log.error("Failed to create windows subprocess: %s", e)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
236
277
|
async def shutdown(parent_pid: str, caller_id: str, mpm_auth_token: str):
|
|
237
278
|
"""
|
|
238
279
|
Shutdown the MATLAB proxy server if the provided authentication token is valid.
|
|
@@ -20,8 +20,8 @@ class ServerProcess:
|
|
|
20
20
|
mwi_base_url: Optional[str] = None
|
|
21
21
|
headers: Optional[dict] = None
|
|
22
22
|
errors: Optional[list] = None
|
|
23
|
-
pid: Optional[
|
|
24
|
-
parent_pid: Optional[
|
|
23
|
+
pid: Optional[str] = None
|
|
24
|
+
parent_pid: Optional[str] = None
|
|
25
25
|
absolute_url: Optional[str] = field(default=None)
|
|
26
26
|
id: Optional[str] = None
|
|
27
27
|
type: Optional[str] = None
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Copyright 2024 The MathWorks, Inc.
|
|
2
|
+
import http
|
|
2
3
|
import os
|
|
3
4
|
import socket
|
|
4
5
|
import time
|
|
6
|
+
from contextlib import contextmanager
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Dict
|
|
8
|
+
from typing import Dict, Generator, Optional, Tuple
|
|
9
|
+
from urllib.parse import urlparse
|
|
7
10
|
|
|
8
11
|
import psutil
|
|
9
12
|
import requests
|
|
@@ -18,7 +21,7 @@ from matlab_proxy_manager.utils import logger
|
|
|
18
21
|
log = logger.get()
|
|
19
22
|
|
|
20
23
|
|
|
21
|
-
def is_server_ready(url: str, retries: int = 2, backoff_factor=None) -> bool:
|
|
24
|
+
def is_server_ready(url: Optional[str], retries: int = 2, backoff_factor=None) -> bool:
|
|
22
25
|
"""
|
|
23
26
|
Check if the server at the given URL is ready.
|
|
24
27
|
|
|
@@ -29,13 +32,19 @@ def is_server_ready(url: str, retries: int = 2, backoff_factor=None) -> bool:
|
|
|
29
32
|
bool: True if the server is ready, False otherwise.
|
|
30
33
|
"""
|
|
31
34
|
try:
|
|
35
|
+
# Validate URL
|
|
36
|
+
parsed_url = urlparse(url)
|
|
37
|
+
if not all([parsed_url.scheme, parsed_url.netloc]):
|
|
38
|
+
log.debug("Invalid URL provided: %s", url)
|
|
39
|
+
return False
|
|
40
|
+
|
|
32
41
|
matlab_proxy_index_page_identifier = "MWI_MATLAB_PROXY_IDENTIFIER"
|
|
33
42
|
resp = requests_retry_session(
|
|
34
43
|
retries=retries, backoff_factor=backoff_factor
|
|
35
44
|
).get(f"{url}", verify=False)
|
|
36
45
|
log.debug("Response status code from server readiness: %s", resp.status_code)
|
|
37
46
|
return (
|
|
38
|
-
resp.status_code ==
|
|
47
|
+
resp.status_code == http.HTTPStatus.OK
|
|
39
48
|
and matlab_proxy_index_page_identifier in resp.text
|
|
40
49
|
)
|
|
41
50
|
except Exception as e:
|
|
@@ -71,16 +80,14 @@ def requests_retry_session(
|
|
|
71
80
|
return session
|
|
72
81
|
|
|
73
82
|
|
|
74
|
-
def does_process_exist(pid:
|
|
83
|
+
def does_process_exist(pid: Optional[str]) -> bool:
|
|
75
84
|
"""
|
|
76
85
|
Checks if the parent process is alive.
|
|
77
86
|
|
|
78
87
|
Returns:
|
|
79
88
|
bool: True if the parent process is alive, False otherwise.
|
|
80
89
|
"""
|
|
81
|
-
|
|
82
|
-
log.debug("Parent liveness check returned: %s", parent_status)
|
|
83
|
-
return parent_status
|
|
90
|
+
return bool(pid and psutil.pid_exists(int(pid)))
|
|
84
91
|
|
|
85
92
|
|
|
86
93
|
def convert_mwi_env_vars_to_header_format(
|
|
@@ -124,11 +131,11 @@ async def delete_dangling_servers(app: web.Application) -> None:
|
|
|
124
131
|
Args:
|
|
125
132
|
app (web.Application): aiohttp web application
|
|
126
133
|
"""
|
|
127
|
-
is_delete_successful = _are_orphaned_servers_deleted(
|
|
134
|
+
is_delete_successful = _are_orphaned_servers_deleted()
|
|
128
135
|
log.debug("Deleted dangling matlab proxy servers: %s", is_delete_successful)
|
|
129
136
|
|
|
130
137
|
|
|
131
|
-
def _are_orphaned_servers_deleted(predicate: str) -> bool:
|
|
138
|
+
def _are_orphaned_servers_deleted(predicate: Optional[str] = "") -> bool:
|
|
132
139
|
"""
|
|
133
140
|
Get all the files under the proxy manager directory, check the status of the servers,
|
|
134
141
|
and delete orphaned servers and their corresponding files.
|
|
@@ -146,7 +153,7 @@ def _are_orphaned_servers_deleted(predicate: str) -> bool:
|
|
|
146
153
|
servers: dict = storage.get_all()
|
|
147
154
|
|
|
148
155
|
def _matches_predicate(filename: str) -> bool:
|
|
149
|
-
return filename.split("_")[0] == predicate
|
|
156
|
+
return filename.split("_")[0] == str(predicate)
|
|
150
157
|
|
|
151
158
|
# Checks only a subset of servers (that matches the parent_pid of the caller)
|
|
152
159
|
# to reduce the MATLAB proxy startup time
|
|
@@ -154,7 +161,7 @@ def _are_orphaned_servers_deleted(predicate: str) -> bool:
|
|
|
154
161
|
servers = {
|
|
155
162
|
filename: server
|
|
156
163
|
for filename, server in servers.items()
|
|
157
|
-
if _matches_predicate(filename)
|
|
164
|
+
if _matches_predicate(Path(filename).stem)
|
|
158
165
|
}
|
|
159
166
|
if not servers:
|
|
160
167
|
log.debug("Parent pid not matched, nothing to cleanup")
|
|
@@ -163,7 +170,7 @@ def _are_orphaned_servers_deleted(predicate: str) -> bool:
|
|
|
163
170
|
return _delete_server_and_file(storage, servers)
|
|
164
171
|
|
|
165
172
|
|
|
166
|
-
def _delete_server_and_file(storage, servers):
|
|
173
|
+
def _delete_server_and_file(storage, servers) -> bool:
|
|
167
174
|
is_server_deleted = False
|
|
168
175
|
for filename, server in servers.items():
|
|
169
176
|
if not server.is_server_alive():
|
|
@@ -201,7 +208,7 @@ def poll_for_server_deletion() -> None:
|
|
|
201
208
|
start_time = time.time()
|
|
202
209
|
|
|
203
210
|
while time.time() - start_time < timeout_in_seconds:
|
|
204
|
-
is_server_deleted = _are_orphaned_servers_deleted(
|
|
211
|
+
is_server_deleted = _are_orphaned_servers_deleted()
|
|
205
212
|
if is_server_deleted:
|
|
206
213
|
log.debug("Servers deleted, breaking out of loop")
|
|
207
214
|
break
|
|
@@ -210,21 +217,31 @@ def poll_for_server_deletion() -> None:
|
|
|
210
217
|
time.sleep(0.5)
|
|
211
218
|
|
|
212
219
|
|
|
213
|
-
|
|
220
|
+
@contextmanager
|
|
221
|
+
def find_free_port() -> Generator[Tuple[str, socket.socket], None, None]:
|
|
214
222
|
"""
|
|
215
|
-
|
|
223
|
+
Context manager for finding a free port on the system.
|
|
216
224
|
|
|
217
|
-
This function creates a socket, binds it to an available port,
|
|
218
|
-
the port number
|
|
225
|
+
This function creates a socket, binds it to an available port, and yields
|
|
226
|
+
the port number along with the socket object. The socket is automatically
|
|
227
|
+
closed when exiting the context.
|
|
219
228
|
|
|
220
|
-
|
|
221
|
-
str:
|
|
229
|
+
Yields:
|
|
230
|
+
Tuple[str, socket.socket]: A tuple containing:
|
|
231
|
+
- str: The free port number as a string.
|
|
232
|
+
- socket.socket: The socket object.
|
|
222
233
|
"""
|
|
223
234
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
224
235
|
s.bind(("", 0))
|
|
225
236
|
port = str(s.getsockname()[1])
|
|
226
|
-
|
|
227
|
-
|
|
237
|
+
try:
|
|
238
|
+
yield port, s
|
|
239
|
+
finally:
|
|
240
|
+
try:
|
|
241
|
+
s.close()
|
|
242
|
+
except OSError as ex:
|
|
243
|
+
# Socket already closed, log and ignore the exception
|
|
244
|
+
log.debug("Failed to close socket: %s", ex)
|
|
228
245
|
|
|
229
246
|
|
|
230
247
|
def pre_load_from_state_file(data_dir: str) -> Dict[str, str]:
|
matlab_proxy_manager/web/app.py
CHANGED
|
@@ -14,17 +14,18 @@ from aiohttp import ClientSession, client_exceptions, web
|
|
|
14
14
|
import matlab_proxy.util.mwi.environment_variables as mwi_env
|
|
15
15
|
import matlab_proxy.util.system as mwi_sys
|
|
16
16
|
import matlab_proxy_manager.lib.api as mpm_lib
|
|
17
|
-
from matlab_proxy.util.event_loop import get_event_loop
|
|
18
17
|
from matlab_proxy_manager.utils import constants, helpers, logger
|
|
19
18
|
from matlab_proxy_manager.utils import environment_variables as mpm_env
|
|
20
19
|
from matlab_proxy_manager.utils.auth import authenticate_access_decorator
|
|
21
20
|
from matlab_proxy_manager.web import watcher
|
|
22
21
|
from matlab_proxy_manager.web.monitor import OrphanedProcessMonitor
|
|
23
22
|
|
|
24
|
-
#
|
|
25
|
-
|
|
23
|
+
# List of public-facing APIs exported by this module.
|
|
24
|
+
# This list contains the names of functions or classes that are intended to be
|
|
25
|
+
# used by external code importing this module. Only items listed here will be
|
|
26
|
+
# directly accessible when using "from module import *".
|
|
27
|
+
__all__ = ["proxy"]
|
|
26
28
|
|
|
27
|
-
SHUTDOWN_EVENT = None
|
|
28
29
|
log = logger.get(init=True)
|
|
29
30
|
|
|
30
31
|
|
|
@@ -40,6 +41,8 @@ def init_app() -> web.Application:
|
|
|
40
41
|
web.Application: The configured aiohttp web application.
|
|
41
42
|
"""
|
|
42
43
|
app = web.Application()
|
|
44
|
+
# Async event is utilized to signal app termination from this and other modules
|
|
45
|
+
app["shutdown_event"] = asyncio.Event()
|
|
43
46
|
|
|
44
47
|
# Create and get the proxy manager data directory
|
|
45
48
|
try:
|
|
@@ -53,7 +56,7 @@ def init_app() -> web.Application:
|
|
|
53
56
|
|
|
54
57
|
async def start_idle_monitor(app):
|
|
55
58
|
"""Start the idle timeout monitor."""
|
|
56
|
-
asyncio.create_task(monitor.start())
|
|
59
|
+
app["monitor_task"] = asyncio.create_task(monitor.start())
|
|
57
60
|
|
|
58
61
|
async def create_client_session(app):
|
|
59
62
|
"""Create an aiohttp client session."""
|
|
@@ -65,10 +68,34 @@ def init_app() -> web.Application:
|
|
|
65
68
|
"""Cleanup the aiohttp client session."""
|
|
66
69
|
await app["session"].close()
|
|
67
70
|
|
|
71
|
+
async def cleanup_monitor(app):
|
|
72
|
+
"""Cancel the idle timeout monitor task."""
|
|
73
|
+
if "monitor_task" in app:
|
|
74
|
+
app["monitor_task"].cancel()
|
|
75
|
+
try:
|
|
76
|
+
await app["monitor_task"]
|
|
77
|
+
except asyncio.CancelledError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
async def cleanup_watcher(app):
|
|
81
|
+
"""Cleanup the filesystem watcher."""
|
|
82
|
+
if "observer" in app:
|
|
83
|
+
loop = asyncio.get_running_loop()
|
|
84
|
+
await loop.run_in_executor(None, watcher.stop_watcher, app)
|
|
85
|
+
|
|
86
|
+
if "watcher_future" in app:
|
|
87
|
+
app["watcher_future"].cancel()
|
|
88
|
+
try:
|
|
89
|
+
await app["watcher_future"]
|
|
90
|
+
except asyncio.CancelledError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
68
93
|
app.on_startup.append(start_idle_monitor)
|
|
69
94
|
app.on_startup.append(create_client_session)
|
|
70
95
|
app.on_cleanup.append(helpers.delete_dangling_servers)
|
|
71
96
|
app.on_cleanup.append(cleanup_client_session)
|
|
97
|
+
app.on_cleanup.append(cleanup_monitor)
|
|
98
|
+
app.on_cleanup.append(cleanup_watcher)
|
|
72
99
|
|
|
73
100
|
app.router.add_route("*", "/{tail:.*}", proxy)
|
|
74
101
|
|
|
@@ -86,11 +113,6 @@ async def start_app(env_vars: namedtuple):
|
|
|
86
113
|
Raises:
|
|
87
114
|
Exception: If any error occurs during the application startup or runtime.
|
|
88
115
|
"""
|
|
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
116
|
app = init_app()
|
|
95
117
|
|
|
96
118
|
app["port"] = env_vars.mpm_port
|
|
@@ -110,47 +132,51 @@ async def start_app(env_vars: namedtuple):
|
|
|
110
132
|
log.debug("Proxy manager started at http://127.0.0.1:%d", site._port)
|
|
111
133
|
|
|
112
134
|
# Get the default event loop
|
|
113
|
-
loop =
|
|
135
|
+
loop = asyncio.get_running_loop()
|
|
114
136
|
|
|
115
|
-
# Run the observer in a separate thread
|
|
116
|
-
loop.run_in_executor(None, watcher.start_watcher, app)
|
|
137
|
+
# Run the observer in a separate thread and store the future
|
|
138
|
+
app["watcher_future"] = loop.run_in_executor(None, watcher.start_watcher, app)
|
|
117
139
|
|
|
118
140
|
# Register signal handler for graceful shutdown
|
|
119
|
-
_register_signal_handler(loop)
|
|
120
|
-
|
|
121
|
-
SHUTDOWN_EVENT = asyncio.Event()
|
|
141
|
+
_register_signal_handler(loop, app)
|
|
122
142
|
|
|
123
143
|
# Wait for receiving shutdown_event (set by interrupts or by monitoring process)
|
|
124
|
-
await
|
|
144
|
+
await app.get("shutdown_event").wait()
|
|
125
145
|
|
|
126
146
|
# After receiving the shutdown signal, perform cleanup by stopping the web server
|
|
127
147
|
await runner.cleanup()
|
|
128
148
|
|
|
129
149
|
|
|
130
|
-
def _register_signal_handler(loop):
|
|
150
|
+
def _register_signal_handler(loop, app):
|
|
131
151
|
"""
|
|
132
|
-
Registers signal handlers for
|
|
133
|
-
|
|
152
|
+
Registers signal handlers for graceful shutdown of the application.
|
|
153
|
+
|
|
154
|
+
This function sets up handlers for supported termination signals to allow
|
|
155
|
+
the application to shut down gracefully. It uses different methods for
|
|
156
|
+
POSIX and non-POSIX systems to add the signal handlers.
|
|
134
157
|
|
|
135
158
|
Args:
|
|
136
159
|
loop (asyncio.AbstractEventLoop): The event loop to which the signal handlers
|
|
137
|
-
|
|
160
|
+
should be added.
|
|
161
|
+
app (aiohttp.web.Application): The web application instance.
|
|
138
162
|
"""
|
|
139
163
|
signals = mwi_sys.get_supported_termination_signals()
|
|
140
164
|
for sig_name in signals:
|
|
141
165
|
if mwi_sys.is_posix():
|
|
142
|
-
loop.add_signal_handler(sig_name,
|
|
166
|
+
loop.add_signal_handler(sig_name, lambda: _catch_signals(app))
|
|
143
167
|
else:
|
|
144
168
|
# loop.add_signal_handler() is not yet supported in Windows.
|
|
145
169
|
# Using the 'signal' package instead.
|
|
146
|
-
signal
|
|
170
|
+
# signal module expects a handler function that takes two arguments:
|
|
171
|
+
# the signal number and the current stack frame
|
|
172
|
+
signal.signal(sig_name, lambda s, f: _catch_signals(app))
|
|
147
173
|
|
|
148
174
|
|
|
149
|
-
def
|
|
175
|
+
def _catch_signals(app):
|
|
150
176
|
"""Handle termination signals for graceful shutdown."""
|
|
151
177
|
# Poll for parent process to clean up to avoid race conditions in cleanup of matlab proxies
|
|
152
178
|
helpers.poll_for_server_deletion()
|
|
153
|
-
|
|
179
|
+
app.get("shutdown_event").set()
|
|
154
180
|
|
|
155
181
|
|
|
156
182
|
async def _start_default_proxy(app):
|
|
@@ -162,7 +188,7 @@ async def _start_default_proxy(app):
|
|
|
162
188
|
"""
|
|
163
189
|
server_process = await mpm_lib.start_matlab_proxy_for_jsp(
|
|
164
190
|
parent_id=app.get("parent_pid"),
|
|
165
|
-
|
|
191
|
+
is_shared_matlab=True,
|
|
166
192
|
mpm_auth_token=app.get("auth_token"),
|
|
167
193
|
)
|
|
168
194
|
if not server_process:
|
|
@@ -206,7 +232,7 @@ async def proxy(req):
|
|
|
206
232
|
|
|
207
233
|
# Set content length in case of modification
|
|
208
234
|
req_headers["Content-Length"] = str(len(req_body))
|
|
209
|
-
req_headers["
|
|
235
|
+
req_headers["X-Forwarded-Proto"] = "http"
|
|
210
236
|
req_path = req.rel_url
|
|
211
237
|
|
|
212
238
|
# Redirect block to move /*/matlab to /*/matlab/default/
|
|
@@ -230,7 +256,7 @@ async def proxy(req):
|
|
|
230
256
|
if not ctx:
|
|
231
257
|
log.debug("MPM Context header not found in the request")
|
|
232
258
|
return _render_error_page(
|
|
233
|
-
"Required header
|
|
259
|
+
f"Required header: ${constants.HEADER_MWI_MPM_CONTEXT} not found in the request"
|
|
234
260
|
)
|
|
235
261
|
|
|
236
262
|
client_key = f"{ctx}_{ident}"
|
|
@@ -246,9 +272,11 @@ async def proxy(req):
|
|
|
246
272
|
and req_headers.get(upgrade, "").lower() == "websocket"
|
|
247
273
|
and req.method == "GET"
|
|
248
274
|
):
|
|
249
|
-
return await
|
|
275
|
+
return await _forward_websocket_request(req, proxy_url)
|
|
250
276
|
try:
|
|
251
|
-
return await
|
|
277
|
+
return await _forward_http_request(
|
|
278
|
+
req, req_body, proxy_url, _collate_headers(req_headers, backend_server)
|
|
279
|
+
)
|
|
252
280
|
except web.HTTPFound:
|
|
253
281
|
log.debug("Redirection to path with /default")
|
|
254
282
|
raise
|
|
@@ -266,7 +294,23 @@ async def proxy(req):
|
|
|
266
294
|
raise web.HTTPNotFound() from err
|
|
267
295
|
|
|
268
296
|
|
|
269
|
-
|
|
297
|
+
# Helper private functions
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _collate_headers(req_headers: dict, backend_server: dict) -> dict:
|
|
301
|
+
"""Combines request headers with backend server (matlab-proxy) headers.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
req_headers (dict): The headers from the incoming request.
|
|
305
|
+
backend_server (dict): The backend server configuration.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
dict: A new dictionary containing all headers from both sources.
|
|
309
|
+
"""
|
|
310
|
+
return {**req_headers, **backend_server.get("headers")}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _forward_websocket_request(
|
|
270
314
|
req: web.Request, proxy_url: str
|
|
271
315
|
) -> web.WebSocketResponse:
|
|
272
316
|
"""Handles a websocket request to the backend matlab proxy server
|
|
@@ -352,11 +396,11 @@ async def _handle_websocket_request(
|
|
|
352
396
|
raise aiohttp.WebSocketError(code=code, message=message)
|
|
353
397
|
|
|
354
398
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
399
|
+
async def _forward_http_request(
|
|
400
|
+
req: web.Request,
|
|
401
|
+
req_body: Optional[bytes],
|
|
402
|
+
proxy_url: str,
|
|
403
|
+
headers: dict,
|
|
360
404
|
) -> web.Response:
|
|
361
405
|
"""
|
|
362
406
|
Forwards an incoming HTTP request to a specified backend server.
|
|
@@ -370,7 +414,7 @@ async def _handle_http_request(
|
|
|
370
414
|
proxy_url,
|
|
371
415
|
allow_redirects=True,
|
|
372
416
|
data=req_body,
|
|
373
|
-
headers=
|
|
417
|
+
headers=headers,
|
|
374
418
|
) as res:
|
|
375
419
|
headers = res.headers.copy()
|
|
376
420
|
body = await res.read()
|
|
@@ -410,16 +454,6 @@ def _render_error_page(error_msg: str) -> web.Response:
|
|
|
410
454
|
return web.HTTPServiceUnavailable(text=f"Error: {error_msg}")
|
|
411
455
|
|
|
412
456
|
|
|
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
457
|
def _fetch_and_validate_required_env_vars() -> namedtuple:
|
|
424
458
|
EnvVars = namedtuple("EnvVars", ["mpm_port", "mpm_auth_token", "mpm_parent_pid"])
|
|
425
459
|
|
|
@@ -428,7 +462,7 @@ def _fetch_and_validate_required_env_vars() -> namedtuple:
|
|
|
428
462
|
ctx = os.getenv(mpm_env.get_env_name_mwi_mpm_parent_pid())
|
|
429
463
|
|
|
430
464
|
if not ctx or not port or not mpm_auth_token:
|
|
431
|
-
|
|
465
|
+
log.error("Error: One or more required environment variables are missing.")
|
|
432
466
|
sys.exit(1)
|
|
433
467
|
|
|
434
468
|
try:
|
|
@@ -437,10 +471,19 @@ def _fetch_and_validate_required_env_vars() -> namedtuple:
|
|
|
437
471
|
mpm_port=mwi_mpm_port, mpm_auth_token=mpm_auth_token, mpm_parent_pid=ctx
|
|
438
472
|
)
|
|
439
473
|
except ValueError as ve:
|
|
440
|
-
|
|
474
|
+
log.error("Error: Invalid type for port: %s", ve)
|
|
441
475
|
sys.exit(1)
|
|
442
476
|
|
|
443
477
|
|
|
478
|
+
def main() -> None:
|
|
479
|
+
"""
|
|
480
|
+
The main entry point of the application. Starts the app and run until the shutdown
|
|
481
|
+
signal to terminate the app is received.
|
|
482
|
+
"""
|
|
483
|
+
env_vars: namedtuple = _fetch_and_validate_required_env_vars()
|
|
484
|
+
asyncio.run(start_app(env_vars))
|
|
485
|
+
|
|
486
|
+
|
|
444
487
|
if __name__ == "__main__":
|
|
445
488
|
# This ensures that the app is not created when the module is imported and
|
|
446
489
|
# is only started when the script is run directly or via executable invocation
|
|
@@ -36,10 +36,9 @@ class OrphanedProcessMonitor:
|
|
|
36
36
|
"""
|
|
37
37
|
Triggers the shutdown process by setting the shutdown event.
|
|
38
38
|
"""
|
|
39
|
-
from matlab_proxy_manager.web.app import SHUTDOWN_EVENT
|
|
40
39
|
|
|
41
40
|
try:
|
|
42
41
|
# Set the shutdown async event to signal app shutdown to the app runner
|
|
43
|
-
|
|
42
|
+
self.app.get("shutdown_event").set()
|
|
44
43
|
except Exception as ex:
|
|
45
44
|
log.debug("Unable to set proxy manager shutdown event, err: %s", ex)
|
|
@@ -51,4 +51,15 @@ def start_watcher(app: web.Application):
|
|
|
51
51
|
observer = Observer()
|
|
52
52
|
observer.schedule(event_handler, path_to_watch, recursive=True)
|
|
53
53
|
observer.start()
|
|
54
|
+
app["observer"] = observer
|
|
54
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()
|
tests/unit/test_app_state.py
CHANGED
|
@@ -364,9 +364,23 @@ def test_are_required_processes_ready(
|
|
|
364
364
|
assert actual == expected
|
|
365
365
|
|
|
366
366
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
# The test: test_track_embedded_connector has been split into:
|
|
368
|
+
# 1) test_track_embedded_connector_posix: Test to check if stop_matlab is called on posix systems.
|
|
369
|
+
# 2) test_track_embedded_connector : Test to check if stop_matlab is not called in windows.
|
|
370
|
+
|
|
371
|
+
# In windows, errors are shown as UI windows and calling stop_matlab() if MATLAB had not started in
|
|
372
|
+
# PROCESS_TIMEOUT seconds would remove the window thereby leaving the user without knowing why MATLAB
|
|
373
|
+
# failed to start.
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@pytest.mark.parametrize("platform", [("linux"), ("mac")])
|
|
377
|
+
async def test_track_embedded_connector_posix(
|
|
378
|
+
mocker_os_patching_fixture, app_state_fixture
|
|
379
|
+
):
|
|
380
|
+
"""Test to check track_embedded_connector task for posix platforms.
|
|
381
|
+
|
|
382
|
+
Checks if stop_matlab() has been called when the embedded connector doesn't respond
|
|
383
|
+
even after PROCESS_TIMEOUT seconds of starting MATLAB.
|
|
370
384
|
|
|
371
385
|
Args:
|
|
372
386
|
mocker_os_patching_fixture (mocker): Custom pytest fixture for mocking
|
|
@@ -374,7 +388,16 @@ async def test_track_embedded_connector(mocker_os_patching_fixture, app_state_fi
|
|
|
374
388
|
"""
|
|
375
389
|
|
|
376
390
|
# Arrange
|
|
377
|
-
#
|
|
391
|
+
# Patching embedded_connector_start_time to EPOCH+1 seconds and state to be "down".
|
|
392
|
+
|
|
393
|
+
# For this test, the embedded_connector_start_time can be patched to ant value 600(default PROCESS_TIMEOUT) seconds
|
|
394
|
+
# before the current time.
|
|
395
|
+
|
|
396
|
+
# To always ensure that the time difference between the embedded_connector_start_time
|
|
397
|
+
# and the current time is greater than PROCESS_TIMEOUT, the embedded_connector_start_time is patched to
|
|
398
|
+
# EPOCH + 1 seconds so that the time_diff = current_time - embedded_connector_start_time is greater
|
|
399
|
+
# than PROCESS_TIMEOUT always evaluates to True.
|
|
400
|
+
|
|
378
401
|
mocker_os_patching_fixture.patch.object(
|
|
379
402
|
app_state_fixture, "embedded_connector_start_time", new=float(1.0)
|
|
380
403
|
)
|
|
@@ -392,6 +415,58 @@ async def test_track_embedded_connector(mocker_os_patching_fixture, app_state_fi
|
|
|
392
415
|
spy.assert_called_once()
|
|
393
416
|
|
|
394
417
|
|
|
418
|
+
@pytest.mark.parametrize("platform", [("windows")])
|
|
419
|
+
async def test_track_embedded_connector(mocker_os_patching_fixture, app_state_fixture):
|
|
420
|
+
"""Test to check track_embedded_connector task on windows.
|
|
421
|
+
|
|
422
|
+
In windows, since errors are shown in native UI windows , calling stop_matlab() would remove them,
|
|
423
|
+
thereby not knowing the error with which MATLAB failed to start.
|
|
424
|
+
|
|
425
|
+
Hence, this test checks that stop_matlab() is not called.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
mocker_os_patching_fixture (mocker): Custom pytest fixture for mocking
|
|
429
|
+
app_state_fixture (AppState): Object of AppState class with defaults set
|
|
430
|
+
"""
|
|
431
|
+
# Arrange
|
|
432
|
+
# Patching embedded_connector_start_time to EPOCH+1 seconds and state to be "down".
|
|
433
|
+
|
|
434
|
+
# For this test, the embedded_connector_start_time can be patched to any value 600(default PROCESS_TIMEOUT) seconds
|
|
435
|
+
# before the current time.
|
|
436
|
+
|
|
437
|
+
# To always ensure that the time difference between the embedded_connector_start_time
|
|
438
|
+
# and the current time is greater than PROCESS_TIMEOUT, the embedded_connector_start_time is patched to
|
|
439
|
+
# EPOCH + 1 seconds so that the time_diff = current_time - embedded_connector_start_time is greater
|
|
440
|
+
# than PROCESS_TIMEOUT always evaluates to True.
|
|
441
|
+
|
|
442
|
+
mocker_os_patching_fixture.patch.object(
|
|
443
|
+
app_state_fixture, "embedded_connector_start_time", new=float(1.0)
|
|
444
|
+
)
|
|
445
|
+
mocker_os_patching_fixture.patch.object(
|
|
446
|
+
app_state_fixture, "embedded_connector_state", return_value="down"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
spy = mocker_os_patching_fixture.spy(app_state_fixture, "stop_matlab")
|
|
450
|
+
|
|
451
|
+
# Act
|
|
452
|
+
|
|
453
|
+
# Unlike the posix test (test_track_embedded_connector_posix) where the task track_embedded_connector_state()
|
|
454
|
+
# would exit automatically after stopping MATLAB, in windows, the task will never exit(until the user checks the error
|
|
455
|
+
# manually and clicks on "Stop MATLAB").
|
|
456
|
+
|
|
457
|
+
# So, the task is manually stopped by raising a timeout error(set to 3 seconds). This is a generous amount of
|
|
458
|
+
# time for the error to be set as a MatlabError in CI systems.
|
|
459
|
+
with pytest.raises(asyncio.TimeoutError):
|
|
460
|
+
await asyncio.wait_for(
|
|
461
|
+
app_state_fixture._AppState__track_embedded_connector_state(),
|
|
462
|
+
timeout=3, # timeout of 3 seconds to account for CI systems. This is to wait for the error to be set as MatlabError.
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Assert
|
|
466
|
+
spy.assert_not_called() # In windows, MATLAB process should not be stopped so that the UI error window is not closed.
|
|
467
|
+
assert isinstance(app_state_fixture.error, MatlabError)
|
|
468
|
+
|
|
469
|
+
|
|
395
470
|
@pytest.mark.parametrize(
|
|
396
471
|
"env_var_name, filter_prefix, is_filtered",
|
|
397
472
|
[("MWI_AUTH_TOKEN", "MWI_", None), ("MWIFOO_AUTH_TOKEN", "MWI_", "foo")],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|