matlab-proxy 0.5.3__py3-none-any.whl → 0.30.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. matlab_proxy/app.py +578 -205
  2. matlab_proxy/app_state.py +1061 -431
  3. matlab_proxy/constants.py +37 -0
  4. matlab_proxy/default_configuration.py +39 -4
  5. matlab_proxy/devel.py +18 -22
  6. matlab_proxy/gui/index.html +20 -1
  7. matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
  8. matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
  9. matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
  10. matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
  11. matlab_proxy/matlab/startup.m +3 -28
  12. matlab_proxy/settings.py +543 -112
  13. matlab_proxy/util/__init__.py +187 -59
  14. matlab_proxy/util/cookie_jar.py +72 -0
  15. matlab_proxy/util/event_loop.py +28 -10
  16. matlab_proxy/util/list_servers.py +71 -26
  17. matlab_proxy/util/mw.py +16 -15
  18. matlab_proxy/util/mwi/download.py +136 -0
  19. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  20. matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
  21. matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
  22. matlab_proxy/util/mwi/environment_variables.py +120 -27
  23. matlab_proxy/util/mwi/exceptions.py +63 -9
  24. matlab_proxy/util/mwi/logger.py +141 -27
  25. matlab_proxy/util/mwi/session_name.py +28 -0
  26. matlab_proxy/util/mwi/token_auth.py +264 -121
  27. matlab_proxy/util/mwi/validators.py +231 -88
  28. matlab_proxy/util/system.py +9 -0
  29. matlab_proxy/util/windows.py +32 -6
  30. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
  31. matlab_proxy-0.30.1.dist-info/RECORD +88 -0
  32. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
  33. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
  34. matlab_proxy_manager/README.md +85 -0
  35. matlab_proxy_manager/__init__.py +6 -0
  36. matlab_proxy_manager/lib/README.md +53 -0
  37. matlab_proxy_manager/lib/__init__.py +1 -0
  38. matlab_proxy_manager/lib/api.py +419 -0
  39. matlab_proxy_manager/storage/README.md +54 -0
  40. matlab_proxy_manager/storage/__init__.py +1 -0
  41. matlab_proxy_manager/storage/file_repository.py +144 -0
  42. matlab_proxy_manager/storage/interface.py +62 -0
  43. matlab_proxy_manager/storage/server.py +172 -0
  44. matlab_proxy_manager/utils/__init__.py +1 -0
  45. matlab_proxy_manager/utils/auth.py +77 -0
  46. matlab_proxy_manager/utils/constants.py +8 -0
  47. matlab_proxy_manager/utils/decorators.py +37 -0
  48. matlab_proxy_manager/utils/environment_variables.py +51 -0
  49. matlab_proxy_manager/utils/exceptions.py +45 -0
  50. matlab_proxy_manager/utils/helpers.py +314 -0
  51. matlab_proxy_manager/utils/logger.py +76 -0
  52. matlab_proxy_manager/web/README.md +37 -0
  53. matlab_proxy_manager/web/__init__.py +1 -0
  54. matlab_proxy_manager/web/app.py +536 -0
  55. matlab_proxy_manager/web/monitor.py +45 -0
  56. matlab_proxy_manager/web/watcher.py +65 -0
  57. matlab_proxy/gui/asset-manifest.json +0 -23
  58. matlab_proxy/gui/authorization.html +0 -115
  59. matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
  60. matlab_proxy/gui/navbar.css +0 -8
  61. matlab_proxy/gui/signin.css +0 -42
  62. matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
  63. matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
  64. matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
  65. matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
  66. matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
  67. matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
  68. matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
  69. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
  70. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
  71. matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
  72. matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
  73. matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
  74. matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
  75. matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
  76. matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
  77. matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
  78. matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
  79. matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
  80. matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
  81. matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
  82. matlab_proxy/gui/token.html +0 -123
  83. matlab_proxy-0.5.3.dist-info/RECORD +0 -84
  84. matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
  85. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
  86. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
  87. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
  88. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
  89. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
  90. /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
  91. /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
  92. /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
  93. /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
  94. /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
  95. /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
  96. /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
  97. /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
  98. /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
  99. /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
  100. /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
  101. /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
  102. /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
  103. /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
  104. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info/licenses}/LICENSE.md +0 -0
