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.

Files changed (33) hide show
  1. matlab_proxy/app.py +54 -43
  2. matlab_proxy/app_state.py +370 -155
  3. matlab_proxy/constants.py +3 -0
  4. matlab_proxy/gui/asset-manifest.json +6 -6
  5. matlab_proxy/gui/index.html +1 -1
  6. matlab_proxy/gui/static/css/{main.47712126.css → main.da9c4eb8.css} +2 -2
  7. matlab_proxy/gui/static/css/main.da9c4eb8.css.map +1 -0
  8. matlab_proxy/gui/static/js/{main.5b5ca2f2.js → main.e07799e7.js} +3 -3
  9. matlab_proxy/gui/static/js/main.e07799e7.js.map +1 -0
  10. matlab_proxy/matlab/startup.m +0 -20
  11. matlab_proxy/settings.py +28 -3
  12. matlab_proxy/util/__init__.py +101 -1
  13. matlab_proxy/util/event_loop.py +28 -10
  14. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  15. matlab_proxy/util/mwi/embedded_connector/helpers.py +9 -0
  16. matlab_proxy/util/mwi/embedded_connector/request.py +51 -21
  17. matlab_proxy/util/mwi/environment_variables.py +6 -1
  18. matlab_proxy/util/mwi/exceptions.py +16 -1
  19. matlab_proxy/util/mwi/validators.py +33 -0
  20. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/METADATA +1 -1
  21. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/RECORD +31 -31
  22. tests/unit/test_app.py +45 -22
  23. tests/unit/test_app_state.py +404 -111
  24. tests/unit/test_constants.py +1 -0
  25. tests/unit/util/mwi/test_validators.py +30 -1
  26. tests/unit/util/test_util.py +83 -0
  27. matlab_proxy/gui/static/css/main.47712126.css.map +0 -1
  28. matlab_proxy/gui/static/js/main.5b5ca2f2.js.map +0 -1
  29. /matlab_proxy/gui/static/js/{main.5b5ca2f2.js.LICENSE.txt → main.e07799e7.js.LICENSE.txt} +0 -0
  30. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/LICENSE.md +0 -0
  31. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/WHEEL +0 -0
  32. {matlab_proxy-0.18.1.dist-info → matlab_proxy-0.19.0.dist-info}/entry_points.txt +0 -0
  33. {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
- self.tasks = {}
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
- # An event loop task to handle the detection of client activity
113
- self.task_detect_client_status = None
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 get_matlab_state(self):
253
- """Determine the state of MATLAB to be down/starting/up.
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
- # MATLAB can either be "up", "starting" or "down" state depending upon Xvfb, MATLAB and the Embedded Connector
259
- # Return matlab status as "down" if the processes validation fails
260
- if not self._are_required_processes_ready():
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
- # If execution reaches here, it implies that:
264
- # 1) MATLAB process has started.
265
- # 2) Embedded connector has not started yet.
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 _get_matlab_connector_status(self) -> str:
335
- """Returns the status of MATLABs Embedded Connector.
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
- Returns:
338
- str: Returns any of "up", "down" or "starting" indicating the status of Embedded Connector.
613
+ Args:
614
+ func_to_update_matlab_state(callable): Function which updates MATLAB and its 'busy' state.
339
615
  """
340
- if not self.matlab_session_files["matlab_ready_file"].exists():
341
- return "starting"
342
-
343
- # Proceed to query the Embedded Connector about its state.
344
- # matlab-proxy sends a request to itself to the endpoint: /messageservice/json/state
345
- # which the server redirects to the matlab_view() function to handle (which then sends the request to EC)
346
- # As the matlab_view is now a protected endpoint, we need to pass token information through headers.
347
-
348
- # Include token information into the headers if authentication is enabled.
349
- headers = self._get_token_auth_headers()
350
-
351
- embedded_connector_status = await mwi.embedded_connector.request.get_state(
352
- mwi_server_url=self.settings["mwi_server_url"],
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
- self.embedded_connector_state = embedded_connector_status
630
+ else:
631
+ await self.matlab_state_updater_lock.release()
632
+ return
363
633
 
364
- if self.embedded_connector_state == "down":
365
- # Even if the embedded connector's status is 'down', we return matlab status as
366
- # 'starting' because the MATLAB process itself has been created and matlab-proxy
367
- # is waiting for the embedded connector to start serving content.
368
- matlab_status = "starting"
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
- # Update time stamp when MATLAB state is "starting".
371
- if not self.embedded_connector_start_time:
372
- self.embedded_connector_start_time = time.time()
644
+ else:
645
+ await self.matlab_state_updater_lock.release()
646
+ return
373
647
 
374
- # Set matlab_status to "up" since embedded connector is up.
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
- return matlab_status
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:{matlab_env['DISPLAY']} for launching MATLAB"
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.tasks["matlab_stderr_reader_posix"] = loop.create_task(
1351
+ self.matlab_tasks["matlab_stderr_reader_posix"] = loop.create_task(
1076
1352
  self.__matlab_stderr_reader_posix()
1077
1353
  )
1078
- self.tasks["track_embedded_connector_state"] = loop.create_task(
1354
+ self.matlab_tasks["track_embedded_connector_state"] = loop.create_task(
1079
1355
  self.__track_embedded_connector_state()
1080
1356
  )
1081
- self.tasks["update_matlab_port"] = loop.create_task(
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
- # Fetch matlab state before deleting its session files because
1178
- # get_matlab_state() checks for the existence of these files in
1179
- # determining matlab state.
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 _, session_file in self.matlab_session_files.items():
1187
- if session_file is not None:
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:{session_file}")
1190
- session_file.unlink()
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
- # Canceling all the async tasks in the list
1285
- for name, task in list(self.tasks.items()):
1286
- if task:
1287
- try:
1288
- task.cancel()
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.tasks to empty dict
1295
- self.tasks = {}
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.task_detect_client_status:
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.task_detect_client_status = loop.create_task(
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
- if self.task_detect_client_status:
1405
- try:
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")])