matlab-proxy 0.16.0__py3-none-any.whl → 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of matlab-proxy might be problematic. Click here for more details.
- matlab_proxy/app.py +40 -24
- matlab_proxy/app_state.py +108 -4
- matlab_proxy/constants.py +1 -0
- matlab_proxy/gui/asset-manifest.json +3 -3
- matlab_proxy/gui/index.html +1 -1
- matlab_proxy/gui/static/js/{main.522d83ba.js → main.5b5ca2f2.js} +3 -3
- matlab_proxy/gui/static/js/main.5b5ca2f2.js.map +1 -0
- matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
- matlab_proxy/matlab/startup.m +2 -2
- matlab_proxy/settings.py +18 -2
- matlab_proxy/util/__init__.py +16 -7
- matlab_proxy/util/mwi/environment_variables.py +5 -0
- matlab_proxy/util/mwi/validators.py +13 -11
- matlab_proxy/util/windows.py +6 -2
- {matlab_proxy-0.16.0.dist-info → matlab_proxy-0.18.0.dist-info}/METADATA +4 -2
- {matlab_proxy-0.16.0.dist-info → matlab_proxy-0.18.0.dist-info}/RECORD +25 -24
- tests/unit/test_app.py +14 -0
- tests/unit/test_app_state.py +112 -0
- tests/unit/test_settings.py +35 -0
- tests/unit/util/test_util.py +4 -1
- matlab_proxy/gui/static/js/main.522d83ba.js.map +0 -1
- /matlab_proxy/gui/static/js/{main.522d83ba.js.LICENSE.txt → main.5b5ca2f2.js.LICENSE.txt} +0 -0
- {matlab_proxy-0.16.0.dist-info → matlab_proxy-0.18.0.dist-info}/LICENSE.md +0 -0
- {matlab_proxy-0.16.0.dist-info → matlab_proxy-0.18.0.dist-info}/WHEEL +0 -0
- {matlab_proxy-0.16.0.dist-info → matlab_proxy-0.18.0.dist-info}/entry_points.txt +0 -0
- {matlab_proxy-0.16.0.dist-info → matlab_proxy-0.18.0.dist-info}/top_level.txt +0 -0
matlab_proxy/app.py
CHANGED
|
@@ -6,7 +6,6 @@ import mimetypes
|
|
|
6
6
|
import pkgutil
|
|
7
7
|
import secrets
|
|
8
8
|
import sys
|
|
9
|
-
import uuid
|
|
10
9
|
|
|
11
10
|
import aiohttp
|
|
12
11
|
from aiohttp import client_exceptions, web
|
|
@@ -17,12 +16,11 @@ from cryptography import fernet
|
|
|
17
16
|
import matlab_proxy
|
|
18
17
|
from matlab_proxy import constants, settings, util
|
|
19
18
|
from matlab_proxy.app_state import AppState
|
|
19
|
+
from matlab_proxy.constants import IS_CONCURRENCY_CHECK_ENABLED
|
|
20
20
|
from matlab_proxy.util import mwi
|
|
21
|
+
from matlab_proxy.util.mwi import download, token_auth
|
|
21
22
|
from matlab_proxy.util.mwi import environment_variables as mwi_env
|
|
22
|
-
from matlab_proxy.util.mwi import token_auth, download
|
|
23
23
|
from matlab_proxy.util.mwi.exceptions import AppError, InvalidTokenError, LicensingError
|
|
24
|
-
from matlab_proxy.constants import IS_CONCURRENCY_CHECK_ENABLED
|
|
25
|
-
|
|
26
24
|
|
|
27
25
|
mimetypes.add_type("font/woff", ".woff")
|
|
28
26
|
mimetypes.add_type("font/woff2", ".woff2")
|
|
@@ -97,16 +95,15 @@ def marshal_error(error):
|
|
|
97
95
|
|
|
98
96
|
|
|
99
97
|
async def create_status_response(
|
|
100
|
-
app, loadUrl=None, client_id=None,
|
|
98
|
+
app, loadUrl=None, client_id=None, is_active_client=None
|
|
101
99
|
):
|
|
102
100
|
"""Send a generic status response about the state of server, MATLAB, MATLAB Licensing and the client session status.
|
|
103
101
|
|
|
104
102
|
Args:
|
|
105
103
|
app (aiohttp.web.Application): Web Server
|
|
106
104
|
loadUrl (String, optional): Represents the root URL. Defaults to None.
|
|
107
|
-
client_id (String, optional): Represents the
|
|
108
|
-
|
|
109
|
-
is_desktop (Boolean, optional): Represents whether the request is made by the desktop app or some other kernel. Defaults to False.
|
|
105
|
+
client_id (String, optional): Represents the generated client_id when concurrency check is enabled and client does not have a client_id. Defaults to None.
|
|
106
|
+
is_active_client (Boolean, optional): Represents whether the current client is the active_client when concurrency check is enabled. Defaults to None.
|
|
110
107
|
|
|
111
108
|
Returns:
|
|
112
109
|
JSONResponse: A JSONResponse object containing the generic state of the server, MATLAB, MATLAB Licensing and the client session status.
|
|
@@ -124,17 +121,30 @@ async def create_status_response(
|
|
|
124
121
|
"wsEnv": state.settings.get("ws_env", ""),
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
if client_id:
|
|
125
|
+
status["clientId"] = client_id
|
|
126
|
+
if is_active_client is not None:
|
|
127
|
+
status["isActiveClient"] = is_active_client
|
|
128
|
+
|
|
129
|
+
return web.json_response(status)
|
|
131
130
|
|
|
132
|
-
if not state.active_client or transfer_session:
|
|
133
|
-
state.active_client = client_id
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
@token_auth.authenticate_access_decorator
|
|
133
|
+
async def clear_client_id(req):
|
|
134
|
+
"""API endpoint to reset the active client
|
|
136
135
|
|
|
137
|
-
|
|
136
|
+
Args:
|
|
137
|
+
req (HTTPRequest): HTTPRequest Object
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Response: an empty response in JSON format
|
|
141
|
+
"""
|
|
142
|
+
# Sleep for one second prior to clearing the client id to ensure that any remaining get_status responses are fully processed first.
|
|
143
|
+
await asyncio.sleep(1)
|
|
144
|
+
state = req.app["state"]
|
|
145
|
+
state.active_client = None
|
|
146
|
+
# This response is of no relevance to the front-end as the client has already exited
|
|
147
|
+
return web.json_response({})
|
|
138
148
|
|
|
139
149
|
|
|
140
150
|
@token_auth.authenticate_access_decorator
|
|
@@ -201,15 +211,17 @@ async def get_status(req):
|
|
|
201
211
|
JSONResponse: JSONResponse object containing information about the server, MATLAB and MATLAB Licensing.
|
|
202
212
|
"""
|
|
203
213
|
# The client sends the CLIENT_ID as a query parameter if the concurrency check has been set to true.
|
|
214
|
+
state = req.app["state"]
|
|
204
215
|
client_id = req.query.get("MWI_CLIENT_ID", None)
|
|
205
216
|
transfer_session = json.loads(req.query.get("TRANSFER_SESSION", "false"))
|
|
206
217
|
is_desktop = req.query.get("IS_DESKTOP", False)
|
|
207
218
|
|
|
219
|
+
generated_client_id, is_active_client = state.get_session_status(
|
|
220
|
+
is_desktop, client_id, transfer_session
|
|
221
|
+
)
|
|
222
|
+
|
|
208
223
|
return await create_status_response(
|
|
209
|
-
req.app,
|
|
210
|
-
client_id=client_id,
|
|
211
|
-
transfer_session=transfer_session,
|
|
212
|
-
is_desktop=is_desktop,
|
|
224
|
+
req.app, client_id=generated_client_id, is_active_client=is_active_client
|
|
213
225
|
)
|
|
214
226
|
|
|
215
227
|
|
|
@@ -409,7 +421,7 @@ async def termination_integration_delete(req):
|
|
|
409
421
|
await req.app.cleanup()
|
|
410
422
|
"""When testing with pytest, its not possible to catch sys.exit(0) using the construct
|
|
411
423
|
'with pytest.raises()', there by causing the test : test_termination_integration_delete()
|
|
412
|
-
to fail.
|
|
424
|
+
to fail. In order to avoid this, adding the below if condition to check to skip sys.exit(0) when testing
|
|
413
425
|
"""
|
|
414
426
|
logger.debug("Exiting with return code 0")
|
|
415
427
|
if not mwi_env.is_testing_mode_enabled():
|
|
@@ -462,7 +474,7 @@ def make_static_route_table(app):
|
|
|
462
474
|
Returns:
|
|
463
475
|
Dict: Containing information about the static files and header information.
|
|
464
476
|
"""
|
|
465
|
-
|
|
477
|
+
import importlib_resources
|
|
466
478
|
|
|
467
479
|
from matlab_proxy import gui
|
|
468
480
|
from matlab_proxy.gui import static
|
|
@@ -479,8 +491,9 @@ def make_static_route_table(app):
|
|
|
479
491
|
(gui.static.js.__name__, "/static/js"),
|
|
480
492
|
(gui.static.media.__name__, "/static/media"),
|
|
481
493
|
]:
|
|
482
|
-
for
|
|
483
|
-
|
|
494
|
+
for entry in importlib_resources.files(mod).iterdir():
|
|
495
|
+
name = entry.name
|
|
496
|
+
if not importlib_resources.files(mod).joinpath(name).is_dir():
|
|
484
497
|
if name != "__init__.py":
|
|
485
498
|
# Special case for manifest.json
|
|
486
499
|
if "manifest.json" in name:
|
|
@@ -773,6 +786,8 @@ async def cleanup_background_tasks(app):
|
|
|
773
786
|
# Stop any running async tasks
|
|
774
787
|
logger = mwi.logger.get()
|
|
775
788
|
tasks = state.tasks
|
|
789
|
+
if state.task_detect_client_status:
|
|
790
|
+
tasks["detect_client_status"] = state.task_detect_client_status
|
|
776
791
|
for task_name, task in tasks.items():
|
|
777
792
|
if not task.cancelled():
|
|
778
793
|
logger.debug(f"Cancelling MWI task: {task_name} : {task} ")
|
|
@@ -868,6 +883,7 @@ def create_app(config_name=matlab_proxy.get_default_config_name()):
|
|
|
868
883
|
app.router.add_route("GET", f"{base_url}/get_auth_token", get_auth_token)
|
|
869
884
|
app.router.add_route("GET", f"{base_url}/get_env_config", get_env_config)
|
|
870
885
|
app.router.add_route("PUT", f"{base_url}/start_matlab", start_matlab)
|
|
886
|
+
app.router.add_route("POST", f"{base_url}/clear_client_id", clear_client_id)
|
|
871
887
|
app.router.add_route("DELETE", f"{base_url}/stop_matlab", stop_matlab)
|
|
872
888
|
app.router.add_route("PUT", f"{base_url}/set_licensing_info", set_licensing_info)
|
|
873
889
|
app.router.add_route("PUT", f"{base_url}/update_entitlement", update_entitlement)
|
matlab_proxy/app_state.py
CHANGED
|
@@ -9,15 +9,18 @@ import time
|
|
|
9
9
|
from collections import deque
|
|
10
10
|
from datetime import datetime, timedelta, timezone
|
|
11
11
|
from typing import Final, Optional
|
|
12
|
+
import uuid
|
|
12
13
|
|
|
13
14
|
from matlab_proxy import util
|
|
14
15
|
from matlab_proxy.constants import (
|
|
15
16
|
CONNECTOR_SECUREPORT_FILENAME,
|
|
16
17
|
MATLAB_LOGS_FILE_NAME,
|
|
18
|
+
IS_CONCURRENCY_CHECK_ENABLED,
|
|
19
|
+
USER_CODE_OUTPUT_FILE_NAME,
|
|
17
20
|
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
from matlab_proxy.settings import get_process_startup_timeout
|
|
23
|
+
|
|
21
24
|
from matlab_proxy.util import mw, mwi, system, windows
|
|
22
25
|
from matlab_proxy.util.mwi import environment_variables as mwi_env
|
|
23
26
|
from matlab_proxy.util.mwi import token_auth
|
|
@@ -103,6 +106,12 @@ class AppState:
|
|
|
103
106
|
# connected to the backend
|
|
104
107
|
self.active_client = None
|
|
105
108
|
|
|
109
|
+
# Used to detect whether the active client is actively sending out request or is inactive
|
|
110
|
+
self.active_client_request_detected = False
|
|
111
|
+
|
|
112
|
+
# An event loop task to handle the detection of client activity
|
|
113
|
+
self.task_detect_client_status = None
|
|
114
|
+
|
|
106
115
|
def __get_cached_config_file(self):
|
|
107
116
|
"""Get the cached config file
|
|
108
117
|
|
|
@@ -586,6 +595,24 @@ class AppState:
|
|
|
586
595
|
self.matlab_session_files["matlab_ready_file"] = matlab_ready_file
|
|
587
596
|
|
|
588
597
|
logger.debug(f"matlab_session_files:{self.matlab_session_files}")
|
|
598
|
+
|
|
599
|
+
# check if the user has provided any code or not
|
|
600
|
+
if self.settings.get("has_custom_code_to_execute"):
|
|
601
|
+
# Keep a reference to the user code output file in the matlab_session_files for cleanup
|
|
602
|
+
user_code_output_file = mwi_logs_dir / USER_CODE_OUTPUT_FILE_NAME
|
|
603
|
+
self.matlab_session_files["startup_code_output_file"] = (
|
|
604
|
+
user_code_output_file
|
|
605
|
+
)
|
|
606
|
+
logger.info(
|
|
607
|
+
util.prettify(
|
|
608
|
+
boundary_filler="*",
|
|
609
|
+
text_arr=[
|
|
610
|
+
f"When MATLAB starts, you can see the output for your startup code at:",
|
|
611
|
+
f"{self.matlab_session_files.get('startup_code_output_file', ' ')}",
|
|
612
|
+
],
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
|
|
589
616
|
return
|
|
590
617
|
|
|
591
618
|
def create_server_info_file(self):
|
|
@@ -838,6 +865,10 @@ class AppState:
|
|
|
838
865
|
|
|
839
866
|
return matlab
|
|
840
867
|
|
|
868
|
+
except UIVisibleFatalError as e:
|
|
869
|
+
self.error = e
|
|
870
|
+
log_error(logger, e)
|
|
871
|
+
|
|
841
872
|
except Exception as err:
|
|
842
873
|
self.error = err
|
|
843
874
|
log_error(logger, err)
|
|
@@ -964,7 +995,7 @@ class AppState:
|
|
|
964
995
|
)
|
|
965
996
|
await self.stop_matlab(force_quit=True)
|
|
966
997
|
self.error = MatlabError(
|
|
967
|
-
"
|
|
998
|
+
"MATLAB startup has timed out. Click Start MATLAB to try again."
|
|
968
999
|
)
|
|
969
1000
|
|
|
970
1001
|
async def __read_matlab_ready_file(self, delay):
|
|
@@ -1304,3 +1335,76 @@ class AppState:
|
|
|
1304
1335
|
if err is not None:
|
|
1305
1336
|
self.error = err
|
|
1306
1337
|
log_error(logger, err)
|
|
1338
|
+
|
|
1339
|
+
def get_session_status(self, is_desktop, client_id, transfer_session):
|
|
1340
|
+
"""
|
|
1341
|
+
Determines the session status for a client, potentially generating a new client ID.
|
|
1342
|
+
|
|
1343
|
+
This function is responsible for managing and tracking the session status of a client.
|
|
1344
|
+
It can generate a new client ID if one is not provided and the conditions are met.
|
|
1345
|
+
It also manages the active client status within the session, especially in scenarios
|
|
1346
|
+
involving desktop clients and when concurrency checks are enabled.
|
|
1347
|
+
|
|
1348
|
+
Args:
|
|
1349
|
+
is_desktop (bool): A flag indicating whether the client is a desktop client.
|
|
1350
|
+
client_id (str or None): The client ID. If None, a new client ID may be generated.
|
|
1351
|
+
transfer_session (bool): Indicates whether the session should be transferred to this client.
|
|
1352
|
+
|
|
1353
|
+
Returns:
|
|
1354
|
+
tuple:
|
|
1355
|
+
- A 2-tuple containing the generated client ID (or None if not generated) and
|
|
1356
|
+
a boolean indicating whether the client is considered the active client.
|
|
1357
|
+
- If concurrency checks are not enabled or the client is not a desktop client, it returns None for both
|
|
1358
|
+
the generated client ID and the active client status.
|
|
1359
|
+
"""
|
|
1360
|
+
if IS_CONCURRENCY_CHECK_ENABLED and is_desktop:
|
|
1361
|
+
generated_client_id = None
|
|
1362
|
+
if not client_id:
|
|
1363
|
+
generated_client_id = str(uuid.uuid4())
|
|
1364
|
+
client_id = generated_client_id
|
|
1365
|
+
|
|
1366
|
+
if not self.active_client or transfer_session:
|
|
1367
|
+
self.active_client = client_id
|
|
1368
|
+
|
|
1369
|
+
if not self.task_detect_client_status:
|
|
1370
|
+
# Create the loop to detect the active status of the client
|
|
1371
|
+
loop = util.get_event_loop()
|
|
1372
|
+
self.task_detect_client_status = loop.create_task(
|
|
1373
|
+
self.detect_active_client_status()
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
if self.active_client == client_id:
|
|
1377
|
+
is_active_client = True
|
|
1378
|
+
self.active_client_request_detected = True
|
|
1379
|
+
else:
|
|
1380
|
+
is_active_client = False
|
|
1381
|
+
return generated_client_id, is_active_client
|
|
1382
|
+
return None, None
|
|
1383
|
+
|
|
1384
|
+
async def detect_active_client_status(self, sleep_time=1, max_inactive_count=10):
|
|
1385
|
+
"""Detects whether the client is online or not by continuously checking if the active client is making requests
|
|
1386
|
+
|
|
1387
|
+
Args:
|
|
1388
|
+
sleep_time (int): The time in seconds for which the process waits before checking for the next get_status request from the active client.
|
|
1389
|
+
max_inactive_count (int): The maximum number of times the check for the request from the active_client fails before reseting the active client id.
|
|
1390
|
+
"""
|
|
1391
|
+
inactive_count = 0
|
|
1392
|
+
while self.active_client:
|
|
1393
|
+
# Check if the get_status request from the active client is received or not
|
|
1394
|
+
await asyncio.sleep(sleep_time)
|
|
1395
|
+
if self.active_client_request_detected:
|
|
1396
|
+
self.active_client_request_detected = False
|
|
1397
|
+
inactive_count = 0
|
|
1398
|
+
else:
|
|
1399
|
+
inactive_count = inactive_count + 1
|
|
1400
|
+
if inactive_count > max_inactive_count:
|
|
1401
|
+
# If no request is received from the active_client for more than 10 seconds then clear the active client id
|
|
1402
|
+
inactive_count = 0
|
|
1403
|
+
self.active_client = None
|
|
1404
|
+
if self.task_detect_client_status:
|
|
1405
|
+
try:
|
|
1406
|
+
# Self cleanup of the task
|
|
1407
|
+
self.task_detect_client_status.cancel()
|
|
1408
|
+
self.task_detect_client_status = None
|
|
1409
|
+
except Exception as e:
|
|
1410
|
+
logger.error("Cleaning of task: 'detect_client_status' failed.")
|
matlab_proxy/constants.py
CHANGED
|
@@ -7,6 +7,7 @@ CONNECTOR_SECUREPORT_FILENAME: Final[str] = "connector.securePort"
|
|
|
7
7
|
VERSION_INFO_FILE_NAME: Final[str] = "VersionInfo.xml"
|
|
8
8
|
MAX_HTTP_REQUEST_SIZE: Final[int] = 500_000_000 # 500MB
|
|
9
9
|
MATLAB_LOGS_FILE_NAME: Final[str] = "matlab_logs.txt"
|
|
10
|
+
USER_CODE_OUTPUT_FILE_NAME: Final[str] = "startup_code_output.txt"
|
|
10
11
|
|
|
11
12
|
# Max startup duration in seconds for processes launched by matlab-proxy
|
|
12
13
|
# This constant is meant for internal use within matlab-proxy
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": {
|
|
3
3
|
"main.css": "./static/css/main.47712126.css",
|
|
4
|
-
"main.js": "./static/js/main.
|
|
4
|
+
"main.js": "./static/js/main.5b5ca2f2.js",
|
|
5
5
|
"static/media/mathworks-pictograms.svg?20181009": "./static/media/mathworks-pictograms.f6f087b008b5a9435f26.svg",
|
|
6
6
|
"static/media/MATLAB-env-blur.png": "./static/media/MATLAB-env-blur.4fc94edbc82d3184e5cb.png",
|
|
7
7
|
"static/media/mathworks.svg?20181004": "./static/media/mathworks.80a3218e1ba29f0573fb.svg",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"static/media/gripper.svg": "./static/media/gripper.9defbc5e76d0de8bb6e0.svg",
|
|
36
36
|
"static/media/arrow.svg": "./static/media/arrow.0c2968b90bd9a64c8c3f.svg",
|
|
37
37
|
"main.47712126.css.map": "./static/css/main.47712126.css.map",
|
|
38
|
-
"main.
|
|
38
|
+
"main.5b5ca2f2.js.map": "./static/js/main.5b5ca2f2.js.map"
|
|
39
39
|
},
|
|
40
40
|
"entrypoints": [
|
|
41
41
|
"static/css/main.47712126.css",
|
|
42
|
-
"static/js/main.
|
|
42
|
+
"static/js/main.5b5ca2f2.js"
|
|
43
43
|
]
|
|
44
44
|
}
|
matlab_proxy/gui/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MATLAB"/><meta name="internal_mw_identifier" content="MWI_MATLAB_PROXY_IDENTIFIER"/><link rel="manifest" href="./manifest.json"/><title>MATLAB</title><script defer="defer" src="./static/js/main.
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MATLAB"/><meta name="internal_mw_identifier" content="MWI_MATLAB_PROXY_IDENTIFIER"/><link rel="manifest" href="./manifest.json"/><title>MATLAB</title><script defer="defer" src="./static/js/main.5b5ca2f2.js"></script><link href="./static/css/main.47712126.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|