@@ -1,18 +1,31 @@
1
- # Copyright (c) 2020-2022 The MathWorks, Inc.
1
+ # Copyright 2020-2025 The MathWorks, Inc.
2
2
  import argparse
3
+ import inspect
3
4
  import os
4
5
  import socket
5
- import sys
6
+ import time
7
+
8
+ from pathlib import Path
6
9
 
7
10
  import matlab_proxy
8
11
  from matlab_proxy.util import mwi, system
9
12
  from matlab_proxy.util.event_loop import *
10
13
  from matlab_proxy.util.mwi import environment_variables as mwi_env
14
+ from matlab_proxy.util.mwi.exceptions import (
15
+ UIVisibleFatalError,
16
+ )
17
+
18
+ from matlab_proxy.util.mwi.exceptions import (
19
+ LockAcquisitionError,
20
+ )
11
21
 
12
22
  logger = mwi.logger.get()
13
23
 
24
+ # Global value to detect whether interrupt signal handler has been triggered or not.
25
+ interrupt_signal_caught = False
26
+
14
27
 
15
- def parse_cli_args():
28
+ def parse_main_cli_args():
16
29
  """Parses CLI arguments passed to the main() function.
17
30
 
18
31
  Returns:
@@ -26,9 +39,40 @@ def parse_cli_args():
26
39
  help="A json file which stores the config specific to the environment.",
27
40
  default=matlab_proxy.get_default_config_name(),
28
41
  )
42
+
43
+ parser.add_argument(
44
+ "-v",
45
+ "--version",
46
+ help="prints the version of matlab-proxy.",
47
+ action="store_true",
48
+ )
49
+
29
50
  args = parser.parse_args()
30
51
 
31
52
  parsed_args["config"] = args.config
53
+ parsed_args["version"] = args.version
54
+
55
+ return parsed_args
56
+
57
+
58
+ def parse_list_cli_args():
59
+ """Parses CLI arguments passed to the matlab-proxy-app-list-servers entrypoint.
60
+
61
+ Returns:
62
+ dict: Containing the parsed arguments
63
+ """
64
+ # Parse the --config flag provided to the console script executable.
65
+ parsed_args = {}
66
+ parser = argparse.ArgumentParser()
67
+ parser.add_argument(
68
+ "-q",
69
+ "--quiet",
70
+ help="Return the server list without any additional text.",
71
+ action="store_true",
72
+ )
73
+ args = parser.parse_args()
74
+
75
+ parsed_args["quiet"] = args.quiet
32
76
 
33
77
  return parsed_args
34
78
 
@@ -75,7 +119,7 @@ def prepare_site(app, runner):
75
119
  )
76
120
  break
77
121
  except:
78
- logger.info(f"Failed to launch the site on port {p}")
122
+ logger.error(f"Failed to launch the site on port {p}")
79
123
 
80
124
  return site
81
125
 
@@ -97,8 +141,18 @@ def add_signal_handlers(loop):
97
141
  Raises:
98
142
  SystemExit: Raises SystemExit which will stop execution of loop.run_forever() in app.main()
99
143
  """
100
- logger.debug("Interrupt Signal handler called with args:\n", *args)
101
- raise SystemExit
144
+ logger.debug("Interrupt Signal handler called")
145
+
146
+ # Only raise SystemExit when the handler is invoked for the first time.
147
+ # Ignore subsequent handler invocations of interrupt signals. This is
148
+ # required so that asyncio event loop gracefully cancels pending tasks
149
+ # and exits.
150
+ global interrupt_signal_caught
151
+ if interrupt_signal_caught is False:
152
+ interrupt_signal_caught = True
153
+ raise SystemExit
154
+
155
+ logger.debug("Interrupt is already being serviced.")
102
156
 
103
157
  for interrupt_signal in system.get_supported_termination_signals():
104
158
  logger.debug(f"Registering handler for signal: {interrupt_signal} ")
@@ -115,48 +169,7 @@ def add_signal_handlers(loop):
115
169
  return loop
116
170
 
117
171
 
