matlab-proxy 0.10.1__py3-none-any.whl → 0.12.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 CHANGED
@@ -1,10 +1,11 @@
1
- # Copyright 2020-2023 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
2
 
3
3
  import asyncio
4
4
  import json
5
5
  import mimetypes
6
6
  import pkgutil
7
7
  import sys
8
+ import uuid
8
9
 
9
10
  import aiohttp
10
11
  from aiohttp import client_exceptions, web
@@ -19,6 +20,7 @@ from matlab_proxy.util import mwi
19
20
  from matlab_proxy.util.mwi import environment_variables as mwi_env
20
21
  from matlab_proxy.util.mwi import token_auth
21
22
  from matlab_proxy.util.mwi.exceptions import AppError, InvalidTokenError, LicensingError
23
+ from matlab_proxy.constants import IS_CONCURRENCY_CHECK_ENABLED
22
24
 
23
25
 
24
26
  mimetypes.add_type("font/woff", ".woff")
@@ -93,28 +95,45 @@ def marshal_error(error):
93
95
  return {"message": error.__str__, "logs": "", "type": error.__class__.__name__}
94
96
 
95
97
 
96
- async def create_status_response(app, loadUrl=None):
97
- """Send a generic status response about the state of server,MATLAB and MATLAB Licensing
98
+ async def create_status_response(
99
+ app, loadUrl=None, client_id=None, transfer_session=False, is_desktop=False
100
+ ):
101
+ """Send a generic status response about the state of server, MATLAB, MATLAB Licensing and the client session status.
98
102
 
99
103
  Args:
100
104
  app (aiohttp.web.Application): Web Server
101
105
  loadUrl (String, optional): Represents the root URL. Defaults to None.
106
+ client_id (String, optional): Represents the unique client_id when concurrency check is enabled. Defaults to None.
107
+ transfer_session (Boolean, optional): Represents whether the connection should be transfered or not when concurrency check is enabled. Defaults to False.
108
+ is_desktop (Boolean, optional): Represents whether the request is made by the desktop app or some other kernel. Defaults to False.
102
109
 
103
110
  Returns:
104
- JSONResponse: A JSONResponse object containing the generic state of the server, MATLAB and MATLAB Licensing.
111
+ JSONResponse: A JSONResponse object containing the generic state of the server, MATLAB, MATLAB Licensing and the client session status.
105
112
  """
106
113
  state = app["state"]
107
- return web.json_response(
108
- {
109
- "matlab": {
110
- "status": await state.get_matlab_state(),
111
- },
112
- "licensing": marshal_licensing_info(state.licensing),
113
- "loadUrl": loadUrl,
114
- "error": marshal_error(state.error),
115
- "wsEnv": state.settings.get("ws_env", ""),
116
- }
117
- )
114
+ status = {
115
+ "matlab": {
116
+ "status": await state.get_matlab_state(),
117
+ "version": state.settings["matlab_version"],
118
+ },
119
+ "licensing": marshal_licensing_info(state.licensing),
120
+ "loadUrl": loadUrl,
121
+ "error": marshal_error(state.error),
122
+ "warnings": state.warnings,
123
+ "wsEnv": state.settings.get("ws_env", ""),
124
+ }
125
+
126
+ if IS_CONCURRENCY_CHECK_ENABLED and is_desktop:
127
+ if not client_id:
128
+ client_id = str(uuid.uuid4())
129
+ status["clientId"] = client_id
130
+
131
+ if not state.active_client or transfer_session:
132
+ state.active_client = client_id
133
+
134
+ status["isActiveClient"] = True if state.active_client == client_id else False
135
+
136
+ return web.json_response(status)
118
137
 
119
138
 
120
139
  @token_auth.authenticate_access_decorator
@@ -150,13 +169,21 @@ async def get_env_config(req):
150
169
  """
151
170
  state = req.app["state"]
152
171
  config = state.settings["env_config"]
153
- config["authEnabled"] = state.settings["mwi_is_token_auth_enabled"]
154
172
 
155
173
  config["useMOS"] = mwi_env.Experimental.should_use_mos_html()
156
174
  config["useMRE"] = mwi_env.Experimental.should_use_mre_html()
157
-
175
+ config["isConcurrencyEnabled"] = IS_CONCURRENCY_CHECK_ENABLED
158
176
  # In a previously authenticated session, if the url is accessed without the token(using session cookie), send the token as well.
