matlab-proxy 0.19.0__py3-none-any.whl → 0.21.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.

Files changed (41) hide show
  1. matlab_proxy/app.py +22 -15
  2. matlab_proxy/gui/asset-manifest.json +6 -6
  3. matlab_proxy/gui/index.html +1 -1
  4. matlab_proxy/gui/static/css/main.6cd0caba.css +13 -0
  5. matlab_proxy/gui/static/css/main.6cd0caba.css.map +1 -0
  6. matlab_proxy/gui/static/js/main.61c661b8.js +3 -0
  7. matlab_proxy/gui/static/js/{main.e07799e7.js.LICENSE.txt → main.61c661b8.js.LICENSE.txt} +0 -2
  8. matlab_proxy/gui/static/js/main.61c661b8.js.map +1 -0
  9. matlab_proxy/settings.py +1 -1
  10. matlab_proxy/util/mwi/logger.py +22 -2
  11. matlab_proxy/util/windows.py +4 -1
  12. {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/METADATA +16 -15
  13. {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/RECORD +36 -19
  14. {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/WHEEL +1 -1
  15. {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/entry_points.txt +1 -0
  16. matlab_proxy-0.21.0.dist-info/top_level.txt +3 -0
  17. matlab_proxy_manager/__init__.py +6 -0
  18. matlab_proxy_manager/lib/__init__.py +1 -0
  19. matlab_proxy_manager/lib/api.py +295 -0
  20. matlab_proxy_manager/storage/__init__.py +1 -0
  21. matlab_proxy_manager/storage/file_repository.py +144 -0
  22. matlab_proxy_manager/storage/interface.py +62 -0
  23. matlab_proxy_manager/storage/server.py +144 -0
  24. matlab_proxy_manager/utils/__init__.py +1 -0
  25. matlab_proxy_manager/utils/auth.py +77 -0
  26. matlab_proxy_manager/utils/constants.py +5 -0
  27. matlab_proxy_manager/utils/environment_variables.py +46 -0
  28. matlab_proxy_manager/utils/helpers.py +284 -0
  29. matlab_proxy_manager/utils/logger.py +73 -0
  30. matlab_proxy_manager/web/__init__.py +1 -0
  31. matlab_proxy_manager/web/app.py +447 -0
  32. matlab_proxy_manager/web/monitor.py +45 -0
  33. matlab_proxy_manager/web/watcher.py +54 -0
  34. tests/unit/test_app.py +1 -1
  35. tests/unit/util/mwi/test_logger.py +38 -4
  36. matlab_proxy/gui/static/css/main.da9c4eb8.css +0 -13
  37. matlab_proxy/gui/static/css/main.da9c4eb8.css.map +0 -1
  38. matlab_proxy/gui/static/js/main.e07799e7.js +0 -3
  39. matlab_proxy/gui/static/js/main.e07799e7.js.map +0 -1
  40. matlab_proxy-0.19.0.dist-info/top_level.txt +0 -2
  41. {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/LICENSE.md +0 -0
@@ -0,0 +1,284 @@
1
+ # Copyright 2024 The MathWorks, Inc.
2
+ import os
3
+ import socket
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ import psutil
9
+ import requests
10
+ from aiohttp import web
11
+ from requests.adapters import HTTPAdapter
12
+ from urllib3.util.retry import Retry
13
+
14
+ from matlab_proxy import settings
15
+ from matlab_proxy_manager.storage.file_repository import FileRepository
16
+ from matlab_proxy_manager.utils import logger
17
+
18
+ log = logger.get()
19
+
20
+
21
+ def is_server_ready(url: str, retries: int = 2) -> bool:
22
+ """
23
+ Check if the server at the given URL is ready.
24
+
25
+ Args:
26
+ url (str): The URL of the server.
27
+
28
+ Returns:
29
+ bool: True if the server is ready, False otherwise.
30
+ """
31
+ try:
32
+ matlab_proxy_index_page_identifier = "MWI_MATLAB_PROXY_IDENTIFIER"
33
+ resp = requests_retry_session(retries=retries).get(f"{url}", verify=False)
34
+ log.debug("Response status code from server readiness: %s", resp.status_code)
35
+ return (
36
+ resp.status_code == requests.codes.OK
37
+ and matlab_proxy_index_page_identifier in resp.text
38
+ )
39
+ except Exception as e:
40
+ log.debug("Couldn't reach the server with error: %s", e)
41
+ return False
42
+
43
+
44
+ def requests_retry_session(
45
+ retries=3, backoff_factor=0.1, session=None
46
+ ) -> requests.Session:
47
+ """
48
+ Create a requests session with retry logic.
49
+
50
+ Args:
51
+ retries (int): The number of retries.
52
+ backoff_factor (float): The backoff factor for retries.
53
+ session (requests.Session, optional): An existing requests session.
54
+
55
+ Returns:
56
+ requests.Session: The requests session with retry logic.
57
+ """
58
+ session = session or requests.session()
59
+ retry = Retry(
60
+ total=retries,
61
+ read=retries,
62
+ connect=retries,
63
+ backoff_factor=backoff_factor,
64
+ allowed_methods=frozenset(["DELETE", "GET", "POST"]),
65
+ )
66
+ adapter = HTTPAdapter(max_retries=retry)
67
+ session.mount("http://", adapter)
68
+ session.mount("https://", adapter)
69
+ return session
70
+
71
+
72
+ def does_process_exist(pid: int) -> bool:
73
+ """
74
+ Checks if the parent process is alive.
75
+
76
+ Returns:
77
+ bool: True if the parent process is alive, False otherwise.
78
+ """
79
+ parent_status = pid is not None and psutil.pid_exists(int(pid))
80
+ log.debug("Parent liveness check returned: %s", parent_status)
81
+ return parent_status
82
+
83
+
84
+ def convert_mwi_env_vars_to_header_format(
85
+ env_vars: Dict[str, str], prefix: str
86
+ ) -> Dict[str, str]:
87
+ """
88
+ Parse and transform environment variables with a specific prefix.
89
+
90
+ Args:
91
+ env_vars (dict): The environment variables.
92
+ prefix (str): The prefix to filter the environment variables.
93
+
94
+ Returns:
95
+ dict: The transformed environment variables.
96
+ """
97
+ return {
98
+ key.replace("_", "-"): value
99
+ for key, value in env_vars.items()
100
+ if key.startswith(prefix)
101
+ }
102
+
103
+
104
+ def create_and_get_proxy_manager_data_dir() -> Path:
105
+ """
106
+ Create and get the proxy manager data directory.
107
+
108
+ Returns:
109
+ Path: The path to the proxy manager data directory.
110
+ """
111
+
112
+ config_dir = settings.get_mwi_config_folder(dev=False)
113
+ data_dir = Path(config_dir, "proxy_manager")
114
+ Path.mkdir(data_dir, parents=True, exist_ok=True)
115
+ return data_dir
116
+
117
+
118
+ async def delete_dangling_servers(app: web.Application) -> None:
119
+ """
120
+ Delete dangling matlab proxy servers that are no longer alive.
121
+
122
+ Args:
123
+ app (web.Application): aiohttp web application
124
+ """
125
+ is_delete_successful = _are_orphaned_servers_deleted(None)
126
+ log.debug("Deleted dangling matlab proxy servers: %s", is_delete_successful)
127
+
128
+
129
+ def _are_orphaned_servers_deleted(predicate: str) -> bool:
130
+ """
131
+ Get all the files under the proxy manager directory, check the status of the servers,
132
+ and delete orphaned servers and their corresponding files.
133
+
134
+ - Checks if the parent PID of each server is still alive. If not, sends a SIGKILL
135
+ to the server and deletes the corresponding file.
136
+ - Checks if the servers in those files are still alive by sending GET requests to
137
+ their absolute URLs. If not, deletes the corresponding file.
138
+
139
+ Returns:
140
+ bool: True if any server was deleted, False otherwise.
141
+ """
142
+ data_dir = create_and_get_proxy_manager_data_dir()
143
+ storage = FileRepository(data_dir)
144
+ servers: dict = storage.get_all()
145
+
146
+ def _matches_predicate(filename: str) -> bool:
147
+ return filename.split("_")[0] == predicate
148
+
149
+ # Checks only a subset of servers (that matches the parent_pid of the caller)
150
+ # to reduce the MATLAB proxy startup time
151
+ if predicate:
152
+ servers = {
153
+ filename: server
154
+ for filename, server in servers.items()
155
+ if _matches_predicate(filename)
156
+ }
157
+ if not servers:
158
+ log.debug("Parent pid not matched, nothing to cleanup")
159
+ return True
160
+
161
+ return _delete_server_and_file(storage, servers)
162
+
163
+
164
+ def _delete_server_and_file(storage, servers):
165
+ is_server_deleted = False
166
+ for filename, server in servers.items():
167
+ if not server.is_server_alive():
168
+ log.debug("Server is not alive, cleaning up files")
169
+ try:
170
+ storage.delete(os.path.basename(filename))
171
+ except Exception as ex:
172
+ log.debug("Failed to delete file: %s", ex)
173
+ is_server_deleted = True
174
+ elif not does_process_exist(server.parent_pid):
175
+ log.debug("Server's parent is gone, shutting down matlab proxy")
176
+ try:
177
+ server.shutdown()
178
+ except Exception as ex:
179
+ log.debug("Failed to shutdown the matlab proxy server: %s", ex)
180
+ finally:
181
+ # Ensures files are cleaned up even if shutdown fails
182
+ storage.delete(os.path.basename(filename))
183
+ is_server_deleted = True
184
+
185
+ return is_server_deleted
186
+
187
+
188
+ def poll_for_server_deletion() -> None:
189
+ """
190
+ Poll for server deletion for a specified timeout period.
191
+
192
+ This function continuously checks if orphaned servers are deleted within a
193
+ specified timeout period. If servers are deleted, it breaks out of the loop.
194
+
195
+ Logs the status of server deletion attempts.
196
+ """
197
+ timeout_in_seconds: int = 2
198
+ log.info("Interrupt/termination signal caught, cleaning up resources")
199
+ start_time = time.time()
200
+
201
+ while time.time() - start_time < timeout_in_seconds:
202
+ is_server_deleted = _are_orphaned_servers_deleted(None)
203
+ if is_server_deleted:
204
+ log.debug("Servers deleted, breaking out of loop")
205
+ break
206
+ log.debug("Servers not deleted, waiting")
207
+ # Sleep for a short interval before polling again
208
+ time.sleep(0.5)
209
+
210
+
211
+ def find_free_port() -> str:
212
+ """
213
+ Find a free port on the system.
214
+
215
+ This function creates a socket, binds it to an available port, retrieves
216
+ the port number, and then closes the socket.
217
+
218
+ Returns:
219
+ str: The free port number as a string.
220
+ """
221
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
222
+ s.bind(("", 0))
223
+ port = str(s.getsockname()[1])
224
+ s.close()
225
+ return port
226
+
227
+
228
+ def pre_load_from_state_file(data_dir: str) -> Dict[str, str]:
229
+ """
230
+ Pre-load server states from the state files in the specified data directory.
231
+
232
+ Args:
233
+ data_dir (Path): The directory containing the state files.
234
+
235
+ Returns:
236
+ Dict[str, Dict]: A dictionary with server IDs as keys and server states as values.
237
+ """
238
+ storage = FileRepository(data_dir)
239
+ servers = storage.get_all()
240
+ return {server.id: server.as_dict() for server in servers.values()}
241
+
242
+
243
+ def is_only_reference(file_path: str) -> bool:
244
+ """
245
+ Check if the specified file is the only file in its directory.
246
+
247
+ Args:
248
+ file_path (str): The path to the file.
249
+
250
+ Returns:
251
+ bool: True if the file is the only file in its directory, False otherwise.
252
+ """
253
+ parent_dir = Path(file_path).parent.absolute()
254
+ files = os.listdir(parent_dir)
255
+ return len(files) == 1 and files[0] == os.path.basename(file_path)
256
+
257
+
258
+ def create_state_file(data_dir, server_process, filename: str):
259
+ """
260
+ Create a state file in the specified data directory.
261
+
262
+ This function creates a state file for the given server process in the specified
263
+ data directory. It logs the process and handles any exceptions that might occur.
264
+
265
+ Args:
266
+ data_dir: The directory where the state file will be created.
267
+ server_process (ServerProcess): The server process for which the state file is created.
268
+ filename (str): The name of the state file to be created.
269
+
270
+ Raises:
271
+ IOError: If there is an error creating the state file or adding the server
272
+ process to the repository.
273
+ """
274
+ try:
275
+ storage = FileRepository(data_dir)
276
+ storage.add(server=server_process, filename=filename)
277
+ log.debug("State file %s created in %s", filename, data_dir)
278
+ except Exception as e:
279
+ log.error(
280
+ "Failed to create state file %s in %s, error: %s", filename, data_dir, e
281
+ )
282
+ raise IOError(
283
+ f"Failed to create state file {filename} in {data_dir}, error: {e}"
284
+ ) from e
@@ -0,0 +1,73 @@
1
+ # Copyright 2024 The MathWorks, Inc.
2
+ # Helper functions to access & control the logging behavior of the app
3
+
4
+ import logging
5
+ import os
6
+
7
+
8
+ # TODO: Consolidate all logging setup into a common module (MATLABProxy, MATLABKernel, MATLABProxyManager)
9
+ def get(init=False):
10
+ """Get the logger used by this application.
11
+ Set init=True to initialize the logger
12
+ Returns:
13
+ Logger: The logger used by this application.
14
+ """
15
+ if init is True:
16
+ return __set_logging_configuration()
17
+
18
+ return __get_mw_logger()
19
+
20
+
21
+ def __get_mw_logger_name():
22
+ """Name of logger used by the app
23
+
24
+ Returns:
25
+ String: The name of the Logger.
26
+ """
27
+ return "MATLABProxyManager"
28
+
29
+
30
+ def __get_mw_logger():
31
+ """Returns logger for use in this app.
32
+
33
+ Returns:
34
+ Logger: A logger object
35
+ """
36
+ return logging.getLogger(__get_mw_logger_name())
37
+
38
+
39
+ def __set_logging_configuration():
40
+ """Sets the logging environment for the app
41
+
42
+ Returns:
43
+ Logger: Logger object with the set configuration.
44
+ """
45
+ # query for user specified environment variables
46
+ log_level = os.getenv(__get_env_name_logging_level(), __get_default_log_level())
47
+
48
+ ## Set logging object
49
+ logger = __get_mw_logger()
50
+
51
+ # log_level is either set by environment or is the default value.
52
+ logger.info("Initializing logger with log_level: %s", log_level)
53
+ logger.setLevel(log_level)
54
+
55
+ # Allow other libraries used by this integration to
56
+ # also print their logs at the specified level
57
+ logging.basicConfig(level=log_level)
58
+
59
+ return logger
60
+
61
+
62
+ def __get_env_name_logging_level():
63
+ """Specifies the logging level used by app's loggers"""
64
+ return "MWI_MPM_LOG_LEVEL"
65
+
66
+
67
+ def __get_default_log_level():
68
+ """The default logging level used by this application.
69
+
70
+ Returns:
71
+ String: The default logging level
72
+ """
73
+ return "INFO"
@@ -0,0 +1 @@
1
+ # Copyright 2024 The MathWorks, Inc.