118
- def prettify(boundary_filler=" ", text_arr=[]):
119
- """Prettify array of strings with borders for stdout
120
-
121
- Args:
122
- boundary_filler (str, optional): Upper and lower border filler for text. Defaults to " ".
123
- text_arr (list, optional):The text array to prettify. Each element will be added to a newline. Defaults to [].
124
-
125
- Returns:
126
- [str]: Prettified String
127
- """
128
-
129
- import sys
130
-
131
- if not sys.stdout.isatty():
132
- return (
133
- "\n============================\n"
134
- + "\n".join(text_arr)
135
- + "\n============================\n"
136
- )
137
-
138
- size = os.get_terminal_size()
139
- cols, _ = size.columns, size.lines
140
-
141
- if any(len(text) > cols for text in text_arr):
142
- result = ""
143
- for text in text_arr:
144
- result += text + "\n"
145
- return result
146
-
147
- upper = "\n" + "".ljust(cols, boundary_filler) + "\n" if len(text_arr) > 0 else ""
148
- lower = "".ljust(cols, boundary_filler) if len(text_arr) > 0 else ""
149
-
150
- content = ""
151
- for text in text_arr:
152
- content += text.center(cols) + "\n"
153
-
154
- result = upper + content + lower
155
-
156
- return result
157
-
158
-
159
- def get_child_processes(parent_process):
172
+ def get_child_processes(parent_process, max_attempts=10, sleep_interval=1):
160
173
  """Get list of child processes from a parent process.
161
174
 
162
175
  Args:
@@ -174,7 +187,8 @@ def get_child_processes(parent_process):
174
187
  # to get hold child processes
175
188
  parent_process_psutil = psutil.Process(parent_process.pid)
176
189
 
177
- while True:
190
+ child_processes = None
191
+ for _ in range(max_attempts):
178
192
  try:
179
193
  # Before checking for any child processes, ensure that the parent process is running
180
194
  assert (
@@ -185,13 +199,24 @@ def get_child_processes(parent_process):
185
199
 
186
200
  if not child_processes:
187
201
  logger.debug("Waiting for the child processes to be created...")
202
+ time.sleep(sleep_interval)
188
203
  continue
189
204
 
205
+ else:
206
+ logger.debug(f"Found the child process: {child_processes[0]}")
207
+ break
208
+
190
209
  except AssertionError as err:
191
210
  raise err
192
211
 
193
- if child_processes:
194
- break
212
+ if not child_processes:
213
+ logger.debug(
214
+ f"MATLAB process was not found while searching for the child processes."
215
+ )
216
+
217
+ raise UIVisibleFatalError(
218
+ "Unable to create MATLAB process. Click Start MATLAB to try again."
219
+ )
195
220
 
196
221
  return child_processes
197
222
 
@@ -214,17 +239,120 @@ def get_access_url(app):
214
239
  access_protocol = "https" if ssl_context else "http"
215
240
 
216
241
  # When host interface is set to 0.0.0.0, in a windows system, the server will not be accessible.
217
- # Setting the value to fqdn, will allow it be remotely and locally accessible.
242
+ # Setting the value to 127.0.0.1, will allow it be remotely and locally accessible.
218
243
 
219
244
  # NOTE: When windows container support is introduced this will need to be tweaked accordingly.
220
245
  if host_interface == "0.0.0.0" and system.is_windows():
221
- import socket
222
-
223
- hostname = socket.gethostname()
224
- fqdn = socket.getfqdn(hostname)
246
+ host_interface = "127.0.0.1"
225
247
 
226
- url = f"{access_protocol}://{fqdn}:{port}{base_url}"
227
- else:
228
- url = f"{access_protocol}://{host_interface}:{port}{base_url}"
248
+ url = f"{access_protocol}://{host_interface}:{port}{base_url}"
229
249
 
230
250
  return url
251
+
252
+
253
+ def is_valid_path(path: Path):
254
+ """Returns true if path supplied is a valid path to a file or directory
255
+
256
+ Args:
257
+ path (pathlib.Path): pathlib.Path object of a file or directory
258
+
259
+ Returns:
260
+ bool: True if a valid path is supplied else False
261
+ """
262
+ path = Path(path)
263
+ return path.is_dir() or path.is_file()
264
+
265
+
266
+ def get_caller_name() -> str:
267
+ """Utility function that uses the `inspect` module to access the call stack and returns the name
268
+ of the function that is two levels above in the stack. This is typically the function
269
+ that called the function that invoked `get_caller_name`.
270
+
271
+ Ex: start_matlab() -> set_matlab_state() -> get_caller_name()
272
+ The return value from get_caller_name() would be `start_matlab`
273
+
274
+ Returns:
275
+ str: Name of the parent function.
276
+ """
277
+ try:
278
+ return inspect.stack()[2][3]
279
+
280
+ except Exception as err:
281
+ logger.error(f"Failed to get caller name with err:{err}")
282
+ stack = inspect.stack()
283
+ return stack[len(stack) - 1][3]
284
+
285
+
286
+ class TrackingLock:
287
+ """A class which provides the same features as asyncio.Lock
288
+ and additionally tracks which function acquired the lock.
289
+ """
290
+
291
+ def __init__(self, purpose):
292
+ self._acquired_by = None
293
+ self._lock = asyncio.Lock()
294
+ if not purpose:
295
+ logger.warning("Provide a purpose for this instance of TrackingLock")
296
+ self._purpose = purpose
297
+
298
+ @property
299
+ def acquired_by(self):
300
+ return self._acquired_by
301
+
302
+ @property
303
+ def purpose(self):
304
+ return self._purpose
305
+
306
+ def locked(self):
307
+ return self._lock.locked()
308
+
309
+ async def acquire(self):
310
+ """Acquires the lock"""
311
+ await self._lock.acquire()
312
+ # Store the current task or function information when the lock is acquired
313
+ self._acquired_by = get_caller_name()
314
+ logger.debug(f"Lock acquired by '{self.acquired_by}()'")
315
+
316
+ async def release(self):
317
+ """Releases the lock."""
318
+ if self.locked():
319
+ # Clear the owner information when the lock is released
320
+ self._lock.release()
321
+ logger.debug(f"Lock released by '{self.acquired_by}()'")
322
+ self._acquired_by = None
323
+
324
+ else:
325
+ logger.warn(f"Trying to release {self._purpose} lock before acquiring it.")
326
+
327
+ def validate_lock_for_caller(self, caller):
328
+ """Checks if the specified caller is the current holder of the lock.
329
+
330
+ This method first verifies that the lock is currently held. If it is,
331
+ it then compares the holder's name with the provided caller's name
332
+ to ensure they match.
333
+
334
+ This function should be used by setter methods for verification, before they modify critical sections.
335
+
336
+ Args:
337
+ caller (str): The name of the function to be matched with the lock holder.
338
+
339
+ Returns:
340
+ bool: True if the caller currently holds the lock, False otherwise.
341
+ """
342
+ if not self._lock.locked():
343
+ logger.error(
344
+ LockAcquisitionError(
345
+ f"Lock needs to be acquired by '{caller}()' before modifying {self._purpose}"
346
+ )
347
+ )
348
+ return False
349
+
350
+ if self._acquired_by != caller:
351
+ logger.error(
352
+ LockAcquisitionError(
353
+ f"Lock was acquired by {self._acquired_by} but {self._purpose} is being modified by {caller}."
354
+ )
355
+ )
356
+ return False
357
+
358
+ return True
@@ -0,0 +1,72 @@
1
+ # Copyright 2025 The MathWorks, Inc.
2
+
3
+ from http.cookies import Morsel, SimpleCookie
4
+ from typing import Dict
5
+
6
+ from matlab_proxy.util import mwi
7
+
8
+ logger = mwi.logger.get()
9
+
10
+
11
+ # For more information about HttpOnly attribute
12
+ # of a cookie, check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#httponly
13
+ class HttpOnlyCookieJar:
14
+ """
15
+ A lightweight, HttpOnly, in-memory cookie store.
16
+
17
+ Its sole responsibility is to parse and store 'Set-Cookie' headers as Morsel objects and
18
+ store them in the cookie-jar only if they are marked as HttpOnly.
19
+ """
20
+
21
+ def __init__(self):
22
+ self._cookie_jar: Dict[str, Morsel] = {}
23
+ logger.debug("Cookie Jar Initialized")
24
+
25
+ def _get_cookie_name(self, cookie: SimpleCookie) -> str:
26
+ """
27
+ Returns the name of the cookie.
28
+ """
29
+ return list(cookie.keys())[0]
30
+
31
+ def update_from_response_headers(self, headers) -> None:
32
+ """
33
+ Parses 'Set-Cookie' headers from a response and stores the resulting
34
+ cookie objects (Morsels) only if they are HttpOnly cookies.
35
+ """
36
+ for set_cookie_val in headers.getall("Set-Cookie", []):
37
+ cookie = SimpleCookie()
38
+ cookie.load(set_cookie_val)
39
+ cookie_name = self._get_cookie_name(cookie)
40
+ morsel = cookie[cookie_name]
41
+
42
+ if morsel["httponly"]:
43
+ self._cookie_jar[cookie_name] = morsel
44
+ logger.debug(
45
+ f"Stored cookie object for key '{cookie_name}'. Value: '{cookie[cookie_name]}'"
46
+ )
47
+
48
+ else:
49
+ logger.warning(
50
+ f"Cookie {cookie_name} is not a HttpOnly cookie. Skipping it."
51
+ )
52
+
53
+ def get_cookies(self):
54
+ """
55
+ Returns a copy of the internal dictionary of stored cookie Morsels.
56
+ """
57
+ return list(self._cookie_jar.values())
58
+
59
+ def get_dict(self) -> Dict[str, str]:
60
+ """
61
+ Returns the stored cookies as a simple dictionary of name-to-value strings,
62
+ which is compatible with aiohttp's 'LooseCookies' type.
63
+ """
64
+ loose_cookies = {
65
+ name: morsel.value for name, morsel in self._cookie_jar.items()
66
+ }
67
+ return loose_cookies
68
+
69
+ def clear(self):
70
+ """Clears all stored cookies."""
71
+ logger.info("Cookie Jar Cleared")
72
+ self._cookie_jar.clear()
@@ -1,8 +1,10 @@
1
- # Copyright 2020-2022 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
+
3
+ from typing import Dict, Set, Union
4
+ from contextlib import suppress
2
5
 
3
6
  import asyncio
4
7
 
5
- from matlab_proxy import util
6
8
  from matlab_proxy.util import mwi, system, windows
7
9
 
8
10
  logger = mwi.logger.get()
@@ -30,16 +32,32 @@ def get_event_loop():
30
32
  return loop
31
33
 
32
34
 
33
- async def cancel_tasks(tasks):
35
+ async def cancel_tasks(tasks: Union[Dict[str, asyncio.Task], Set[asyncio.Task]]):
34
36
  """Cancels asyncio tasks.