159
- config["authStatus"] = True if await token_auth.authenticate_request(req) else False
177
+ config["authentication"] = {
178
+ "enabled": state.settings["mwi_is_token_auth_enabled"],
179
+ "status": True if await token_auth.authenticate_request(req) else False,
180
+ }
181
+
182
+ config["matlab"] = {
183
+ "version": state.settings["matlab_version"],
184
+ "supported_versions": constants.SUPPORTED_MATLAB_VERSIONS,
185
+ }
186
+
160
187
  return web.json_response(config)
161
188
 
162
189
 
@@ -172,7 +199,17 @@ async def get_status(req):
172
199
  Returns:
173
200
  JSONResponse: JSONResponse object containing information about the server, MATLAB and MATLAB Licensing.
174
201
  """
175
- return await create_status_response(req.app)
202
+ # The client sends the CLIENT_ID as a query parameter if the concurrency check has been set to true.
203
+ client_id = req.query.get("MWI_CLIENT_ID", None)
204
+ transfer_session = json.loads(req.query.get("TRANSFER_SESSION", "false"))
205
+ is_desktop = req.query.get("IS_DESKTOP", False)
206
+
207
+ return await create_status_response(
208
+ req.app,
209
+ client_id=client_id,
210
+ transfer_session=transfer_session,
211
+ is_desktop=is_desktop,
212
+ )
176
213
 
177
214
 
178
215
  # @token_auth.authenticate_access_decorator
@@ -198,7 +235,7 @@ async def authenticate(req):
198
235
 
199
236
  return web.json_response(
200
237
  {
201
- "authStatus": is_authenticated,
238
+ "status": is_authenticated,
202
239
  "error": error,
203
240
  }
204
241
  )
@@ -263,11 +300,20 @@ async def set_licensing_info(req):
263
300
  await state.set_licensing_nlm(data.get("connectionString"))
264
301
 
265
302
  elif lic_type == "mhlm":
303
+ # If matlab version could not be determined on startup update
304
+ # the value received from the front-end.
305
+ if not state.settings.get(
306
+ "matlab_version_determined_on_startup"
307
+ ) and data.get("matlabVersion"):
308
+ state.settings["matlab_version"] = data.get("matlabVersion")
309
+
266
310
  await state.set_licensing_mhlm(
267
311
  data.get("token"), data.get("emailAddress"), data.get("sourceId")
268
312
  )
313
+
269
314
  elif lic_type == "existing_license":
270
315
  state.set_licensing_existing_license()
316
+
271
317
  else:
272
318
  raise Exception(
273
319
  'License type must be "NLM" or "MHLM" or "ExistingLicense"!'
@@ -325,11 +371,18 @@ async def licensing_info_delete(req):
325
371
  # Removing license information implies terminating MATLAB
326
372
  await state.stop_matlab()
327
373
 
374
+ # When removing licensing data, if matlab version was fetched from the user, remove it
375
+ # on the server side to have a complete 'reset'.
376
+ if state.licensing["type"] == "mhlm" and not state.settings.get(
377
+ "matlab_version_determined_on_startup"
378
+ ):
379
+ state.settings["matlab_version"] = None
380
+
328
381
  # Unset licensing information
329
382
  state.unset_licensing()
330
383
 
331
- # Persist licensing information
332
- state.persist_licensing()
384
+ # Persist config information
385
+ state.persist_config_data()
333
386
 
334
387
  return await create_status_response(req.app)
335
388
 
matlab_proxy/app_state.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2020-2023 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
2
 
3
3
  import asyncio
4
4
  import contextlib
@@ -17,7 +17,6 @@ from matlab_proxy.settings import (
17
17
  from matlab_proxy.constants import (
18
18
  CONNECTOR_SECUREPORT_FILENAME,
19
19
  MATLAB_LOGS_FILE_NAME,
20
- VERSION_INFO_FILE_NAME,
21
20
  )
22
21
  from matlab_proxy.util import mw, mwi, system, windows
23
22
  from matlab_proxy.util.mwi import environment_variables as mwi_env
@@ -85,6 +84,7 @@ class AppState:
85
84
 
86
85
  # Initialize with the error state from the initialization of settings
87
86
  self.error = settings["error"]
87
+ self.warnings = settings["warnings"]
88
88
 
89
89
  if self.error is not None:
90
90
  self.logs["matlab"].clear()
@@ -100,40 +100,44 @@ class AppState:
100
100
  # This variable can be either "up" or "down"
101
101
  self.embedded_connector_state = "down"
102
102
 
103
- def __get_cached_licensing_file(self):
104
- """Get the cached licensing file
103
+ # Specific to concurrent session and is used to track the active client/s that are currently
104
+ # connected to the backend
105
+ self.active_client = None
106
+
107
+ def __get_cached_config_file(self):
108
+ """Get the cached config file
105
109
 
106
110
  Returns:
107
- Path : Path object to cached licensing file
111
+ Path : Path object to cached config file
108
112
  """
