matlab-proxy 0.18.1__py3-none-any.whl → 0.19.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 +54 -43
- matlab_proxy/app_state.py +370 -155
- matlab_proxy/constants.py +3 -0
- matlab_proxy/gui/asset-manifest.json +6 -6
- matlab_proxy/gui/index.html +1 -1
- matlab_proxy/gui/static/css/{main.47712126.css → main.da9c4eb8.css} +2 -2
- matlab_proxy/gui/static/css/main.da9c4eb8.css.map +1 -0
- matlab_proxy/gui/static/js/{main.5b5ca2f2.js → main.e07799e7.js} +3 -3
- matlab_proxy/gui/static/js/main.e07799e7.js.map +1 -0
- matlab_proxy/matlab/startup.m +0 -20
- matlab_proxy/settings.py +28 -3
- matlab_proxy/util/__init__.py +101 -1
- matlab_proxy/util/event_loop.py +28 -10
- matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
- matlab_proxy/util/mwi/embedded_connector/helpers.py +9 -0
- matlab_proxy/util/mwi/embedded_connector/request.py +51 -21
- matlab_proxy/util/mwi/environment_variables.py +6 -1
- matlab_proxy/util/mwi/exceptions.py +16 -1
- matlab_proxy/util/mwi/validators.py +33 -0
- {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/METADATA +1 -1
- {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/RECORD +31 -31
- tests/unit/test_app.py +45 -22
- tests/unit/test_app_state.py +404 -111
- tests/unit/test_constants.py +1 -0
- tests/unit/util/mwi/test_validators.py +30 -1
- tests/unit/util/test_util.py +83 -0
- matlab_proxy/gui/static/css/main.47712126.css.map +0 -1
- matlab_proxy/gui/static/js/main.5b5ca2f2.js.map +0 -1
- /matlab_proxy/gui/static/js/{main.5b5ca2f2.js.LICENSE.txt → main.e07799e7.js.LICENSE.txt} +0 -0
- {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/LICENSE.md +0 -0
- {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/WHEEL +0 -0
- {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/entry_points.txt +0 -0
- {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/top_level.txt +0 -0
matlab_proxy/app_state.py
CHANGED
|
@@ -5,22 +5,22 @@ import contextlib
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
|
+
import sys
|
|
8
9
|
import time
|
|
10
|
+
import uuid
|
|
9
11
|
from collections import deque
|
|
10
12
|
from datetime import datetime, timedelta, timezone
|
|
11
|
-
from typing import Final, Optional
|
|
12
|
-
import uuid
|
|
13
|
+
from typing import Final, Optional, Callable
|
|
13
14
|
|
|
14
15
|
from matlab_proxy import util
|
|
15
16
|
from matlab_proxy.constants import (
|
|
16
17
|
CONNECTOR_SECUREPORT_FILENAME,
|
|
17
|
-
MATLAB_LOGS_FILE_NAME,
|
|
18
18
|
IS_CONCURRENCY_CHECK_ENABLED,
|
|
19
|
+
MATLAB_LOGS_FILE_NAME,
|
|
19
20
|
USER_CODE_OUTPUT_FILE_NAME,
|
|
21
|
+
CHECK_MATLAB_STATUS_INTERVAL_SECONDS,
|
|
20
22
|
)
|
|
21
|
-
|
|
22
23
|
from matlab_proxy.settings import get_process_startup_timeout
|
|
23
|
-
|
|
24
24
|
from matlab_proxy.util import mw, mwi, system, windows
|
|
25
25
|
from matlab_proxy.util.mwi import environment_variables as mwi_env
|
|
26
26
|
from matlab_proxy.util.mwi import token_auth
|
|
@@ -33,6 +33,7 @@ from matlab_proxy.util.mwi.exceptions import (
|
|
|
33
33
|
OnlineLicensingError,
|
|
34
34
|
UIVisibleFatalError,
|
|
35
35
|
XvfbError,
|
|
36
|
+
LockAcquisitionError,
|
|
36
37
|
log_error,
|
|
37
38
|
)
|
|
38
39
|
|
|
@@ -79,7 +80,8 @@ class AppState:
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
self.licensing = None
|
|
82
|
-
|
|
83
|
+
# MATLAB process related tasks which have the same lifetime as MATLAB
|
|
84
|
+
self.matlab_tasks = {}
|
|
83
85
|
self.logs = {
|
|
84
86
|
"matlab": deque(maxlen=200),
|
|
85
87
|
}
|
|
@@ -109,8 +111,105 @@ class AppState:
|
|
|
109
111
|
# Used to detect whether the active client is actively sending out request or is inactive
|
|
110
112
|
self.active_client_request_detected = False
|
|
111
113
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
+
# Initialize matlab with 'down' state.
|
|
115
|
+
# Should only be updated/accessed via setter/getter methods.
|
|
116
|
+
self.__matlab_state = "down"
|
|
117
|
+
|
|
118
|
+
# Initialize busy state as None as matlab state is initialized as 'down'.
|
|
119
|
+
self.matlab_busy_state = None
|
|
120
|
+
|
|
121
|
+
# Lock to be used before modifying MATLAB state
|
|
122
|
+
self.matlab_state_updater_lock = util.TrackingLock(purpose="MATLAB state")
|
|
123
|
+
|
|
124
|
+
loop = util.get_event_loop()
|
|
125
|
+
|
|
126
|
+
# matlab-proxy server related tasks which have the same lifetime as the server
|
|
127
|
+
self.server_tasks = {
|
|
128
|
+
"update_matlab_state": loop.create_task(self.__update_matlab_state())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
self.is_idle_timeout_enabled = (
|
|
132
|
+
True if self.settings["mwi_idle_timeout"] else False
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if self.is_idle_timeout_enabled:
|
|
136
|
+
self.__initial_idle_timeout = self.__remaining_idle_timeout = self.settings[
|
|
137
|
+
"mwi_idle_timeout"
|
|
138
|
+
]
|
|
139
|
+
# Lock to be used before updating IDLE timer.
|
|
140
|
+
self.idle_timeout_lock = util.TrackingLock(purpose="MATLAB IDLE timer")
|
|
141
|
+
self.server_tasks["decrement_idle_timer"] = loop.create_task(
|
|
142
|
+
self.__decrement_idle_timer()
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def set_remaining_idle_timeout(self, new_timeout):
|
|
146
|
+
"""Sets the remaining IDLE timeout after the validating checks.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
new_timeout (int): New timeout value
|
|
150
|
+
"""
|
|
151
|
+
caller = util.get_caller_name()
|
|
152
|
+
if self.idle_timeout_lock.validate_lock_for_caller(caller):
|
|
153
|
+
self.__remaining_idle_timeout = new_timeout
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"'{util.get_caller_name()}()' function acquired the lock to update IDLE timer"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
# NOTE: This code branch should only ever be hit during development. We exit to enforce proper usage of this function during development time.
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
def get_remaining_idle_timeout(self):
|
|
163
|
+
"""Returns the remaining IDLE timeout after which matlab-proxy will shutdown
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
int: Remaining IDLE timeout
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
# Lock is not required when reading __idle_timeout_left as the value maybe atmost 1 second old.
|
|
170
|
+
# Additionally, having a lock for the getter will increase the latency for the /get_status requests coming in.
|
|
171
|
+
return self.__remaining_idle_timeout
|
|
172
|
+
|
|
173
|
+
async def reset_timer(self):
|
|
174
|
+
"""Resets the IDLE timer to its original value after acquiring a lock."""
|
|
175
|
+
await self.idle_timeout_lock.acquire()
|
|
176
|
+
self.set_remaining_idle_timeout(self.__initial_idle_timeout)
|
|
177
|
+
await self.idle_timeout_lock.release()
|
|
178
|
+
|
|
179
|
+
logger.debug(
|
|
180
|
+
f"IDLE timer has been reset to {self.get_remaining_idle_timeout()} seconds"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def __decrement_idle_timer(self):
|
|
184
|
+
"""Decrements the IDLE timer by 1 after acquiring a lock."""
|
|
185
|
+
this_task = "decrement_idle_timer"
|
|
186
|
+
logger.info(f"{this_task}: Starting task...")
|
|
187
|
+
|
|
188
|
+
while self.get_remaining_idle_timeout() > 0:
|
|
189
|
+
# If MATLAB is either starting, stopping or busy, reset the IDLE timer.
|
|
190
|
+
if (
|
|
191
|
+
self.get_matlab_state() in ["starting", "stopping"]
|
|
192
|
+
or self.matlab_busy_state == "busy"
|
|
193
|
+
):
|
|
194
|
+
await self.reset_timer()
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
new_value = self.get_remaining_idle_timeout() - 1
|
|
198
|
+
await self.idle_timeout_lock.acquire()
|
|
199
|
+
self.set_remaining_idle_timeout(new_value)
|
|
200
|
+
await self.idle_timeout_lock.release()
|
|
201
|
+
|
|
202
|
+
logger.debug(
|
|
203
|
+
f"{this_task}: IDLE timer decremented to {new_value} seconds"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
await asyncio.sleep(1)
|
|
207
|
+
|
|
208
|
+
logger.info("The IDLE timer for shutdown has run out...")
|
|
209
|
+
logger.info(f"Shutting down {self.settings['integration_name']}")
|
|
210
|
+
await self.stop_matlab()
|
|
211
|
+
loop = util.get_event_loop()
|
|
212
|
+
loop.stop()
|
|
114
213
|
|
|
115
214
|
def __get_cached_config_file(self):
|
|
116
215
|
"""Get the cached config file
|
|
@@ -249,25 +348,200 @@ class AppState:
|
|
|
249
348
|
except Exception as e:
|
|
250
349
|
self.__reset_and_delete_cached_config()
|
|
251
350
|
|
|
252
|
-
async def
|
|
253
|
-
"""
|
|
351
|
+
async def __update_matlab_state_based_on_connector_state(self):
|
|
352
|
+
"""Updates MATLAB state based on the Embedded Connector state.
|
|
353
|
+
This function is meant to be called after the required processes are ready.
|
|
354
|
+
"""
|
|
355
|
+
if self.embedded_connector_state == "down":
|
|
356
|
+
await self.matlab_state_updater_lock.acquire()
|
|
357
|
+
# Even if the embedded connector's status is 'down', we return matlab status as
|
|
358
|
+
# 'starting' because the MATLAB process itself has been created and matlab-proxy
|
|
359
|
+
# is waiting for the embedded connector to start serving content.
|
|
360
|
+
|
|
361
|
+
if (
|
|
362
|
+
self.embedded_connector_state == "down"
|
|
363
|
+
): # Double check EC state is down before invoking set_matlab_state().
|
|
364
|
+
self.set_matlab_state("starting")
|
|
365
|
+
|
|
366
|
+
# Update time stamp when MATLAB state is "starting".
|
|
367
|
+
if not self.embedded_connector_start_time:
|
|
368
|
+
self.embedded_connector_start_time = time.time()
|
|
369
|
+
|
|
370
|
+
# Set matlab_status to "up" since embedded connector is up.
|
|
371
|
+
else:
|
|
372
|
+
await self.matlab_state_updater_lock.acquire()
|
|
373
|
+
# Double check EC state is up before invoking set_matlab_state().
|
|
374
|
+
if self.embedded_connector_state == "up":
|
|
375
|
+
self.set_matlab_state("up")
|
|
376
|
+
await self.matlab_state_updater_lock.release()
|
|
377
|
+
|
|
378
|
+
async def __update_matlab_state_using_ping_endpoint(self) -> None:
|
|
379
|
+
"""Updates MATLAB and its busy state based on the response from PING endpoint"""
|
|
380
|
+
# matlab-proxy sends a request to itself to the endpoint: /messageservice/json/state
|
|
381
|
+
# which the server redirects to the matlab_view() function to handle (which then sends the request to EC)
|
|
382
|
+
headers = self._get_token_auth_headers()
|
|
383
|
+
self.embedded_connector_state = await mwi.embedded_connector.request.get_state(
|
|
384
|
+
mwi_server_url=self.settings["mwi_server_url"],
|
|
385
|
+
headers=headers,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
await self.__update_matlab_state_based_on_connector_state()
|
|
389
|
+
|
|
390
|
+
# When using the 'ping' endpoint its not possible to determine the busy status
|
|
391
|
+
# of MATLAB, so default to busy until the switch is made to use the 'busy' status endpoint in __update_matlab_state task.
|
|
392
|
+
# If EC is down, set MATLAB busy status to None
|
|
393
|
+
self.matlab_busy_state = (
|
|
394
|
+
"busy" if self.embedded_connector_state == "up" else None
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
async def __update_matlab_state_using_busy_status_endpoint(self) -> None:
|
|
398
|
+
"""Updates MATLAB and its busy state based on the response from 'ping' endpoint"""
|
|
399
|
+
# matlab-proxy sends a request to itself to the endpoint: /messageservice/json/state
|
|
400
|
+
# which the server redirects to the matlab_view() function to handle (which then sends the request to EC)
|
|
401
|
+
headers = self._get_token_auth_headers()
|
|
402
|
+
self.matlab_busy_state = await mwi.embedded_connector.request.get_busy_state(
|
|
403
|
+
mwi_server_url=self.settings["mwi_server_url"],
|
|
404
|
+
headers=headers,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
self.embedded_connector_state = "down" if not self.matlab_busy_state else "up"
|
|
408
|
+
await self.__update_matlab_state_based_on_connector_state()
|
|
409
|
+
|
|
410
|
+
async def __update_matlab_state_based_on_endpoint_to_use(
|
|
411
|
+
self, matlab_endpoint_to_use: Callable[[], None]
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Updates MATLAB state based on:
|
|
414
|
+
1) If the required processes are ready
|
|
415
|
+
2) The response from Embedded connector.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
matlab_endpoint_to_use (Callable): Function reference used to updated MATLAB and its busy status.
|
|
419
|
+
"""
|
|
420
|
+
this_task = "update_matlab_state"
|
|
421
|
+
# First check before acquiring the lock.
|
|
422
|
+
if not self._are_required_processes_ready():
|
|
423
|
+
await self.matlab_state_updater_lock.acquire()
|
|
424
|
+
if (
|
|
425
|
+
not self._are_required_processes_ready()
|
|
426
|
+
): # Double check required processes are not ready before invoking set_matlab_state()
|
|
427
|
+
self.set_matlab_state("down")
|
|
428
|
+
logger.debug(f"{this_task}: Required processes are not ready yet")
|
|
429
|
+
await self.matlab_state_updater_lock.release()
|
|
430
|
+
# Double-checked locking: https://en.wikipedia.org/wiki/Double-checked_locking
|
|
431
|
+
# If the lock is acquired inside the if condition (without a second check), it would lead to intermediate states
|
|
432
|
+
# 'starting'(set by start_matlab) -> 'down' (set in the if condition above) -> 'up' (set by this function after matlab starts)
|
|
433
|
+
|
|
434
|
+
# If lock is acquired before the if condition, it would lead to large sections of code being under lock which in this case would be
|
|
435
|
+
# this entire function (and the functions calls within it).
|
|
436
|
+
|
|
437
|
+
else:
|
|
438
|
+
await self.matlab_state_updater_lock.release()
|
|
439
|
+
await self._update_matlab_state_based_on_ready_file_and_connector_status(
|
|
440
|
+
matlab_endpoint_to_use
|
|
441
|
+
)
|
|
442
|
+
logger.debug(
|
|
443
|
+
f"{this_task}: Required processes are ready, Embedded Connector status is '{self.get_matlab_state()}'"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
else:
|
|
447
|
+
await self._update_matlab_state_based_on_ready_file_and_connector_status(
|
|
448
|
+
matlab_endpoint_to_use
|
|
449
|
+
)
|
|
450
|
+
logger.debug(
|
|
451
|
+
f"{this_task}: Required processes are ready, Embedded Connector status is '{self.get_matlab_state()}'"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
await asyncio.sleep(CHECK_MATLAB_STATUS_INTERVAL_SECONDS)
|
|
455
|
+
|
|
456
|
+
async def __update_matlab_state(self) -> None:
|
|
457
|
+
"""An indefinitely running asyncio task which determines the status of MATLAB to be down/starting/up."""
|
|
458
|
+
this_task = "update_matlab_state"
|
|
459
|
+
logger.info(f"{this_task}: Starting task...")
|
|
460
|
+
|
|
461
|
+
# Start with using the ping endpoint to update matlab and its 'busy' state.
|
|
462
|
+
function_to_call = self.__update_matlab_state_using_ping_endpoint
|
|
463
|
+
logger.debug("Using the 'ping' endpoint to determine MATLAB state")
|
|
464
|
+
|
|
465
|
+
while True:
|
|
466
|
+
await self.__update_matlab_state_based_on_endpoint_to_use(function_to_call)
|
|
467
|
+
|
|
468
|
+
if self.get_matlab_state() == "up":
|
|
469
|
+
logger.debug(
|
|
470
|
+
"MATLAB is up. Checking if 'busy' status endpoint is available"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# MATLAB is up, now switch to 'busy' status endpoint to check if 'busy' status updates
|
|
474
|
+
# to a valid value.
|
|
475
|
+
function_to_call = self.__update_matlab_state_using_busy_status_endpoint
|
|
476
|
+
await self.__update_matlab_state_based_on_endpoint_to_use(
|
|
477
|
+
function_to_call
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# If MATLAB 'busy' status is None even after MATLAB state is 'up', implies that the
|
|
481
|
+
# endpoint is not available. So, fall back to using ping endpoint.
|
|
482
|
+
if not self.matlab_busy_state:
|
|
483
|
+
function_to_call = self.__update_matlab_state_using_ping_endpoint
|
|
484
|
+
logger.debug(
|
|
485
|
+
"'busy' status endpoint returned an invalid response, falling back to using 'ping' endpoint to determine MATLAB state"
|
|
486
|
+
)
|
|
487
|
+
warning = f"{mwi_env.get_env_name_shutdown_on_idle_timeout()} environment variable is supported only for MATLAB versions R2021a or later"
|
|
488
|
+
logger.warn(warning)
|
|
489
|
+
self.warnings.append(warning)
|
|
490
|
+
|
|
491
|
+
else:
|
|
492
|
+
logger.debug(
|
|
493
|
+
"'busy' status endpoint returned a valid response, will continue using it for determining MATLAB and its 'busy' state"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
break
|
|
497
|
+
|
|
498
|
+
# Continue to use the same endpoint determined above.
|
|
499
|
+
while True:
|
|
500
|
+
await self.__update_matlab_state_based_on_endpoint_to_use(function_to_call)
|
|
501
|
+
|
|
502
|
+
def set_matlab_state(self, new_state) -> None:
|
|
503
|
+
"""Updates MATLAB state. Will exit the matlab-proxy process if a lock is not acquired
|
|
504
|
+
before calling this function.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
new_state (str): The new state of MATLAB
|
|
508
|
+
"""
|
|
509
|
+
caller = util.get_caller_name()
|
|
510
|
+
if self.matlab_state_updater_lock.validate_lock_for_caller(caller):
|
|
511
|
+
self.__matlab_state = new_state
|
|
512
|
+
logger.debug(f"'{caller}()' function updated MATLAB state to '{new_state}'")
|
|
513
|
+
|
|
514
|
+
else:
|
|
515
|
+
# NOTE: This code branch should only ever be hit during development. We exit to enforce proper usage of this function during development time.
|
|
516
|
+
sys.exit(1)
|
|
517
|
+
|
|
518
|
+
def get_matlab_state(self) -> str:
|
|
519
|
+
"""Returns the state of MATLAB to be down/starting/up.
|
|
254
520
|
|
|
255
521
|
Returns:
|
|
256
522
|
String: Status of MATLAB. Returns either up, down or starting.
|
|
257
523
|
"""
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
return "down"
|
|
524
|
+
# Lock is not required when reading __matlab_state as the value maybe atmost 1 second old.
|
|
525
|
+
# Additionally, having a lock for the getter will increase the latency for the /get_status requests coming in.
|
|
526
|
+
return self.__matlab_state
|
|
262
527
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return await self._get_matlab_connector_status()
|
|
528
|
+
async def stop_server_tasks(self):
|
|
529
|
+
"""Stops all matlab-proxy server tasks"""
|
|
530
|
+
await util.cancel_tasks(self.server_tasks)
|
|
267
531
|
|
|
268
532
|
def _are_required_processes_ready(
|
|
269
533
|
self, matlab_process=None, xvfb_process=None
|
|
270
534
|
) -> bool:
|
|
535
|
+
"""Checks if the required platform specific processes are ready.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
matlab_process (asyncio.subprocess.Process | psutil.Process, optional): MATLAB process. Defaults to None.
|
|
539
|
+
xvfb_process (asyncio.subprocess.Process, optional): Xvfb Process. Defaults to None.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
bool: Whether the required processes are ready or not.
|
|
543
|
+
"""
|
|
544
|
+
|
|
271
545
|
# Update the processes to what is tracked in the instance's processes if a None is received
|
|
272
546
|
if matlab_process is None:
|
|
273
547
|
matlab_process = self.processes["matlab"]
|
|
@@ -331,51 +605,49 @@ class AppState:
|
|
|
331
605
|
else None
|
|
332
606
|
)
|
|
333
607
|
|
|
334
|
-
async def
|
|
335
|
-
|
|
608
|
+
async def _update_matlab_state_based_on_ready_file_and_connector_status(
|
|
609
|
+
self, func_to_update_matlab_state: Callable[[], None]
|
|
610
|
+
) -> None:
|
|
611
|
+
"""Updates MATLAB and its 'busy' state based on Embedded Connector status.
|
|
336
612
|
|
|
337
|
-
|
|
338
|
-
|
|
613
|
+
Args:
|
|
614
|
+
func_to_update_matlab_state(callable): Function which updates MATLAB and its 'busy' state.
|
|
339
615
|
"""
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
headers=headers,
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
# Embedded Connector can be in either "up" or "down" state
|
|
357
|
-
assert embedded_connector_status in [
|
|
358
|
-
"up",
|
|
359
|
-
"down",
|
|
360
|
-
], "Invalid embedded connector state returned"
|
|
616
|
+
# NOTE: Double-checked locking should be applied where set_matlab_state() is called within this function,
|
|
617
|
+
# as it is invoked frequently (from the __update_matlab_state or anywhere set_matlab_state() is invoked frequently)
|
|
618
|
+
matlab_ready_file = self.matlab_session_files.get("matlab_ready_file")
|
|
619
|
+
|
|
620
|
+
if not matlab_ready_file:
|
|
621
|
+
await self.matlab_state_updater_lock.acquire()
|
|
622
|
+
|
|
623
|
+
if (
|
|
624
|
+
not matlab_ready_file
|
|
625
|
+
): # Double check that matlab_ready_file is truthy before invoking set_matlab_state()
|
|
626
|
+
self.set_matlab_state("down")
|
|
627
|
+
await self.matlab_state_updater_lock.release()
|
|
628
|
+
return
|
|
361
629
|
|
|
362
|
-
|
|
630
|
+
else:
|
|
631
|
+
await self.matlab_state_updater_lock.release()
|
|
632
|
+
return
|
|
363
633
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
634
|
+
# If the matlab_ready_file path is constructed and is not yet created by the embedded connector.
|
|
635
|
+
if matlab_ready_file and not matlab_ready_file.exists():
|
|
636
|
+
await self.matlab_state_updater_lock.acquire()
|
|
637
|
+
if (
|
|
638
|
+
matlab_ready_file and not matlab_ready_file.exists()
|
|
639
|
+
): # Double check that matlab_ready_file is truthy and exists before invoking set_matlab_state()
|
|
640
|
+
self.set_matlab_state("starting")
|
|
641
|
+
await self.matlab_state_updater_lock.release()
|
|
642
|
+
return
|
|
369
643
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
644
|
+
else:
|
|
645
|
+
await self.matlab_state_updater_lock.release()
|
|
646
|
+
return
|
|
373
647
|
|
|
374
|
-
#
|
|
375
|
-
else:
|
|
376
|
-
matlab_status = "up"
|
|
648
|
+
# Proceed to query the Embedded Connector about its state and update MATLAB and its 'busy' state.
|
|
377
649
|
|
|
378
|
-
|
|
650
|
+
await func_to_update_matlab_state()
|
|
379
651
|
|
|
380
652
|
async def set_licensing_nlm(self, conn_str):
|
|
381
653
|
"""Set the licensing type to NLM and the connection string."""
|
|
@@ -675,12 +947,6 @@ class AppState:
|
|
|
675
947
|
matlab_env["MLM_WEB_LICENSE"] = "true"
|
|
676
948
|
matlab_env["MLM_WEB_USER_CRED"] = access_token_data["token"]
|
|
677
949
|
matlab_env["MLM_WEB_ID"] = self.licensing["entitlement_id"]
|
|
678
|
-
matlab_env["MW_LOGIN_EMAIL_ADDRESS"] = self.licensing["email_addr"]
|
|
679
|
-
matlab_env["MW_LOGIN_FIRST_NAME"] = self.licensing["first_name"]
|
|
680
|
-
matlab_env["MW_LOGIN_LAST_NAME"] = self.licensing["last_name"]
|
|
681
|
-
matlab_env["MW_LOGIN_DISPLAY_NAME"] = self.licensing["display_name"]
|
|
682
|
-
matlab_env["MW_LOGIN_USER_ID"] = self.licensing["user_id"]
|
|
683
|
-
matlab_env["MW_LOGIN_PROFILE_ID"] = self.licensing["profile_id"]
|
|
684
950
|
|
|
685
951
|
matlab_env["MHLM_CONTEXT"] = (
|
|
686
952
|
"MATLAB_JAVASCRIPT_DESKTOP"
|
|
@@ -714,7 +980,7 @@ class AppState:
|
|
|
714
980
|
if self.settings.get("matlab_display", None):
|
|
715
981
|
matlab_env["DISPLAY"] = self.settings["matlab_display"]
|
|
716
982
|
logger.info(
|
|
717
|
-
f"Using the display number supplied by Xvfb process
|
|
983
|
+
f"Using the display number supplied by Xvfb process'{matlab_env['DISPLAY']}' for launching MATLAB"
|
|
718
984
|
)
|
|
719
985
|
else:
|
|
720
986
|
if "DISPLAY" in matlab_env:
|
|
@@ -1015,10 +1281,15 @@ class AppState:
|
|
|
1015
1281
|
Args:
|
|
1016
1282
|
restart_matlab (bool, optional): Whether to restart MATLAB. Defaults to False.
|
|
1017
1283
|
"""
|
|
1018
|
-
|
|
1019
1284
|
# Ensure that previous processes are stopped
|
|
1020
1285
|
await self.stop_matlab()
|
|
1021
1286
|
|
|
1287
|
+
# Acquire lock before setting MATLAB state to 'starting'.
|
|
1288
|
+
|
|
1289
|
+
# The lock is held for a substantial part of this function's execution to prevent asynchronous updates
|
|
1290
|
+
# to MATLAB state by other functions/tasks until the lock is released, ensuring consistency. It's released early only in case of exceptions.
|
|
1291
|
+
await self.matlab_state_updater_lock.acquire()
|
|
1292
|
+
self.set_matlab_state("starting")
|
|
1022
1293
|
# Clear MATLAB errors and logging
|
|
1023
1294
|
self.error = None
|
|
1024
1295
|
self.logs["matlab"].clear()
|
|
@@ -1048,6 +1319,8 @@ class AppState:
|
|
|
1048
1319
|
# If there's something wrong with setting up files or env setup for starting matlab, capture the error for logging
|
|
1049
1320
|
# and to pass to the front-end. Halt MATLAB process startup by returning early
|
|
1050
1321
|
except Exception as err:
|
|
1322
|
+
# Release lock if an exception occurs as we are returning early and since it will be required by stop_matlab
|
|
1323
|
+
await self.matlab_state_updater_lock.release()
|
|
1051
1324
|
self.error = err
|
|
1052
1325
|
log_error(logger, err)
|
|
1053
1326
|
# stop_matlab() does the teardown work by removing any residual files and processes created till now
|
|
@@ -1060,6 +1333,9 @@ class AppState:
|
|
|
1060
1333
|
|
|
1061
1334
|
matlab = await self.__start_matlab_process(matlab_env)
|
|
1062
1335
|
|
|
1336
|
+
# Release the lock after MATLAB process has started.
|
|
1337
|
+
await self.matlab_state_updater_lock.release()
|
|
1338
|
+
|
|
1063
1339
|
# matlab variable would be None if creation of the process failed.
|
|
1064
1340
|
if matlab is None:
|
|
1065
1341
|
# call self.stop_matlab().This does the teardown work by removing any residual files and processes created till now.
|
|
@@ -1072,76 +1348,16 @@ class AppState:
|
|
|
1072
1348
|
|
|
1073
1349
|
loop = util.get_event_loop()
|
|
1074
1350
|
# Start all tasks relevant to MATLAB process
|
|
1075
|
-
self.
|
|
1351
|
+
self.matlab_tasks["matlab_stderr_reader_posix"] = loop.create_task(
|
|
1076
1352
|
self.__matlab_stderr_reader_posix()
|
|
1077
1353
|
)
|
|
1078
|
-
self.
|
|
1354
|
+
self.matlab_tasks["track_embedded_connector_state"] = loop.create_task(
|
|
1079
1355
|
self.__track_embedded_connector_state()
|
|
1080
1356
|
)
|
|
1081
|
-
self.
|
|
1357
|
+
self.matlab_tasks["update_matlab_port"] = loop.create_task(
|
|
1082
1358
|
self.__update_matlab_port(self.MATLAB_PORT_CHECK_DELAY_IN_SECONDS)
|
|
1083
1359
|
)
|
|
1084
1360
|
|
|
1085
|
-
"""
|
|
1086
|
-
async def __send_terminate_integration_request(self):
|
|
1087
|
-
Private method to programmatically shutdown the matlab-proxy server.
|
|
1088
|
-
Sends a HTTP request to the server to shut itself down gracefully.
|
|
1089
|
-
|
|
1090
|
-
# Clean up session files which determine various states of the server &/ MATLAB.
|
|
1091
|
-
# Do this first as stopping MATLAB/Xvfb take longer and may fail
|
|
1092
|
-
try:
|
|
1093
|
-
for session_file in self.matlab_session_files.items():
|
|
1094
|
-
if session_file[1] is not None:
|
|
1095
|
-
logger.info(f"Deleting:{session_file[1]}")
|
|
1096
|
-
session_file[1].unlink()
|
|
1097
|
-
except FileNotFoundError:
|
|
1098
|
-
# Files won't exist when stop_matlab is called for the first time.
|
|
1099
|
-
pass
|
|
1100
|
-
|
|
1101
|
-
# Cancel the asyncio task which reads MATLAB process' stderr
|
|
1102
|
-
if "matlab_stderr_reader" in self.tasks:
|
|
1103
|
-
try:
|
|
1104
|
-
self.tasks["matlab_stderr_reader"].cancel()
|
|
1105
|
-
except asyncio.CancelledError:
|
|
1106
|
-
pass
|
|
1107
|
-
If the request fails, the server process exits with error code 1.
|
|
1108
|
-
|
|
1109
|
-
url = self.settings["mwi_server_url"] + "/terminate_integration"
|
|
1110
|
-
|
|
1111
|
-
try:
|
|
1112
|
-
async with aiohttp.ClientSession() as client_session:
|
|
1113
|
-
async with client_session.delete(url) as res:
|
|
1114
|
-
pass
|
|
1115
|
-
|
|
1116
|
-
matlab = self.processes["matlab"]
|
|
1117
|
-
if matlab is not None and matlab.returncode is None:
|
|
1118
|
-
try:
|
|
1119
|
-
logger.info(
|
|
1120
|
-
f"Calling terminate on MATLAB process with PID: {matlab.pid}!"
|
|
1121
|
-
)
|
|
1122
|
-
matlab.terminate()
|
|
1123
|
-
await matlab.wait()
|
|
1124
|
-
except:
|
|
1125
|
-
logger.info(
|
|
1126
|
-
f"Exception occurred during termination of MATLAB process with PID: {matlab.pid}!"
|
|
1127
|
-
)
|
|
1128
|
-
pass
|
|
1129
|
-
|
|
1130
|
-
xvfb = self.processes["xvfb"]
|
|
1131
|
-
logger.debug(f"Attempting XVFB Termination Xvfb)")
|
|
1132
|
-
if xvfb is not None and xvfb.returncode is None:
|
|
1133
|
-
logger.info(f"Terminating Xvfb (PID={xvfb.pid})")
|
|
1134
|
-
xvfb.terminate()
|
|
1135
|
-
await xvfb.wait()
|
|
1136
|
-
|
|
1137
|
-
except aiohttp.client_exceptions.ServerDisconnectedError:
|
|
1138
|
-
logger.error("Server has already disconnected...")
|
|
1139
|
-
|
|
1140
|
-
# If even the terminate integration request fails, exit with error code 1.
|
|
1141
|
-
except Exception as err:
|
|
1142
|
-
logger.error("Failed to send terminate integration request:\n", err)
|
|
1143
|
-
sys.exit(1)"""
|
|
1144
|
-
|
|
1145
1361
|
async def __send_stop_request_to_matlab(self):
|
|
1146
1362
|
"""Private method to send a HTTP request to MATLAB to shutdown gracefully
|
|
1147
1363
|
|
|
@@ -1174,20 +1390,28 @@ class AppState:
|
|
|
1174
1390
|
async def stop_matlab(self, force_quit=False):
|
|
1175
1391
|
"""Terminate MATLAB."""
|
|
1176
1392
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
#
|
|
1180
|
-
matlab_state = await self.get_matlab_state()
|
|
1393
|
+
matlab_state = self.get_matlab_state()
|
|
1394
|
+
|
|
1395
|
+
# Acquire lock before setting MATLAB state to 'stopping'.
|
|
1181
1396
|
|
|
1397
|
+
# The lock is held for a substantial part of this function's execution to prevent asynchronous updates
|
|
1398
|
+
# to MATLAB state by other functions/tasks until the lock is released, ensuring consistency. It's released early only in case of exceptions.
|
|
1399
|
+
await self.matlab_state_updater_lock.acquire()
|
|
1400
|
+
|
|
1401
|
+
self.set_matlab_state("stopping")
|
|
1182
1402
|
# Clean up session files which determine various states of the server &/ MATLAB.
|
|
1183
1403
|
# Do this first as stopping MATLAB/Xvfb takes longer and may fail
|
|
1184
1404
|
|
|
1185
1405
|
# Files won't exist when stop_matlab is called for the first time.
|
|
1186
|
-
for
|
|
1187
|
-
|
|
1406
|
+
for (
|
|
1407
|
+
session_file_name,
|
|
1408
|
+
session_file_path,
|
|
1409
|
+
) in self.matlab_session_files.items():
|
|
1410
|
+
if session_file_path is not None:
|
|
1411
|
+
self.matlab_session_files[session_file_name] = None
|
|
1188
1412
|
with contextlib.suppress(FileNotFoundError):
|
|
1189
|
-
logger.info(f"Deleting:{
|
|
1190
|
-
|
|
1413
|
+
logger.info(f"Deleting:{session_file_path}")
|
|
1414
|
+
session_file_path.unlink()
|
|
1191
1415
|
|
|
1192
1416
|
# In posix systems, variable matlab is an instance of asyncio.subprocess.Process()
|
|
1193
1417
|
# In windows systems, variable matlab is an instance of psutil.Process()
|
|
@@ -1281,18 +1505,14 @@ class AppState:
|
|
|
1281
1505
|
for waiter in waiters:
|
|
1282
1506
|
await waiter
|
|
1283
1507
|
|
|
1284
|
-
#
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
await task
|
|
1290
|
-
logger.debug(f"{name} task stopped successfully")
|
|
1291
|
-
except asyncio.CancelledError:
|
|
1292
|
-
pass
|
|
1508
|
+
# Release lock for the __update_matlab_state task to determine MATLAB state.
|
|
1509
|
+
await self.matlab_state_updater_lock.release()
|
|
1510
|
+
|
|
1511
|
+
# Canceling all MATLAB process related tasks
|
|
1512
|
+
await util.cancel_tasks(self.matlab_tasks)
|
|
1293
1513
|
|
|
1294
|
-
# After stopping all the tasks, set self.
|
|
1295
|
-
self.
|
|
1514
|
+
# After stopping all the tasks, set self.matlab_tasks to empty dict
|
|
1515
|
+
self.matlab_tasks = {}
|
|
1296
1516
|
|
|
1297
1517
|
# Clear logs if MATLAB stopped intentionally
|
|
1298
1518
|
logger.debug("Clearing logs!")
|
|
@@ -1366,10 +1586,10 @@ class AppState:
|
|
|
1366
1586
|
if not self.active_client or transfer_session:
|
|
1367
1587
|
self.active_client = client_id
|
|
1368
1588
|
|
|
1369
|
-
if not self.
|
|
1589
|
+
if not self.server_tasks.get("detect_client_status", None):
|
|
1370
1590
|
# Create the loop to detect the active status of the client
|
|
1371
1591
|
loop = util.get_event_loop()
|
|
1372
|
-
self.
|
|
1592
|
+
self.server_tasks["detect_client_status"] = loop.create_task(
|
|
1373
1593
|
self.detect_active_client_status()
|
|
1374
1594
|
)
|
|
1375
1595
|
|
|
@@ -1401,10 +1621,5 @@ class AppState:
|
|
|
1401
1621
|
# If no request is received from the active_client for more than 10 seconds then clear the active client id
|
|
1402
1622
|
inactive_count = 0
|
|
1403
1623
|
self.active_client = None
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
# Self cleanup of the task
|
|
1407
|
-
self.task_detect_client_status.cancel()
|
|
1408
|
-
self.task_detect_client_status = None
|
|
1409
|
-
except Exception as e:
|
|
1410
|
-
logger.error("Cleaning of task: 'detect_client_status' failed.")
|
|
1624
|
+
|
|
1625
|
+
await util.cancel_tasks([self.server_tasks.get("detect_client_status")])
|