35
37
 
36
38
  Args:
37
- tasks (asyncio.Task): Asyncio task
39
+ tasks: If a Dict[str, asyncio.Task], contains (task_name, task) as entries.
40
+ If a Set[asyncio.Task], contains a set of asyncio.Task objects.
41
+ """
42
+ if isinstance(tasks, dict):
43
+ for name, task in list(tasks.items()):
44
+ if task:
45
+ await __cancel_task(task)
46
+ logger.debug(f"{name} task stopped successfully")
47
+
48
+ elif isinstance(tasks, set):
49
+ for task in tasks:
50
+ if task:
51
+ await __cancel_task(task)
52
+ logger.debug(f"Task stopped successfully")
53
+
54
+
55
+ async def __cancel_task(task):
56
+ """Cancels a given asyncio task, suppressing CancelledError.
57
+
58
+ Args:
59
+ task (asyncio.Task): The asyncio task to be cancelled.
38
60
  """
39
- for task in tasks:
40
- logger.debug(f"Calling cancel on task: {task}")
61
+ with suppress(asyncio.CancelledError):
41
62
  task.cancel()
42
- try:
43
- await task
44
- except asyncio.CancelledError:
45
- pass
63
+ await task
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2020-2022 The MathWorks, Inc.
1
+ # Copyright (c) 2020-2025 The MathWorks, Inc.
2
2
  # Script to print information about all running matlab-proxy servers for current user on current machine.
3
3
 
4
4
  import glob
@@ -7,6 +7,66 @@ import os
7
7
  import matlab_proxy.settings as mwi_settings
8
8
  import matlab_proxy.util as mwi_util
9
9
 
10
+ from datetime import datetime
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ __NO_SERVERS_MSG = "No MATLAB-PROXY Servers are currently running."
15
+
16
+
17
+ def _extract_version_and_session(title):
18
+ """Extracts session name and MATLAB version from the title."""
19
+ parts = title.split("-")
20
+ if len(parts) < 2:
21
+ return title.replace("MATLAB ", ""), ""
22
+ session_name = parts[0].strip()
23
+ matlab_version = parts[1].strip().replace("MATLAB ", "")
24
+ return matlab_version, session_name
25
+
26
+
27
+ def _get_server_info(server):
28
+ """Helper function to parse info from server file."""
29
+ with open(server) as f:
30
+ # Assumes that the server file contains the address on the first line,
31
+ # the browser_title on the second line, and the timestamp is derived from the file's last modified time.
32
+ address = f.readline().strip()
33
+ browser_title = f.readline().strip()
34
+ matlab_version, session_name = _extract_version_and_session(browser_title)
35
+ timestamp = _get_timestamp(server)
36
+ return timestamp, matlab_version, session_name, address
37
+
38
+
39
+ def _print_server_info_as_table(servers):
40
+ console = Console()
41
+ table = Table(
42
+ title="MATLAB Proxy Servers",
43
+ title_style="cyan",
44
+ title_justify="center",
45
+ caption="No servers found." if not servers else "",
46
+ caption_style="bold red",
47
+ show_header=True,
48
+ header_style="yellow",
49
+ show_lines=True,
50
+ show_edge=True,
51
+ )
52
+ table.add_column("Created On")
53
+ table.add_column("MATLAB\nVersion")
54
+ table.add_column("Session Name")
55
+ table.add_column("Server URL", overflow="fold")
56
+
57
+ # Build server information
58
+ for server in servers:
59
+ table.add_row(*_get_server_info(server))
60
+
61
+ console.print(table)
62
+
63
+
64
+ def _get_timestamp(filename):
65
+ """Get the last modified timestamp of the file in a human-readable format."""
66
+ timestamp = os.path.getmtime(filename)
67
+ readable_time = datetime.fromtimestamp(timestamp).strftime("%d/%m/%y %H:%M:%S")
68
+ return readable_time
69
+
10
70
 
11
71
  def print_server_info():
12
72
  """Print information about all matlab-proxy servers (with version > 0.4.0) running on this machine"""
@@ -15,30 +75,15 @@ def print_server_info():
15
75
  # Look for files in port folders
16
76
  ports_folder = home_folder / "ports"
17
77
  search_string = str(ports_folder) + "/**/mwi_server.info"
78
+ servers = sorted(glob.glob(search_string), key=os.path.getmtime)
18
79
 
19
- print_output = str(
20
- mwi_util.prettify(
21
- boundary_filler="-",
22
- text_arr=["Your running servers are:"],
23
- )
24
- )
25
- print_output += "\n"
26
- search_results = sorted(glob.glob(search_string), key=os.path.getmtime)
27
- if len(search_results) == 0:
28
- print_output += "No MATLAB-PROXY Servers are currently running."
29
- else:
30
- server_number = 0
31
- for server in search_results:
32
- server_number += 1
33
- with open(server) as f:
34
- server_info = f.read()
35
- print_output += str(server_number) + ". " + str(server_info)
36
-
37
- print_output += str(
38
- mwi_util.prettify(
39
- boundary_filler="-",
40
- text_arr=["Thank you."],
41
- )
42
- )
80
+ args = mwi_util.parse_list_cli_args()
43
81
 
44
- return print_output
82
+ if args["quiet"]:
83
+ for server in servers:
84
+ with open(server) as f:
85
+ server_info = f.readline().strip()
86
+ print(f"{server_info}", end="\n")
87
+ else:
88
+ _print_server_info_as_table(servers)
89
+ return
matlab_proxy/util/mw.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2020-2022 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
2
 
3
3
  import asyncio
4
4
  import os
@@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET
8
8
  import aiohttp
9
9
  from matlab_proxy.default_configuration import config
10
10
  from matlab_proxy.util import mwi
11
+ from matlab_proxy.settings import get_process_startup_timeout
11
12
  from matlab_proxy.util.mwi.exceptions import (
12
13
  EntitlementError,
13
14
  MatlabError,
@@ -45,7 +46,7 @@ async def fetch_entitlements(mhlm_api_endpoint, access_token, matlab_release):
45
46
  list: Representing a list of Dicts containing the id, label and license_number.
46
47
  """