109
113
  return self.settings["matlab_config_file"]
110
114
 
111
- def __delete_cached_licensing_file(self):
112
- """Deletes the cached licensing file"""
115
+ def __delete_cached_config_file(self):
116
+ """Deletes the cached config file"""
113
117
  try:
114
- logger.info(f"Deleting any cached licensing files!")
115
- os.remove(self.__get_cached_licensing_file())
118
+ logger.info(f"Deleting any cached config files!")
119
+ os.remove(self.__get_cached_config_file())
116
120
  except FileNotFoundError:
117
121
  # The file being absent is acceptable.
118
122
  pass
119
123
 
120
- def __reset_and_delete_cached_licensing(self):
121
- """Reset licensing variable of the class and removes the cached licensing file."""
122
- logger.info(f"Resetting cached licensing information...")
124
+ def __reset_and_delete_cached_config(self):
125
+ """Reset licensing variable of the class and removes the cached config file."""
126
+ logger.info(f"Resetting cached config information...")
123
127
  self.licensing = None
124
- self.__delete_cached_licensing_file()
128
+ self.__delete_cached_config_file()
125
129
 
126
130
  async def __update_and_persist_licensing(self):
127
- """Update entitlements from mhlm servers and persist licensing
131
+ """Update entitlements from mhlm servers and persist config data
128
132
 
129
133
  Returns:
130
134
  Boolean: True when entitlements were updated and persisted successfully. False otherwise.
131
135
  """
132
136
  successful_update = await self.update_entitlements()
133
137
  if successful_update:
134
- self.persist_licensing()
138
+ self.persist_config_data()
135
139
  else:
136
- self.__reset_and_delete_cached_licensing()
140
+ self.__reset_and_delete_cached_config()
137
141
  return successful_update
138
142
 
139
143
  async def init_licensing(self):
@@ -158,8 +162,8 @@ class AppState:
158
162
  f"!!! Launching MATLAB without providing any additional licensing information. This requires MATLAB to have been activated on the machine from which its being launched !!!"
159
163
  )
160
164
 
161
- # Delete old licensing mode info from cache to ensure its wiped out first before persisting new info.
162
- self.__delete_cached_licensing_file()
165
+ # Delete old config info from cache to ensure its wiped out first before persisting new info.
166
+ self.__delete_cached_config_file()
163
167
 
164
168
  # NLM Connection String set in environment
165
169
  elif self.settings.get("nlm_conn_str", None) is not None:
@@ -171,17 +175,25 @@ class AppState:
171
175
  "conn_str": nlm_licensing_str,
172
176
  }
173
177
 
174
- # Delete old licensing mode info from cache to ensure its wiped out first before persisting new info.
175
- self.__delete_cached_licensing_file()
178
+ # Delete old config info from cache to ensure its wiped out first before persisting new info.
179
+ self.__delete_cached_config_file()
176
180
 
177
181
  # If NLM connection string is not present or if an existing license is not being used,
178
182
  # then look for persistent LNU info
179
- elif self.__get_cached_licensing_file().exists():
180
- with open(self.__get_cached_licensing_file(), "r") as f:
183
+ elif self.__get_cached_config_file().exists():
184
+ with open(self.__get_cached_config_file(), "r") as f:
181
185
  logger.debug("Found cached licensing information...")
182
186
  try:
183
- # Load can throw if the file is empty for some reason.
184
- licensing = json.loads(f.read())
187
+ # Load can throw if the file is empty or expected fields in the json object are missing.
188
+ cached_data = json.loads(f.read())
189
+ licensing = cached_data["licensing"]
190
+ matlab = cached_data["matlab"]
191
+
192
+ # If Matlab version could not be determined on startup and 'version' is available in
193
+ # cached config, update it.
194
+ if not self.settings["matlab_version"]:
195
+ self.settings["matlab_version"] = matlab["version"]
196
+
185
197
  if licensing["type"] == "nlm":
186
198
  # Note: Only NLM settings entered in browser were cached.
187
199
  self.licensing = {
@@ -219,15 +231,15 @@ class AppState:
219
231
  "Using cached Online Licensing to launch MATLAB."
220
232
  )
221
233
  else:
222
- self.__reset_and_delete_cached_licensing()
234
+ self.__reset_and_delete_cached_config()
223
235
  elif licensing["type"] == "existing_license":
224
236
  logger.info("Using cached existing license to launch MATLAB")
225
237
  self.licensing = licensing
226
238
  else:
227
239
  # Somethings wrong, licensing is neither NLM or MHLM
228
- self.__reset_and_delete_cached_licensing()
240
+ self.__reset_and_delete_cached_config()
229
241
  except Exception as e:
230
- self.__reset_and_delete_cached_licensing()
242
+ self.__reset_and_delete_cached_config()
231
243
 
232
244
  async def get_matlab_state(self):
233
245
  """Determine the state of MATLAB to be down/starting/up.
