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.
- matlab_proxy/app.py +22 -15
- matlab_proxy/gui/asset-manifest.json +6 -6
- matlab_proxy/gui/index.html +1 -1
- matlab_proxy/gui/static/css/main.6cd0caba.css +13 -0
- matlab_proxy/gui/static/css/main.6cd0caba.css.map +1 -0
- matlab_proxy/gui/static/js/main.61c661b8.js +3 -0
- matlab_proxy/gui/static/js/{main.e07799e7.js.LICENSE.txt → main.61c661b8.js.LICENSE.txt} +0 -2
- matlab_proxy/gui/static/js/main.61c661b8.js.map +1 -0
- matlab_proxy/settings.py +1 -1
- matlab_proxy/util/mwi/logger.py +22 -2
- matlab_proxy/util/windows.py +4 -1
- {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/METADATA +16 -15
- {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/RECORD +36 -19
- {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/WHEEL +1 -1
- {matlab_proxy-0.19.0.dist-info → matlab_proxy-0.21.0.dist-info}/entry_points.txt +1 -0
- matlab_proxy-0.21.0.dist-info/top_level.txt +3 -0
- matlab_proxy_manager/__init__.py +6 -0
- matlab_proxy_manager/lib/__init__.py +1 -0
- matlab_proxy_manager/lib/api.py +295 -0
- matlab_proxy_manager/storage/__init__.py +1 -0
- matlab_proxy_manager/storage/file_repository.py +144 -0
- matlab_proxy_manager/storage/interface.py +62 -0
- matlab_proxy_manager/storage/server.py +144 -0
- matlab_proxy_manager/utils/__init__.py +1 -0
- matlab_proxy_manager/utils/auth.py +77 -0
- matlab_proxy_manager/utils/constants.py +5 -0
- matlab_proxy_manager/utils/environment_variables.py +46 -0
- matlab_proxy_manager/utils/helpers.py +284 -0
- matlab_proxy_manager/utils/logger.py +73 -0
- matlab_proxy_manager/web/__init__.py +1 -0
- matlab_proxy_manager/web/app.py +447 -0
- matlab_proxy_manager/web/monitor.py +45 -0
- matlab_proxy_manager/web/watcher.py +54 -0
- tests/unit/test_app.py +1 -1
- tests/unit/util/mwi/test_logger.py +38 -4
- matlab_proxy/gui/static/css/main.da9c4eb8.css +0 -13
- matlab_proxy/gui/static/css/main.da9c4eb8.css.map +0 -1
- matlab_proxy/gui/static/js/main.e07799e7.js +0 -3
- matlab_proxy/gui/static/js/main.e07799e7.js.map +0 -1
- matlab_proxy-0.19.0.dist-info/top_level.txt +0 -2
- {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.
|