47
48
  # Get entitlements for token
48
- async with aiohttp.ClientSession() as client_session:
49
+ async with aiohttp.ClientSession(trust_env=True) as client_session:
49
50
  async with client_session.post(
50
51
  mhlm_api_endpoint,
51
52
  headers={"content-type": "application/x-www-form-urlencoded"},
@@ -59,8 +60,7 @@ async def fetch_entitlements(mhlm_api_endpoint, access_token, matlab_release):
59
60
  }
60
61
  ),
61
62
  ) as res:
62
-
63
- if res.reason != "OK":
63
+ if not res.ok:
64
64
  raise OnlineLicensingError(
65
65
  f"Communication with {mhlm_api_endpoint} failed ({res.status}). For more details, see {__get_licensing_url()}."
66
66
  )
@@ -70,7 +70,7 @@ async def fetch_entitlements(mhlm_api_endpoint, access_token, matlab_release):
70
70
 
71
71
  if entitlement_el is None or len(entitlement_el) == 0:
72
72
  raise EntitlementError(
73
- f"Your MathWorks account is not linked to a valid license for MATLAB {matlab_release}."
73
+ f"Your MathWorks account is not linked to a valid license for MATLAB {matlab_release}.\nSign out and login with a licensed user."
74
74
  )
75
75
 
