matlab-proxy 0.23.1__py3-none-any.whl → 0.23.3__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 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 launched by matlab-proxy
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(launched by this matlab-proxy process) starts on.
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 (launched by this matlab-proxy process) will write logs to.
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 launching the Embedded Connector(say an issue with licensing),
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.info(f"Deleting any cached config files!")
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.info(f"Resetting cached config information...")
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 launching MATLAB directly
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"!!! Launching MATLAB without providing any additional licensing information. This requires MATLAB to have been activated on the machine from which its being launched !!!"
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.debug(f"Using NLM string to connect ... ")
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.info("Using cached NLM licensing to launch MATLAB")
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 launch MATLAB."
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.info("Using cached existing license to launch MATLAB")
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.info(f"Deleting:{session_file[1]}")
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 launching MATLAB by matlab-proxy.
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.info(
983
- f"Using the display number supplied by Xvfb process'{matlab_env['DISPLAY']}' for launching MATLAB"
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.info(
988
- f"Using the existing DISPLAY environment variable with value:{matlab_env['DISPLAY']} for launching MATLAB"
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.info(
992
- "No DISPLAY environment variable found. Launching MATLAB without it."
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 launching Xvfb process, capture error for logging
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 launching Xvfb process.
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 in a stuck state and cannot be launched.
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
- if len(self.logs["matlab"]) == 0:
1191
- await self.__force_stop_matlab(
1192
- user_visible_error, this_task
1193
- )
1194
- # Breaking out of the loop to end this task as matlab-proxy was unable to launch MATLAB successfully
1195
- # even after waiting for self.PROCESS_TIMEOUT
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 in a stuck state and cannot be launched.
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
- if len(self.logs["matlab"]) == 0:
1210
- await self.__force_stop_matlab(
1211
- user_visible_error, this_task
1212
- )
1213
- # Breaking out of the loop to end this task as matlab-proxy was unable to launch MATLAB successfully
1214
- # even after waiting for self.PROCESS_TIMEOUT
1215
- break
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.info(f"Deleting:{session_file_path}")
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()
@@ -1496,7 +1491,7 @@ class AppState:
1496
1491
  if system.is_posix():
1497
1492
  xvfb = self.processes["xvfb"]
1498
1493
  if xvfb is not None and xvfb.returncode is None:
1499
- logger.info(f"Terminating Xvfb (PID={xvfb.pid})")
1494
+ logger.debug(f"Terminating Xvfb (PID={xvfb.pid})")
1500
1495
  xvfb.terminate()
1501
1496
  waiters.append(xvfb.wait())
1502
1497
 
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.info(error_message)
106
+ logger.error(error_message)
107
107
  raise MatlabInstallError(error_message)
108
108
 
109
109
 
@@ -98,7 +98,7 @@ def prepare_site(app, runner):
98
98
  )
99
99
  break
100
100
  except:
101
- logger.info(f"Failed to launch the site on port {p}")
101
+ logger.error(f"Failed to launch the site on port {p}")
102
102
 
103
103
  return site
104
104
 
@@ -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.info(
329
+ logger.debug(
330
330
  f"MATLAB root path: {matlab_root} exists, continuing to verify its validity..."
331
331
  )
332
332
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: matlab-proxy
3
- Version: 0.23.1
3
+ Version: 0.23.3
4
4
  Summary: Python® package enables you to launch MATLAB® and access it from a web browser.
5
5
  Home-page: https://github.com/mathworks/matlab-proxy/
6
6
  Author: The MathWorks, Inc.
@@ -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=kzgcdzU7O5AgUWtL0pPDPQxRBxpQLW0ZSpEEkZKEvHU,71298
3
+ matlab_proxy/app_state.py,sha256=fGZjhY8ZupX54Q6gEbXjfrxR8svDBicnsO97vetjpo4,71015
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=63pyCUvT7WLBRUpUr3F4r4nL13pQ2Hn6-oZODAhuGLo,26722
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=CoH6sfgrQ1phhO1FXeyQwTjtl7PPgeZOCGNrkyGv-5o,11711
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=CQnwf2kIm-bApSYClasbJCIWP-2ZxBNGn0X4B1U-2_0,12632
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=IBAYvrTCA2TQ1pBzz8mkn1GQXUpIC1mUwaqVktvl4aE,10639
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=kQ4jtG2xqCa8c7zqDBFYxOhxMxwZwqTIITBxcJRDcvE,4848
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=8RlUe2WfhJ65YEVlhfU6ivpjpy4AAyA70vzmnJh0ypA,9267
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=Lu6sl3Mft98oL-Z1NwBdmmG1OvnwYEN31pU2bOdzFi0,16572
89
- matlab_proxy_manager/web/monitor.py,sha256=Gj0DxwX0c1PEAly5jWmuIGqNJYGDDjTkQIzbVXu4zCQ,1589
90
- matlab_proxy_manager/web/watcher.py,sha256=aNa_UDwvzaZrIdHyvWGX7dILH199uc6xVH4odrKU5-E,1817
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=ZtM79HAe5W6pVVWV-NICjDJBlmuxJ4NnZn-urGGtH-E,33429
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.1.dist-info/LICENSE.md,sha256=oF0h3UdSF-rlUiMGYwi086ZHqelzz7yOOk9HFDv9ELo,2344
125
- matlab_proxy-0.23.1.dist-info/METADATA,sha256=T9odF5XgnV7G2o8ETI3vvLvYCCGBu8WqSbF_b9vAWYY,10199
126
- matlab_proxy-0.23.1.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
127
- matlab_proxy-0.23.1.dist-info/entry_points.txt,sha256=ZAlCUsgKzGcAeQaMZOq31FrTB5tQ8Ypq8Op_8U600-A,305
128
- matlab_proxy-0.23.1.dist-info/top_level.txt,sha256=KF-347aoRGsfHTpiSqfIPUZ95bzK5-oMIu8S_TUcu-w,40
129
- matlab_proxy-0.23.1.dist-info/RECORD,,
124
+ matlab_proxy-0.23.3.dist-info/LICENSE.md,sha256=oF0h3UdSF-rlUiMGYwi086ZHqelzz7yOOk9HFDv9ELo,2344
125
+ matlab_proxy-0.23.3.dist-info/METADATA,sha256=l32qvymxSsGA0amtHFIGVvMQDMFMBs6758Qm2_YSq-A,10199
126
+ matlab_proxy-0.23.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
127
+ matlab_proxy-0.23.3.dist-info/entry_points.txt,sha256=ZAlCUsgKzGcAeQaMZOq31FrTB5tQ8Ypq8Op_8U600-A,305
128
+ matlab_proxy-0.23.3.dist-info/top_level.txt,sha256=KF-347aoRGsfHTpiSqfIPUZ95bzK5-oMIu8S_TUcu-w,40
129
+ matlab_proxy-0.23.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.44.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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, is_isolated_matlab: bool
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, is_isolated_matlab=is_isolated_matlab
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, is_isolated_matlab: bool, mpm_auth_token: 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
- is_isolated_matlab=is_isolated_matlab,
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
- - is_isolated_matlab (bool, optional): Whether to start an isolated MATLAB proxy instance.
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 `is_isolated_matlab` is True.
72
+ ValueError: If `caller_id` is "default" and `is_shared_matlab` is False.
73
73
  """
74
- caller_id: str = options.get("caller_id")
75
- ctx: str = options.get("ctx")
76
- is_isolated_matlab: bool = options.get("is_isolated_matlab", False)
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 is_isolated_matlab and caller_id == "default":
86
+ if not is_shared_matlab and caller_id == "default":
80
87
  raise ValueError(
81
- "Caller id cannot be default when isolated_matlab is set to true"
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 is_isolated_matlab else "default"
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, is_isolated_matlab)
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: ServerProcess | None = (
105
- await _start_subprocess_and_check_for_readiness(
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, isolated: bool, mpm_auth_token: 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
- process_id, url, mwi_base_url = await _start_subprocess(
140
- matlab_proxy_cmd, matlab_proxy_env, server_id
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="named" if isolated else "shared",
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("Could not start matlab proxy")
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, closer to starting the matlab proxy appx
203
- port: str = helpers.find_free_port()
204
- env.update(
205
- {
206
- "MWI_APP_PORT": port,
207
- "MWI_BASE_URL": mwi_base_url,
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
- # Using loopback address so that DNS resolution doesn't add latency in Windows
212
- url: str = f"http://127.0.0.1:{port}"
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
- if mwi_sys.is_posix():
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 %d", process.returncode)
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[int] = None
24
- parent_pid: Optional[int] = None
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 == requests.codes.OK
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: int) -> bool:
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
- parent_status = pid is not None and psutil.pid_exists(int(pid))
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(None)
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(None)
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
- def find_free_port() -> str:
220
+ @contextmanager
221
+ def find_free_port() -> Generator[Tuple[str, socket.socket], None, None]:
214
222
  """
215
- Find a free port on the system.
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, retrieves
218
- the port number, and then closes the socket.
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
- Returns:
221
- str: The free port number as a string.
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
- s.close()
227
- return port
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]:
@@ -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
- # We use __all__ to list down all the public-facing APIs exported by this module
25
- __all__ = ["proxy", "SHUTDOWN_EVENT"]
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 = get_event_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 SHUTDOWN_EVENT.wait()
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 supported termination signals to allow for graceful shutdown
133
- of the application.
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
- should be added.
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, catch_signals)
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.signal(sig_name, catch_signals)
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 catch_signals(*args):
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
- SHUTDOWN_EVENT.set()
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
- is_isolated_matlab=False,
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["x-forwarded-proto"] = "http"
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 (MWI-MPM-CONTEXT) not found in the request"
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 _handle_websocket_request(req, proxy_url)
275
+ return await _forward_websocket_request(req, proxy_url)
250
276
  try:
251
- return await _handle_http_request(req, req_body, proxy_url, backend_server)
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
- async def _handle_websocket_request(
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
- # 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
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=backend_server.get("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
- print("Error: One or more required environment variables are missing.")
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
- print("Error: Invalid type for port: ", ve)
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
- SHUTDOWN_EVENT.set()
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()
@@ -364,9 +364,23 @@ def test_are_required_processes_ready(
364
364
  assert actual == expected
365
365
 
366
366
 
367
- @pytest.mark.parametrize("platform", [("linux"), ("windows"), ("mac")])
368
- async def test_track_embedded_connector(mocker_os_patching_fixture, app_state_fixture):
369
- """Test to check track_embedded_connector task
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
- # patching embedded_connector_start_time to EPOCH+1 seconds and state to be "down"
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")],