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
matlab_proxy/settings.py CHANGED
@@ -1,39 +1,141 @@
1
- # Copyright (c) 2020-2022 The MathWorks, Inc.
1
+ # Copyright 2020-2025 The MathWorks, Inc.
2
2
 
3
+ import datetime
3
4
  import os
4
5
  import shutil
5
6
  import socket
6
7
  import ssl
7
- import sys
8
8
  import tempfile
9
9
  import uuid
10
10
  import xml.etree.ElementTree as ET
11
11
  from pathlib import Path
12
12
 
13
+ from cryptography import x509
14
+ from cryptography.hazmat.primitives import hashes, serialization
15
+ from cryptography.hazmat.primitives.asymmetric import rsa
16
+ from cryptography.x509.oid import NameOID
17
+
13
18
  import matlab_proxy
14
- from matlab_proxy.util import mwi
19
+ from matlab_proxy import constants
20
+ from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
21
+ from matlab_proxy.util import mwi, system
22
+ from matlab_proxy.util.cookie_jar import HttpOnlyCookieJar
15
23
  from matlab_proxy.util.mwi import environment_variables as mwi_env
16
- from matlab_proxy.util.mwi import token_auth
24
+ from matlab_proxy.util.mwi import session_name, token_auth
25
+ from matlab_proxy.util.mwi.exceptions import (
26
+ FatalError,
27
+ MatlabInstallError,
28
+ UIVisibleFatalError,
29
+ )
17
30
 
18
31
  logger = mwi.logger.get()
19
32
 
20
33
 
21
- def get_matlab_path():
22
- which_matlab = shutil.which("matlab")
23
- if which_matlab is None:
24
- return None
25
- return Path(which_matlab).resolve().parent.parent
34
+ def get_process_startup_timeout():
35
+ """Returns the timeout for a process launched by matlab-proxy as specified by MWI_PROCESS_START_TIMEOUT environment variable
36
+ if valid, else returns the default value.
37
+
38
+ Returns:
39
+ int: timeout for a process launched by matlab-proxy
40
+ """
41
+ custom_startup_timeout = os.getenv(mwi_env.get_env_name_process_startup_timeout())
42
+
43
+ if custom_startup_timeout:
44
+ if custom_startup_timeout.isdigit():
45
+ logger.info(
46
+ f"Using custom process startup timeout {custom_startup_timeout} seconds"
47
+ )
48
+ return int(custom_startup_timeout)
49
+
50
+ else:
51
+ logger.warning(
52
+ f"The value set for {mwi_env.get_env_name_process_startup_timeout()}:{custom_startup_timeout} is not a number. Using {constants.DEFAULT_PROCESS_START_TIMEOUT} as the default value"
53
+ )
54
+ return constants.DEFAULT_PROCESS_START_TIMEOUT
55
+
56
+ logger.debug(
57
+ f"Using {constants.DEFAULT_PROCESS_START_TIMEOUT} seconds as the default timeout value"
58
+ )
59
+
60
+ return constants.DEFAULT_PROCESS_START_TIMEOUT
61
+
62
+
63
+ def get_matlab_executable_and_root_path():
64
+ """Returns the path from the MWI_CUSTOM_MATLAB_ROOT environment variable if valid, else returns
65
+ MATLAB root based on the matlab executable if found on the system path.
66
+
67
+ Returns:
68
+ pathlib.Path: pathlib.Path objects to MATLAB executable & MATLAB root.
69
+ """
70
+
71
+ # Use custom matlab root path if provided.
72
+ custom_matlab_root_path = os.environ.get(mwi_env.get_env_name_custom_matlab_root())
73
+
74
+ if custom_matlab_root_path:
75
+ matlab_root_path = Path(custom_matlab_root_path)
76
+
77
+ # Terminate process if invalid Custom Path was provided!
78
+ matlab_root_path = mwi.validators.validate_matlab_root_path(
79
+ matlab_root_path, is_custom_matlab_root=True
80
+ )
26
81
 
82
+ # Generate executable path from root path
83
+ matlab_executable_path = matlab_root_path / "bin" / "matlab"
84
+ if system.is_windows():
85
+ matlab_executable_path = matlab_executable_path.with_suffix(".exe")
27
86
 
