matlab-proxy 0.5.3__py3-none-any.whl → 0.30.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matlab_proxy/app.py +578 -205
- matlab_proxy/app_state.py +1061 -431
- matlab_proxy/constants.py +37 -0
- matlab_proxy/default_configuration.py +39 -4
- matlab_proxy/devel.py +18 -22
- matlab_proxy/gui/index.html +20 -1
- matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
- matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
- matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
- matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
- matlab_proxy/matlab/startup.m +3 -28
- matlab_proxy/settings.py +543 -112
- matlab_proxy/util/__init__.py +187 -59
- matlab_proxy/util/cookie_jar.py +72 -0
- matlab_proxy/util/event_loop.py +28 -10
- matlab_proxy/util/list_servers.py +71 -26
- matlab_proxy/util/mw.py +16 -15
- matlab_proxy/util/mwi/download.py +136 -0
- matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
- matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
- matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
- matlab_proxy/util/mwi/environment_variables.py +120 -27
- matlab_proxy/util/mwi/exceptions.py +63 -9
- matlab_proxy/util/mwi/logger.py +141 -27
- matlab_proxy/util/mwi/session_name.py +28 -0
- matlab_proxy/util/mwi/token_auth.py +264 -121
- matlab_proxy/util/mwi/validators.py +231 -88
- matlab_proxy/util/system.py +9 -0
- matlab_proxy/util/windows.py +32 -6
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
- matlab_proxy-0.30.1.dist-info/RECORD +88 -0
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
- matlab_proxy_manager/README.md +85 -0
- matlab_proxy_manager/__init__.py +6 -0
- matlab_proxy_manager/lib/README.md +53 -0
- matlab_proxy_manager/lib/__init__.py +1 -0
- matlab_proxy_manager/lib/api.py +419 -0
- matlab_proxy_manager/storage/README.md +54 -0
- matlab_proxy_manager/storage/__init__.py +1 -0
- matlab_proxy_manager/storage/file_repository.py +144 -0
- matlab_proxy_manager/storage/interface.py +62 -0
- matlab_proxy_manager/storage/server.py +172 -0
- matlab_proxy_manager/utils/__init__.py +1 -0
- matlab_proxy_manager/utils/auth.py +77 -0
- matlab_proxy_manager/utils/constants.py +8 -0
- matlab_proxy_manager/utils/decorators.py +37 -0
- matlab_proxy_manager/utils/environment_variables.py +51 -0
- matlab_proxy_manager/utils/exceptions.py +45 -0
- matlab_proxy_manager/utils/helpers.py +314 -0
- matlab_proxy_manager/utils/logger.py +76 -0
- matlab_proxy_manager/web/README.md +37 -0
- matlab_proxy_manager/web/__init__.py +1 -0
- matlab_proxy_manager/web/app.py +536 -0
- matlab_proxy_manager/web/monitor.py +45 -0
- matlab_proxy_manager/web/watcher.py +65 -0
- matlab_proxy/gui/asset-manifest.json +0 -23
- matlab_proxy/gui/authorization.html +0 -115
- matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
- matlab_proxy/gui/navbar.css +0 -8
- matlab_proxy/gui/signin.css +0 -42
- matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
- matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
- matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
- matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
- matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
- matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
- matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
- matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
- matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
- matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
- matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
- matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
- matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
- matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
- matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
- matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
- matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
- matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
- matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
- matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
- matlab_proxy/gui/token.html +0 -123
- matlab_proxy-0.5.3.dist-info/RECORD +0 -84
- matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
- /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
- /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
- /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
- /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
- /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info/licenses}/LICENSE.md +0 -0
matlab_proxy/util/__init__.py
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2020-2025 The MathWorks, Inc.
|
|
2
2
|
import argparse
|
|
3
|
+
import inspect
|
|
3
4
|
import os
|
|
4
5
|
import socket
|
|
5
|
-
import
|
|
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
|
|
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.
|
|
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
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
hostname = socket.gethostname()
|
|
224
|
-
fqdn = socket.getfqdn(hostname)
|
|
246
|
+
host_interface = "127.0.0.1"
|
|
225
247
|
|
|
226
|
-
|
|
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()
|
matlab_proxy/util/event_loop.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# Copyright 2020-
|
|
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
|
|
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
|
-
|
|
40
|
-
logger.debug(f"Calling cancel on task: {task}")
|
|
61
|
+
with suppress(asyncio.CancelledError):
|
|
41
62
|
task.cancel()
|
|
42
|
-
|
|
43
|
-
await task
|
|
44
|
-
except asyncio.CancelledError:
|
|
45
|
-
pass
|
|
63
|
+
await task
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2020-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
297
|
-
|
|
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:
|