@@ -255,7 +267,10 @@ class AppState:
255
267
  xvfb_process = self.processes["xvfb"]
256
268
 
257
269
  if system.is_linux():
258
- if xvfb_process is None or xvfb_process.returncode is not None:
270
+ # If Xvfb is on system PATH, check if it up and running.
271
+ if self.settings["is_xvfb_available"] and (
272
+ xvfb_process is None or xvfb_process.returncode is not None
273
+ ):
259
274
  logger.debug(
260
275
  "Xvfb has not started"
261
276
  if xvfb_process is None
@@ -355,12 +370,12 @@ class AppState:
355
370
 
356
371
  # TODO Validate connection string
357
372
  self.licensing = {"type": "nlm", "conn_str": conn_str}
358
- self.persist_licensing()
373
+ self.persist_config_data()
359
374
 
360
375
  def set_licensing_existing_license(self):
361
376
  """Set the licensing type to NLM and the connection string."""
362
377
  self.licensing = {"type": "existing_license"}
363
- self.persist_licensing()
378
+ self.persist_config_data()
364
379
 
365
380
  async def set_licensing_mhlm(
366
381
  self,
@@ -461,17 +476,6 @@ class AppState:
461
476
  "MHLM licensing must be configured to update entitlements!"
462
477
  )
463
478
 
464
- # TODO: Updating entitlements requires the matlab version. If it
465
- # could not be determined at server startup, take input from the user
466
- # for the MATLAB version they intend to start and update settings.
467
-
468
- # As MHLM licensing requires matlab version to update entitlements, if it couldn't be determined
469
- # for now show an error to the user to set MWI_CUSTOM_MATLAB_ROOT env var.
470
- if self.settings["matlab_version"] is None:
471
- raise UIVisibleFatalError(
472
- f"Could not find {VERSION_INFO_FILE_NAME} file at MATLAB root. Set {mwi_env.get_env_name_custom_matlab_root()} to a valid MATLAB root path"
473
- )
474
-
475
479
  try:
476
480
  # Fetch an access token
477
481
  access_token_data = await mw.fetch_access_token(
@@ -532,19 +536,23 @@ class AppState:
532
536
  async def update_user_selected_entitlement_info(self, entitlement_id):
533
537
  self.licensing["entitlement_id"] = entitlement_id
534
538
  logger.debug(f"Successfully set {entitlement_id} as the entitlement_id")
535
- self.persist_licensing()
539
+ self.persist_config_data()
536
540
 
537
- def persist_licensing(self):
538
- """Saves licensing information to file"""
541
+ def persist_config_data(self):
542
+ """Saves config information to file"""
539
543
  if self.licensing is None:
540
- self.__delete_cached_licensing_file()
544
+ self.__delete_cached_config_file()
541
545
 
542
546
  elif self.licensing["type"] in ["mhlm", "nlm", "existing_license"]:
543
547
  logger.debug("Saving licensing information...")
544
- cached_licensing_file = self.__get_cached_licensing_file()
545
- cached_licensing_file.parent.mkdir(parents=True, exist_ok=True)
546
- with open(cached_licensing_file, "w") as f:
547
- f.write(json.dumps(self.licensing))
548
+ cached_config_file = self.__get_cached_config_file()
549
+ cached_config_file.parent.mkdir(parents=True, exist_ok=True)
550
+ config = {
551
+ "licensing": self.licensing,
552
+ "matlab": {"version": self.settings["matlab_version"]},
553
+ }
554
+ with open(cached_config_file, "w") as f:
555
+ f.write(json.dumps(config))
548
556
 
549
557
  def create_logs_dir_for_MATLAB(self):
550
558
  """Creates the root folder where MATLAB writes the ready file and updates attibutes on self."""
@@ -671,9 +679,22 @@ class AppState:
671
679
  # DDUX info for MATLAB
672
680
  matlab_env["MW_CONTEXT_TAGS"] = self.settings.get("mw_context_tags")
673
681
 
682
+ # Update DISPLAY env variable for MATLAB only if it was supplied by Xvfb.
674
683
  if system.is_linux():
675
- # Adding DISPLAY key which is only available after starting Xvfb successfully.
676
- matlab_env["DISPLAY"] = self.settings["matlab_display"]
684
+ if self.settings.get("matlab_display", None):
685
+ matlab_env["DISPLAY"] = self.settings["matlab_display"]
686
+ logger.info(
687
+ f"Using the display number supplied by Xvfb process:{matlab_env['DISPLAY']} for launching MATLAB"
688
+ )
689
+ else:
690
+ if "DISPLAY" in matlab_env:
691
+ logger.info(
692
+ f"Using the existing DISPLAY environment variable with value:{matlab_env['DISPLAY']} for launching MATLAB"
693
+ )
694
+ else:
695
+ logger.info(
696
+ "No DISPLAY environment variable found. Launching MATLAB without it."
697
+ )
677
698
 
678
699
  # The matlab ready file is written into this location(self.mwi_logs_dir) by MATLAB
679
700
  # The mwi_logs_dir is where MATLAB will write any subsequent logs
@@ -968,8 +989,8 @@ class AppState:
968
989
  self.error = None
969
990
  self.logs["matlab"].clear()
970
991
 
971
- # Start Xvfb process if in a posix system
972
- if system.is_linux():
992
+ # Start Xvfb process on linux if possible
993
+ if system.is_linux() and self.settings["is_xvfb_available"]:
973
994
  xvfb = await self.__start_xvfb_process()
974
995
 
975
996
  # xvfb variable would be None if creation of the process failed.
matlab_proxy/constants.py CHANGED
@@ -1,5 +1,5 @@
1
- # Copyright 2023 The MathWorks, Inc.
2
- from typing import Final
1
+ # Copyright 2023-2024 The MathWorks, Inc.
2
+ from typing import Final, List
3
3
 
4
4
  """This module defines project-level constants"""
5
5
 
@@ -12,3 +12,16 @@ MATLAB_LOGS_FILE_NAME: Final[str] = "matlab_logs.txt"
12
12
  # This constant is meant for internal use within matlab-proxy
13
13
  # Clients of this package should use settings.py::get_process_startup_timeout() function
14
14
  DEFAULT_PROCESS_START_TIMEOUT: Final[int] = 600
15
+
16
+ SUPPORTED_MATLAB_VERSIONS: Final[List[str]] = [
17
+ "R2020b",
18
+ "R2021a",
19
+ "R2021b",
20
+ "R2022a",
21
+ "R2022b",
22
+ "R2023a",
23
+ "R2023b",
24
+ ]
25
+
26
+ # This constant when set to True restricts the number of active sessions to one
27
+ IS_CONCURRENCY_CHECK_ENABLED: Final[bool] = True
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "files": {
3
- "main.css": "./static/css/main.aa888962.css",
4
- "main.js": "./static/js/main.fbc5c728.js",
3
+ "main.css": "./static/css/main.47712126.css",
4
+ "main.js": "./static/js/main.ea1ebdce.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",
@@ -34,11 +34,11 @@
34
34
  "index.html": "./index.html",
35
35
  "static/media/gripper.svg": "./static/media/gripper.9defbc5e76d0de8bb6e0.svg",
36
36
  "static/media/arrow.svg": "./static/media/arrow.0c2968b90bd9a64c8c3f.svg",
37
- "main.aa888962.css.map": "./static/css/main.aa888962.css.map",
38
- "main.fbc5c728.js.map": "./static/js/main.fbc5c728.js.map"
37
+ "main.47712126.css.map": "./static/css/main.47712126.css.map",
38
+ "main.ea1ebdce.js.map": "./static/js/main.ea1ebdce.js.map"
39
39
  },
40
40
  "entrypoints": [
41
- "static/css/main.aa888962.css",
42
- "static/js/main.fbc5c728.js"
41
+ "static/css/main.47712126.css",
42
+ "static/js/main.ea1ebdce.js"
43
43
  ]
44
44
  }
@@ -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.fbc5c728.js"></script><link href="./static/css/main.aa888962.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
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.ea1ebdce.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>