28
- def get_matlab_version(matlab_path):
29
- """Get the MATLAB Release version in this image"""
87
+ logger.info(
88
+ f"Using Custom MATLAB Executable: {matlab_executable_path} with Root: {matlab_root_path}"
89
+ )
90
+ return matlab_executable_path, matlab_root_path
91
+
92
+ # Custom matlab root not specified, search for MATLAB on system path
93
+ matlab_executable_path = shutil.which("matlab")
94
+
95
+ if matlab_executable_path:
96
+ matlab_root_path = Path(matlab_executable_path).resolve().parent.parent
97
+ logger.debug(f"MATLAB root folder: {matlab_root_path}")
98
+ matlab_root_path = mwi.validators.validate_matlab_root_path(
99
+ matlab_root_path, is_custom_matlab_root=False
100
+ )
101
+ return matlab_executable_path, matlab_root_path
102
+
103
+ # Control only gets here if custom matlab root was not set AND which matlab returned no results.
104
+ # Note, error messages are formatted as multi-line strings and the front end displays them as is.
105
+ raise MatlabInstallError(
106
+ "Unable to find MATLAB on the system PATH. Add MATLAB to the system PATH, and restart matlab-proxy."
107
+ )
108
+
109
+
110
+ def get_matlab_version(matlab_root_path):
111
+ """Returns MATLAB version from VersionInfo.xml file present at matlab_root_path
112
+
113
+ Args:
114
+ matlab_root_path (pathlib.Path): pathlib.Path to MATLAB root.
30
115
 
31
- if matlab_path is None:
116
+ Returns:
117
+ (str | None): Returns MATLAB version from VersionInfo.xml file.
118
+ """
119
+ if matlab_root_path is None:
32
120
  return None
33
121
 
34
- tree = ET.parse(matlab_path / "VersionInfo.xml")
122
+ version_info_file_path = Path(matlab_root_path) / constants.VERSION_INFO_FILE_NAME
123
+ if not version_info_file_path.exists():
124
+ return None
125
+
126
+ tree = ET.parse(version_info_file_path)
35
127
  root = tree.getroot()
36
- return root.find("release").text
128
+
129
+ matlab_version = root.find("release").text
130
+
131
+ # If the matlab on system PATH is a wrapper script, then it would not be possible to determine MATLAB root (inturn not being able to determine MATLAB version)
132
+ # unless MWI_CUSTOM_MATLAB_ROOT is set. Raising only a warning as the matlab version is only required for communicating with MHLM.
133
+ if not matlab_version:
134
+ logger.warning(
135
+ f"Could not determine MATLAB version from MATLAB root path: {matlab_root_path}. Set {mwi_env.get_env_name_custom_matlab_root()} to a valid MATLAB root path"
136
+ )
137
+
138
+ return matlab_version
37
139
 
38
140
 
39
141
  def get_ws_env_settings():
@@ -46,8 +148,22 @@ def get_ws_env_settings():
46
148
  def get_mwi_config_folder(dev=False):
47
149
  if dev:
48
150
  return get_test_temp_dir()
151
+
49
152
  else:
50
- return Path.home() / ".matlab" / "MWI"
153
+ config_folder_path = Path.home() / ".matlab" / "MWI"
154
+ # In multi-host environments, Path.home() can be the same for
155
+ # multiple hosts and can cause issues when different hosts launch
156
+ # matlab-proxy on the same port.
157
+ # Using hostname to be part of the path of the config folder would avoid collisions.
158
+ hostname = socket.gethostname()
159
+ if hostname:
160
+ config_folder_path = config_folder_path / "hosts" / hostname
161
+
162
+ logger.debug(
163
+ f"{'Hostname could not be determined. ' if not hostname else ''}Using the folder: {config_folder_path} for storing all matlab-proxy related session information"
164
+ )
165
+
166
+ return config_folder_path
51
167
 
52
168
 
53
169
  def get_mwi_logs_root_dir(dev=False):
@@ -58,7 +174,12 @@ def get_dev_settings(config):
58
174
  devel_file = Path(__file__).resolve().parent / "./devel.py"
59
175
  mwi_config_folder = get_mwi_config_folder(dev=True)
60
176
  ws_env, ws_env_suffix = get_ws_env_settings()