76
76
  entitlements = entitlement_el.findall("entitlement")
@@ -86,7 +86,7 @@ async def fetch_entitlements(mhlm_api_endpoint, access_token, matlab_release):
86
86
 
87
87
 
88
88
  async def fetch_expand_token(mwa_api_endpoint, identity_token, source_id):
89
- """Asynchronously fetch tokens from MWA API after MHLM licensing is successful.
89
+ """Asynchronously fetch tokens from MWA API endpoint.
90
90
 
91
91
  Args:
92
92
  mwa_api_endpoint (String): URL of the MWA API endpoint.
@@ -99,7 +99,7 @@ async def fetch_expand_token(mwa_api_endpoint, identity_token, source_id):
99
99
  Returns:
100
100
  Dict: Containing User and License expiration details.
101
101
  """
102
- async with aiohttp.ClientSession() as client_session:
102
+ async with aiohttp.ClientSession(trust_env=True) as client_session:
103
103
  async with client_session.post(
104
104
  f"{mwa_api_endpoint}/tokens",
105
105
  headers={
@@ -115,8 +115,7 @@ async def fetch_expand_token(mwa_api_endpoint, identity_token, source_id):
115
115
  }
116
116
  ),
117
117
  ) as res:
118
-
119
- if res.reason != "OK":
118
+ if not res.ok:
120
119
  raise OnlineLicensingError(
121
120
  f"Communication with {mwa_api_endpoint} failed ({res.status}). For more details, see {__get_licensing_url()}."
122
121
  )
@@ -147,7 +146,7 @@ async def fetch_access_token(mwa_api_endpoint, identity_token, source_id):
147
146
  Returns:
148
147
  Dict : Containing the Access token.
149
148
  """