177
+ (
178
+ mwi_auth_token,
179
+ mwi_auth_token_hash,
180
+ ) = token_auth.generate_mwi_auth_token_and_hash().values()
61
181
  return {
182
+ "error": None,
62
183
  "matlab_path": Path(),
63
184
  "matlab_version": "R2020b",
64
185
  "matlab_cmd": [
@@ -70,7 +191,7 @@ def get_dev_settings(config):
70
191
  "create_xvfb_cmd": create_xvfb_cmd,
71
192
  "base_url": os.environ.get(mwi_env.get_env_name_base_url(), ""),
72
193
  "app_port": os.environ.get(mwi_env.get_env_name_app_port(), 8000),
73
- "host_interface": os.environ.get(mwi_env.get_env_name_app_host(), "127.0.0.1"),
194
+ "host_interface": "127.0.0.1",
74
195
  "mwapikey": str(uuid.uuid4()),
75
196
  "matlab_protocol": "http",
76
197
  "matlab_display": ":1",
@@ -82,14 +203,26 @@ def get_dev_settings(config):
82
203
  "mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
83
204
  "mwi_custom_http_headers": mwi.custom_http_headers.get(),
84
205
  "env_config": mwi.validators.validate_env_config(config),
206
+ "integration_name": "MATLAB Desktop",
85
207
  "ssl_context": None,
86
208
  "mwi_logs_root_dir": get_mwi_logs_root_dir(dev=True),
87
- "mwi_proxy_lock_file_name": "mwi_proxy.lock",
88
209
  "mw_context_tags": get_mw_context_tags(matlab_proxy.get_default_config_name()),
89
210
  "mwi_server_url": None,
90
- "mwi_auth_token": None,
91
- "mwi_is_mwi_token_auth_enabled": False,
92
- "mwi_auth_token_name": mwi_env.get_env_name_mwi_auth_token().lower(),
211
+ "mwi_is_token_auth_enabled": mwi_auth_token is not None,
212
+ "mwi_auth_status": False,
213
+ "mwi_auth_token": mwi_auth_token,
214
+ "mwi_auth_token_hash": mwi_auth_token_hash,
215
+ "mwi_auth_token_name_for_http": MWI_AUTH_TOKEN_NAME_FOR_HTTP,
216
+ "mwi_auth_token_name_for_env": mwi_env.get_env_name_mwi_auth_token().lower(),
217
+ "mwi_use_existing_license": mwi.validators.validate_use_existing_licensing(
218
+ os.getenv(mwi_env.get_env_name_mwi_use_existing_license(), "")
219
+ ),
220
+ "warnings": [],
221
+ "is_xvfb_available": False,
222
+ "is_windowmanager_available": False,
223
+ "mwi_idle_timeout": None,
224
+ "cookie_jar": _get_cookie_jar(),
225
+ "browser_title": session_name.get_browser_title("R2020b"),
93
226
  }
94
227
 
95
228
 
@@ -103,6 +236,13 @@ def get(config_name=matlab_proxy.get_default_config_name(), dev=False):
103
236
 
104
237
  Returns:
105
238
  Dict: Containing data on how to start MATLAB among other information.
239
+
240
+ Raises:
241
+ Initialization of settings is not exception safe.
242
+ Exceptions of Type UIVisibleFatalError are not propagated upwards, and are instead set in the error data member.
243
+ This will allow for the app to error out gracefully in the front end as well.
244
+
245
+ All other exceptions will propagate upwards and result in the app to shutdown.
106
246
  """
107
247
 
108
248
  if dev:
@@ -111,7 +251,6 @@ def get(config_name=matlab_proxy.get_default_config_name(), dev=False):
111
251
  # If running tests using Pytest, it will set environment variable TEST to true before running tests.
112
252
  # Will make test env specific changes before returning the settings.
113
253
  if mwi_env.is_testing_mode_enabled():
114
-
115
254
  # Set ready_delay value to 0 for faster fake MATLAB startup.
116
255
  ready_delay = ["--ready-delay", "0"]
117
256
  matlab_cmd = settings["matlab_cmd"]
@@ -125,77 +264,149 @@ def get(config_name=matlab_proxy.get_default_config_name(), dev=False):
125
264
  s.close()
126
265
 
127
266
  # Set NLM Connection string. Server will start using this connection string for licensing
128
- settings["nlm_conn_str"] = "abc@nlm"
129
-
130
- return settings
267
+ settings["nlm_conn_str"] = "123@nlm"
131
268
 
132
269
  else:
133
- matlab_startup_file = str(
134
- Path(__file__).resolve().parent / "matlab" / "startup.m"
135
- )
136
- matlab_path = get_matlab_path()
137
- ws_env, ws_env_suffix = get_ws_env_settings()
270
+ settings = {"error": None, "warnings": []}
271
+
272
+ # Initializing server settings separately allows us to return
273
+ # a minimal set of settings required to launch the server even if
274
+ # there is an exception thrown when creating the matlab specific settings.
275
+ settings.update(get_server_settings(config_name))
138
276
 
139
- ssl_key_file, ssl_cert_file = mwi.validators.validate_ssl_key_and_cert_file(
140
- os.getenv(mwi_env.get_env_name_ssl_key_file(), None),
141
- os.getenv(mwi_env.get_env_name_ssl_cert_file(), None),
277
+ settings["is_xvfb_available"] = True if shutil.which("Xvfb") else False
278
+ settings["is_windowmanager_available"] = (
279
+ True if shutil.which("fluxbox") else False
142
280
  )
143
281
 
144
- mwi_auth_token = token_auth.generate_mwi_auth_token()
145
-
146
- # All config related to matlab-proxy will be saved to user's home folder.
147
- # This will allow for other user's to launch the integration from the same system
148
- # and not have their config's overwritten.
149
- mwi_config_folder = get_mwi_config_folder()
150
- return {
151
- "matlab_path": matlab_path,
152
- "matlab_version": get_matlab_version(matlab_path),
153
- "matlab_cmd": [
154
- "matlab",
155
- "-nosplash",
156
- "-nodesktop",
157
- "-softwareopengl",
158
- "-r",
159
- f"try; run('{matlab_startup_file}'); catch ME; disp(ME.message); end;",
160
- ],
161
- "create_xvfb_cmd": create_xvfb_cmd,
162
- "base_url": mwi.validators.validate_base_url(
163
- os.getenv(mwi_env.get_env_name_base_url(), "")
164
- ),
165
- "app_port": mwi.validators.validate_app_port_is_free(
166
- os.getenv(mwi_env.get_env_name_app_port())
167
- ),
168
- # Set default to host interface to 0.0.0.0
169
- "host_interface": os.environ.get(
170
- mwi_env.get_env_name_app_host(), "0.0.0.0"
171
- ),
172
- "mwapikey": str(uuid.uuid4()),
173
- "matlab_protocol": "https",
174
- "nlm_conn_str": mwi.validators.validate_mlm_license_file(
175
- os.environ.get(mwi_env.get_env_name_network_license_manager())
176
- ),
177
- "matlab_config_file": mwi_config_folder / "proxy_app_config.json",
178
- "ws_env": ws_env,
179
- "mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
180
- "mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
181
- "mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
182
- "mwi_custom_http_headers": mwi.custom_http_headers.get(),
183
- "env_config": mwi.validators.validate_env_config(config_name),
184
- "ssl_context": get_ssl_context(
185
- ssl_cert_file=ssl_cert_file, ssl_key_file=ssl_key_file
186
- ),
187
- # This directory will be used to store connector.securePort(matlab_ready_file) and its corresponding files. This will be
188
- # a central place to store logs of all the running instances of MATLAB launched by matlab-proxy
189
- "mwi_logs_root_dir": get_mwi_logs_root_dir(),
190
- # Name of the lock file which will be created by this instance of matlab-proxy process.
191
- "mwi_proxy_lock_file_name": "mwi_proxy.lock",
192
- "mw_context_tags": get_mw_context_tags(config_name),
193
- # The url where the matlab-proxy server is accessible at
194
- "mwi_server_url": None,
195
- "mwi_auth_token": mwi_auth_token,
196
- "mwi_is_mwi_token_auth_enabled": mwi_auth_token != None,
197
- "mwi_auth_token_name": mwi_env.get_env_name_mwi_auth_token().lower(),
198
- }
282
+ # Warn user if xvfb is not available on system path.
283
+ if system.is_linux():
284
+ if not settings["is_xvfb_available"]:
285
+ warning = " Unable to find Xvfb on the system PATH. Xvfb enables graphical abilities like plots and figures in the MATLAB desktop.\nConsider adding Xvfb to the system PATH and restart matlab-proxy.\nFor details, see https://github.com/mathworks/matlab-proxy#requirements."
286
+ logger.warning(warning)
287
+ settings["warnings"].append(warning)
288
+
289
+ if not settings["is_windowmanager_available"]:
290
+ warning = " Unable to find fluxbox on the system PATH. To use Simulink Online, add Fluxbox to the system PATH and restart matlab-proxy. For details, see https://github.com/mathworks/matlab-proxy#requirements."
291
+ logger.warning(warning)
292
+
293
+ settings.update(get_matlab_settings())
294
+
295
+ return settings
296
+
297
+
298
+ def get_server_settings(config_name):
299
+ """Get the settings required to launch the MATLAB-PROXY web server.
300
+
301
+ Args:
302
+ config : Dictionary as specified by the default_configuration.py file. Used to customize the app.
303
+ dev (bool, optional): development environment. Defaults to False.
304
+
305
+ Raises:
306
+ This function is not exception safe, and all exceptions will result in the termination of the app.
307
+ If you need to add exceptions which need to be presented in the UI, add them to get_matlab_settings
308
+ """
309
+ (
310
+ mwi_auth_token,
311
+ mwi_auth_token_hash,
312
+ ) = token_auth.generate_mwi_auth_token_and_hash().values()
313
+ mwi_config_folder = get_mwi_config_folder()
314
+
315
+ # log file validation check is already done in logger.py
316
+ mwi_log_file = os.getenv(mwi_env.get_env_name_log_file(), None)
317
+
318
+ env_config = mwi.validators.validate_env_config(config_name)
319
+ short_desc = env_config["extension_name_short_description"]
320
+ integration_name = (
321
+ short_desc
322
+ if env_config["extension_name"] == matlab_proxy.get_default_config_name()
323
+ else f"{short_desc} - MATLAB Integration"
324
+ )
325
+
326
+ cookie_jar = _get_cookie_jar()
327
+ return {
328
+ "create_xvfb_cmd": create_xvfb_cmd,
329
+ "base_url": mwi.validators.validate_base_url(
330
+ os.getenv(mwi_env.get_env_name_base_url(), "")
331
+ ),
332
+ # Set host interface to 0.0.0.0 to bind on all interfaces
333
+ "host_interface": "0.0.0.0",
334
+ # not_exception_safe, can_terminate_process by throwing FatalError
335
+ "app_port": mwi.validators.validate_app_port_is_free(
336
+ os.getenv(mwi_env.get_env_name_app_port())
337
+ ),
338
+ "env_config": env_config,
339
+ "integration_name": integration_name,
340
+ "mwapikey": str(uuid.uuid4()),
341
+ "matlab_protocol": "https",
342
+ "matlab_config_file": mwi_config_folder / "proxy_app_config.json",
343
+ "mwi_custom_http_headers": mwi.custom_http_headers.get(),
344
+ # This directory will be used to store connector.securePort(matlab_ready_file) and its corresponding files. This will be
345
+ # a central place to store logs of all the running instances of MATLAB launched by matlab-proxy
346
+ "mwi_logs_root_dir": get_mwi_logs_root_dir(),
347
+ "mwi_log_file": mwi_log_file,
348
+ "mw_context_tags": get_mw_context_tags(config_name),
349
+ # The url where the matlab-proxy server is accessible at
350
+ "mwi_server_url": None,
351
+ "mwi_is_token_auth_enabled": mwi_auth_token is not None,
352
+ "mwi_auth_status": False,
353
+ "mwi_auth_token": mwi_auth_token,
354
+ "mwi_auth_token_hash": mwi_auth_token_hash,
355
+ "mwi_auth_token_name_for_http": MWI_AUTH_TOKEN_NAME_FOR_HTTP,
356
+ "mwi_auth_token_name_for_env": mwi_env.get_env_name_mwi_auth_token().lower(),
357
+ "mwi_use_existing_license": mwi.validators.validate_use_existing_licensing(
358
+ os.getenv(mwi_env.get_env_name_mwi_use_existing_license(), "")
359
+ ),
360
+ "ssl_context": _validate_ssl_files_and_get_ssl_context(mwi_config_folder),
361
+ # validate_idle_timeout converts the timeout from minutes to seconds
362
+ "mwi_idle_timeout": mwi.validators.validate_idle_timeout(
363
+ os.getenv(mwi_env.get_env_name_shutdown_on_idle_timeout())
364
+ ),
365
+ "cookie_jar": cookie_jar,
366
+ }
367
+
368
+
369
+ def get_matlab_settings():
370
+ """Returns the settings required to start MATLAB.
371
+
372
+ Returns:
373
+ Dict: Containing data on how to start MATLAB among other information.
374
+ Raises:
375
+ This function is not exception safe, and all exceptions will result in the termination of the app.
376
+ Unless they are of type UIVisibleFatalError
377
+ """
378
+
379
+ ws_env, ws_env_suffix = get_ws_env_settings()
380
+ mw_licensing_urls = _get_mw_licensing_urls(ws_env_suffix)
381
+ nlm_conn_str = _get_nlm_conn_str()
382
+ has_custom_code_to_execute, code_to_execute = _get_matlab_code_to_execute()
383
+ err = None
384
+
385
+ try:
386
+ matlab_executable_path, matlab_root_path = get_matlab_executable_and_root_path()
387
+
388
+ except UIVisibleFatalError as error:
389
+ logger.error(f"Exception raised during initialization: {error}")
390
+ # Set matlab root and executable path to None as MATLAB root could not be determined
391
+ matlab_executable_path = matlab_root_path = None
392
+ err = error
393
+
394
+ matlab_version = get_matlab_version(matlab_root_path)
395
+ matlab_version_determined_on_startup = bool(matlab_version)
396
+ matlab_cmd = _get_matlab_cmd(matlab_executable_path, code_to_execute, nlm_conn_str)
397
+
398
+ return {
399
+ "error": err,
400
+ "matlab_version": matlab_version,
401
+ "matlab_path": matlab_root_path,
402
+ "matlab_version_determined_on_startup": matlab_version_determined_on_startup,
403
+ "matlab_cmd": matlab_cmd,
404
+ "ws_env": ws_env,
405
+ **mw_licensing_urls,
406
+ "nlm_conn_str": nlm_conn_str,
407
+ "has_custom_code_to_execute": has_custom_code_to_execute,
408
+ "browser_title": session_name.get_browser_title(matlab_version),
409
+ }
199
410
 
200
411
 
201
412
  def get_mw_context_tags(extension_name):
@@ -246,7 +457,7 @@ def create_xvfb_cmd():
246
457
  str(dpipe[1]),
247
458
  "-screen",
248
459
  "0",
249
- "1600x1200x24",
460
+ "3840x2160x24",
250
461
  "-dpi",
251
462
  "100",
252
463
  # "-ac",
@@ -267,30 +478,250 @@ def get_test_temp_dir():
267
478
  return test_temp_dir
268
479
 
269
480
 
270
- def get_ssl_context(ssl_cert_file, ssl_key_file):
271
- """Creates an SSL CONTEXT for use with the TCP Site"""
272
-
273
- # The certfile string must be the path to a single file in PEM format containing the
274
- # certificate as well as any number of CA certificates needed to establish the certificate’s authenticity.
275
- # The keyfile string, if present, must point to a file containing the private key in.
276
- # Otherwise the private key will be taken from certfile as well.
277
- import traceback
278
-
279
- if ssl_cert_file != None:
280
- try:
281
- ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
282
- ssl_context.load_cert_chain(ssl_cert_file, ssl_key_file)
283
- logger.debug(f"Using SSL certification!")
284
- except Exception as e:
285
- # Something was wrong with the certificates provided
286
- logger.error("SSL certificates provided are invalid. Aborting...")
287
- traceback.print_exc()
288
- logger.info("==== Fatal error : ===")
289
- print(e)
290
- # printing stack trace
291
- logger.info("======================")
292
- sys.exit(1)
293
- else:
481
+ def _validate_ssl_files_and_get_ssl_context(mwi_config_folder):
482
+ """Creates an SSL CONTEXT for use with the TCP Site.
483
+ The certfile string must be the path to a single file in PEM format containing the
484
+ certificate as well as any number of CA certificates needed to establish the certificate’s authenticity.
485
+ The keyfile string, if present, must point to a file containing the private key in.
486
+ Otherwise the private key will be taken from certfile as well.
487
+ """
488
+ is_self_signed_certificates = False
489
+ env_name_enable_ssl = mwi_env.get_env_name_enable_ssl()
490
+ is_ssl_enabled = mwi_env._is_env_set_to_true(env_name_enable_ssl)
491
+ env_name_ssl_key_file = mwi_env.get_env_name_ssl_key_file()
492
+ env_name_ssl_cert_file = mwi_env.get_env_name_ssl_cert_file()
493
+
494
+ ssl_key_file, ssl_cert_file = (
495
+ os.getenv(env_name_ssl_key_file, None),
496
+ os.getenv(env_name_ssl_cert_file, None),
497
+ )
498
+
499
+ # Don't use SSL if the user has explicitly disabled SSL communication or not set the respective env var
500
+ if not is_ssl_enabled:
501
+ if ssl_cert_file:
502
+ logger.warning(
503
+ f"Ignoring provided SSL files, as {env_name_enable_ssl} is either unset or set to false"
504
+ )
505
+ return None
506
+
507
+ # Validate that provided SSL files are valid files
508
+ ssl_key_file, ssl_cert_file = mwi.validators.validate_ssl_key_and_cert_file(
509
+ ssl_key_file, ssl_cert_file
510
+ )
511
+
512
+ if not ssl_cert_file and not ssl_key_file:
513
+ logger.debug("Using auto-generated self-signed certificates")
514
+
515
+ # certs dir under the MWI_CONFIG_FOLDER will hold the self-signed certificates
516
+ mwi_certs_dir = mwi_config_folder / "certs"
517
+ mwi_certs_dir.mkdir(parents=True, exist_ok=True)
518
+
519
+ # New certs are generated for every run leading to functionally reliable system, alternative is
520
+ # to check for existing certs and have error handling around expired/bad certs.
521
+ ssl_cert_file, ssl_key_file = generate_new_self_signed_certs(mwi_certs_dir)
522
+ is_self_signed_certificates = True
523
+ try:
524
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
525
+ ssl_context.load_cert_chain(ssl_cert_file, ssl_key_file)
526
+ logger.debug("Certificate chain was correctly loaded")
527
+ except Exception as e:
528
+ logger.error(f"Unable to load certificates. Error: {e}")
529
+
530
+ # Setting to None to use http mode in the event of failing to setup self-signed certificates
294
531
  ssl_context = None
295
532
 
533
+ # Raise a fatal error only in the event of an exception while loading customer-supplied ssl files
534
+ if not is_self_signed_certificates:
535
+ raise FatalError(e)
536
+
296
537
  return ssl_context
538
+
539
+
540
+ def generate_new_self_signed_certs(mwi_certs_dir):
541
+ """
542
+ Generates a new self-signed certificate and corresponding private key, saves them as PEM files in the specified directory.
543
+ The certificate is valid for 365 days from the time of creation.
544
+
545
+ Parameters:
546
+ - mwi_certs_dir (Path): A pathlib.Path object representing the directory where the certificate and key files will be saved.
547
+
548
+ Returns:
549
+ - tuple: A tuple containing the file paths (as strings) to the newly created certificate and private key PEM files.
550
+ The first element is the path to the certificate file (cert.pem), and the second is the path to the key file (key.pem).
551
+
552
+ Raises:
553
+ - FileNotFoundError: If the mwi_certs_dir does not exist.
554
+ - Any other exception that may occur during file writing or certificate generation.
555
+ """
556
+ cert_file = priv_key_file = None
557
+ try:
558
+ # Generate private key
559
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
560
+
561
+ # Self-signed certificate
562
+ subject = issuer = x509.Name(
563
+ [
564
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
565
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Massachusetts"),
566
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "Natick"),
567
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MathWorks Inc."),
568
+ x509.NameAttribute(NameOID.COMMON_NAME, "mathworks.com"),
569
+ ]
570
+ )
571
+ cert = (
572
+ x509.CertificateBuilder()
573
+ .subject_name(subject)
574
+ .issuer_name(issuer)
575
+ .public_key(private_key.public_key())
576
+ .serial_number(x509.random_serial_number())
577
+ .not_valid_before(datetime.datetime.utcnow())
578
+ .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
579
+ .sign(private_key, hashes.SHA256())
580
+ )
581
+
582
+ # Write private key to file
583
+ priv_key_file = mwi_certs_dir / "key.pem"
584
+ with open(priv_key_file, "wb") as f:
585
+ f.write(
586
+ private_key.private_bytes(
587
+ encoding=serialization.Encoding.PEM,
588
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
589
+ encryption_algorithm=serialization.NoEncryption(),
590
+ )
591
+ )
592
+
593
+ # Write certificate to file
594
+ cert_file = mwi_certs_dir / "cert.pem"
595
+ with open(cert_file, "wb") as f:
596
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
597
+
598
+ except Exception as ex:
599
+ logger.warning(
600
+ f"Failed to generate self-signed certificates, proceeding with non-secure mode! Error: {ex}"
601
+ )
602
+ cert_file = priv_key_file = None
603
+
604
+ return cert_file, priv_key_file
605
+
606
+
607
+ def _sanitize_file_path_for_matlab(filepath: str) -> str:
608
+ """
609
+ Replace single quotes in the filepath with double single quotes to preserve the quote when used in MATLAB code.
610
+ """
611
+ filepath_with_single_quotes_escaped = filepath.replace("'", "''")
612
+ return filepath_with_single_quotes_escaped
613
+
614
+
615
+ def _get_matlab_code_to_execute():
616
+ """Returns the code that needs to run on MATLAB startup.
617
+ Will check for user provided custom MATLAB code and execute it along with the default startup script.
618
+
619
+ Returns:
620
+ tuple: With the first value representing whether there is custom MATLAB code to execute, and the second value representing the MATLAB code to execute.
621
+ """
622
+ matlab_code_dir = Path(__file__).resolve().parent / "matlab"
623
+ matlab_startup_file = str(matlab_code_dir / "startup.m")
624
+ matlab_code_file = str(matlab_code_dir / "evaluateUserMatlabCode.m")
625
+
626
+ has_custom_code_to_execute = (
627
+ len(os.getenv(mwi_env.get_env_name_custom_matlab_code(), "").strip()) > 0
628
+ )
629
+
630
+ # Sanitize file paths to avoid MATLAB not running the script due to early breakup of character array.
631
+ mp_code_to_execute = f"try; run('{_sanitize_file_path_for_matlab(matlab_startup_file)}'); catch MATLABProxyInitializationError; disp(MATLABProxyInitializationError.message); end;"
632
+ custom_code_to_execute = f"try; run('{_sanitize_file_path_for_matlab(matlab_code_file)}'); catch MATLABCustomStartupCodeError; disp(MATLABCustomStartupCodeError.message); end;"
633
+ code_to_execute = (
634
+ mp_code_to_execute + custom_code_to_execute
635
+ if has_custom_code_to_execute
636
+ else mp_code_to_execute
637
+ )
638
+
639
+ return has_custom_code_to_execute, code_to_execute
640
+
641
+
642
+ def _get_nlm_conn_str():
643
+ """Get the Network License Manager (NLM) connection string.
644
+
645
+ Returns:
646
+ str: The NLM connection string provided by the MLM_LICENSE_FILE environment variable.
647
+ """
648
+ # NLM Connection String provided by MLM_LICENSE_FILE environment variable
649
+ nlm_conn_str = mwi.validators.validate_mlm_license_file(
650
+ os.environ.get(mwi_env.get_env_name_network_license_manager())
651
+ )
652
+
653
+ return nlm_conn_str
654
+
655
+
656
+ def _get_mw_licensing_urls(ws_env_suffix):
657
+ """Get the MathWorks licensing URLs.
658
+
659
+ Args:
660
+ ws_env_suffix (str): The environment suffix for the licensing URLs.
661
+
662
+ Returns:
663
+ dict: A dictionary containing the MathWorks licensing URLs for authentication and entitlement.
664
+ """
665
+ return {
666
+ "mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
667
+ "mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
668
+ "mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
669
+ }
670
+
671
+
672
+ def _get_matlab_cmd(matlab_executable_path, code_to_execute, nlm_conn_str):
673
+ """Construct the MATLAB command with appropriate flags and arguments.
674
+
675
+ Args:
676
+ matlab_executable_path (str): The path to the MATLAB executable.
677
+ code_to_execute (str): The MATLAB code to execute on startup.
678
+ nlm_conn_str (str): The Network License Manager connection string.
679
+
680
+ Returns:
681
+ list: A list of command-line arguments to launch MATLAB with the specified configuration.
682
+ """
683
+ if not matlab_executable_path:
684
+ return None
685
+
686
+ matlab_lic_mode = ["-licmode", "file"] if nlm_conn_str else ""
687
+
688
+ if mwi_env.Experimental.get_licmode_override():
689
+ matlab_lic_mode = ["-licmode", mwi_env.Experimental.get_licmode_override()]
690
+ logger.info(f"Using MATLAB license mode arguments: {matlab_lic_mode}")
691
+
692
+ # flag to hide MATLAB Window
693
+ flag_to_hide_desktop = ["-nodesktop"]
694
+ if system.is_windows():
695
+ flag_to_hide_desktop.extend(["-noDisplayDesktop", "-wait", "-log"])
696
+
697
+ profile_matlab_startup = (
698
+ "-timing" if mwi_env.Experimental.is_matlab_startup_profiling_enabled() else ""
699
+ )
700
+
701
+ return [
702
+ matlab_executable_path,
703
+ "-nosplash",
704
+ *flag_to_hide_desktop,
705
+ "-softwareopengl",
706
+ *matlab_lic_mode,
707
+ "-externalUI",
708
+ profile_matlab_startup,
709
+ "-r",
710
+ code_to_execute,
711
+ ]
712
+
713
+
714
+ def _get_cookie_jar():
715
+ """Returns an instance of HttpOnly cookie jar if MWI_USE_COOKIE_CACHE environment variable is set to True
716
+
717
+ Returns:
718
+ HttpOnlyCookieJar: An instance of HttpOnly cookie jar if MWI_USE_COOKIE_CACHE environment variable is set to True, otherwise None.
719
+ """
720
+ cookie_jar = None
721
+ if mwi_env.Experimental.should_use_cookie_cache():
722
+ logger.info(
723
+ f"Environment variable {mwi_env.Experimental.get_env_name_use_cookie_cache()} is set. matlab-proxy server will cache cookies from MATLAB"
724
+ )
725
+ cookie_jar = HttpOnlyCookieJar()
726
+
727
+ return cookie_jar