150
- async with aiohttp.ClientSession() as client_session:
149
+ async with aiohttp.ClientSession(trust_env=True) as client_session:
151
150
  async with client_session.post(
152
151
  f"{mwa_api_endpoint}/tokens/access",
153
152
  headers={
@@ -163,8 +162,7 @@ async def fetch_access_token(mwa_api_endpoint, identity_token, source_id):
163
162
  }
164
163
  ),
165
164
  ) as res:
166
-
167
- if res.reason != "OK":
165
+ if not res.ok:
168
166
  raise OnlineLicensingError(
169
167
  f"Communication with {mwa_api_endpoint} failed ({res.status}). For more details, see {__get_licensing_url()}."
170
168
  )
@@ -260,7 +258,7 @@ def parse_other_error(logs):
260
258
  )
261
259
 
262
260
 
263
- async def create_xvfb_process(xvfb_cmd, pipe, env={}):
261
+ async def create_xvfb_process(xvfb_cmd, pipe, env=None):
264
262
  """Creates the Xvfb process.
265
263
 
266
264
  The Xvfb process is run with '-displayfd' flag set. This makes Xvfb choose an available
@@ -293,8 +291,11 @@ async def create_xvfb_process(xvfb_cmd, pipe, env={}):
293
291
  number_of_bytes = 200
294
292
 
295
293
  logger.debug("Waiting for XVFB process to initialize and provide Display Number")
296
- # Waits upto 10 seconds for the read_descriptor to be ready.
297
- ready_descriptors, _, _ = select.select([read_descriptor], [], [], 10)
294
+
295
+ # Wait for timeout specified by matlab-proxy for launching processes.
296
+ ready_descriptors, _, _ = select.select(
297
+ [read_descriptor], [], [], get_process_startup_timeout()
298
+ )
298
299
 
299
300
  # If read_descriptor is in ready_descriptors, read from it.
300
301
  if read_descriptor in ready_descriptors: