11# Copyright (c) 2020-2022 The MathWorks, Inc.
22
33import asyncio
4- import errno
54import json
65import logging
76import os
8- import socket
9- import sys
107import time
118from collections import deque
129from datetime import datetime , timedelta , timezone
1310
14- import aiohttp
15-
1611from matlab_proxy import util
1712from matlab_proxy .util import mw , mwi , system , windows
1813from matlab_proxy .util .mwi import environment_variables as mwi_env
2823 XvfbError ,
2924 log_error ,
3025)
26+ from matlab_proxy .constants import CONNECTOR_SECUREPORT_FILENAME
3127
3228logger = mwi .logger .get ()
3329
@@ -37,6 +33,10 @@ class AppState:
3733 This class handles state of MATLAB, MATLAB Licensing and Xvfb.
3834 """
3935
36+ # Constants that are applicable to AppState class
37+ MATLAB_PORT_CHECK_DELAY_IN_SECONDS = 1
38+ EMBEDDED_CONNECTOR_MAX_STARTUP_DURATION_IN_SECONDS = 120
39+
4040 def __init__ (self , settings ):
4141 """Parameterized constructor for the AppState class.
4242 Initializes member variables and checks for an existing MATLAB installation.
@@ -55,9 +55,6 @@ def __init__(self, settings):
5555
5656 # Dictionary of all files used to manage the MATLAB session.
5757 self .matlab_session_files = {
58- # The file created by this instance of matlab-proxy to signal to other matlab-proxy processes
59- # that this self.matlab_port will be used by this instance.
60- "mwi_proxy_lock_file" : None ,
6158 # The file created and written by MATLAB's Embedded connector to signal readiness.
6259 "matlab_ready_file" : None ,
6360 }
@@ -210,6 +207,9 @@ async def get_matlab_state(self):
210207 if matlab is None or not matlab .is_running ():
211208 return "down"
212209
210+ if not self .matlab_session_files ["matlab_ready_file" ].exists ():
211+ return "starting"
212+
213213 # If execution reaches this else block, it implies that:
214214 # 1) MATLAB process has started.
215215 # 2) Embedded connector has not started yet.
@@ -395,14 +395,8 @@ def persist_licensing(self):
395395 with open (cached_licensing_file , "w" ) as f :
396396 f .write (json .dumps (self .licensing ))
397397
398- def prepare_lock_files_for_MATLAB_launch (self ):
399- """Finds and reserves a free port for MATLAB Embedded Connector in the allowed range.
400- Creates the lock file to prevent any other matlab-proxy process to use the reserved port of this
401- process.
402-
403- Raises:
404- e: socket.error if the exception raised is other than port already occupied.
405- """
398+ def create_logs_dir_for_MATLAB (self ):
399+ """Creates the root folder where MATLAB writes the ready file and updates attibutes on self."""
406400
407401 # NOTE It is not guranteed that the port will remain free!
408402 # FIXME Because of https://github.com/http-party/node-http-proxy/issues/1342 the
@@ -414,77 +408,23 @@ def prepare_lock_files_for_MATLAB_launch(self):
414408 ):
415409 return 31515
416410 else :
417- # TODO If MATLAB Connector is enhanced to allow any port, then the
418- # following can be used to get an unused port instead of the for loop and
419- # try-except.
420- # s.bind(("", 0))
421- # self.matlab_port = s.getsockname()[1]
411+ mwi_logs_root_dir = self .settings ["mwi_logs_root_dir" ]
412+ # Use the app_port number to identify the server as that is user visible
413+ mwi_logs_dir = mwi_logs_root_dir / str (self .settings ["app_port" ])
422414
423- for port in mw .range_matlab_connector_ports ():
424- try :
425- s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
426- s .bind (("" , port ))
427-
428- mwi_logs_root_dir = self .settings ["mwi_logs_root_dir" ]
429-
430- # The mwi_proxy.lock file indicates to any other matlab-proxy processes
431- # that this self.matlab_port number is taken up by this process.
432- mwi_proxy_lock_file = mwi_logs_root_dir / (
433- self .settings ["mwi_proxy_lock_file_name" ] + "." + str (port )
434- )
415+ # Create a folder to hold the matlab_ready_file that will be created by MATLAB to signal readiness
416+ # This is the same folder to which MATLAB will write logs to.
417+ mwi_logs_dir .mkdir (parents = True , exist_ok = True )
435418
436- # Check if the mwi_proxy_lock_file exists.
437- # Implies there was a competing matlab-proxy process which found the same port before this process
438- if mwi_proxy_lock_file .exists ():
439- logger .debug (
440- f"Skipping port number { port } for MATLAB as lock file already exists at { mwi_proxy_lock_file } "
441- )
442- s .close ()
443-
444- else :
445- # Use the app_port number to identify the server as that is user visible
446- mwi_logs_dir = mwi_logs_root_dir / str (
447- self .settings ["app_port" ]
448- )
449-
450- # Create a folder to hold the matlab_ready_file that will be created by MATLAB to signal readiness.
451- # This is the same folder to which MATLAB will write logs to.
452- mwi_logs_dir .mkdir (parents = True , exist_ok = True )
453-
454- # Create the lock file first to minimize the critical section.
455- mwi_proxy_lock_file .touch ()
456- logger .info (
457- f"Communicating with MATLAB on port:{ port } , lock file: { mwi_proxy_lock_file } "
458- )
459-
460- # Created by MATLAB when it is ready to service requests.
461- matlab_ready_file = mwi_logs_dir / "connector.securePort"
462-
463- # Update member variables of AppState class
464- # Store the port number on which MATLAB will be launched for this matlab-proxy process.
465- self .matlab_port = port
466- self .mwi_logs_dir = mwi_logs_dir
467- self .matlab_session_files [
468- "mwi_proxy_lock_file"
469- ] = mwi_proxy_lock_file
470- self .matlab_session_files [
471- "matlab_ready_file"
472- ] = matlab_ready_file
473- s .close ()
474-
475- logger .debug (
476- f"matlab_session_files:{ self .matlab_session_files } "
477- )
478- return
419+ # Created by MATLAB when it is ready to service requests
420+ matlab_ready_file = mwi_logs_dir / CONNECTOR_SECUREPORT_FILENAME
479421
480- # For windows container's (when testing in github workflows) PermissionError and in linux, OSError is
481- # thrown when trying to bind a used port from a previous test instead of the expected socket.error
482- except (OSError , PermissionError ) as e :
483- pass
422+ # Update member variables of AppState class
423+ self .mwi_logs_dir = mwi_logs_dir
424+ self .matlab_session_files ["matlab_ready_file" ] = matlab_ready_file
484425
485- except socket .error as e :
486- if e .errno != errno .EADDRINUSE :
487- raise e
426+ logger .debug (f"matlab_session_files:{ self .matlab_session_files } " )
427+ return
488428
489429 def create_server_info_file (self ):
490430 mwi_logs_root_dir = self .settings ["mwi_logs_root_dir" ]
@@ -582,10 +522,6 @@ async def __setup_env_for_matlab(self) -> dict:
582522 # Adding DISPLAY key which is only available after starting Xvfb successfully.
583523 matlab_env ["DISPLAY" ] = self .settings ["matlab_display" ]
584524
585- # MW_CONNECTOR_SECURE_PORT and MATLAB_LOG_DIR keys to matlab_env as they are available after
586- # reserving port and preparing lockfiles for MATLAB
587- matlab_env ["MW_CONNECTOR_SECURE_PORT" ] = str (self .matlab_port )
588-
589525 # The matlab ready file is written into this location(self.mwi_logs_dir) by MATLAB
590526 # The mwi_logs_dir is where MATLAB will write any subsequent logs
591527 matlab_env ["MATLAB_LOG_DIR" ] = str (self .mwi_logs_dir )
@@ -726,28 +662,28 @@ async def start_matlab(self, restart_matlab=False):
726662 self .processes ["xvfb" ] = xvfb
727663
728664 try :
729- # Finds and reserves a free port, then prepare lock files for the MATLAB process.
730- self .prepare_lock_files_for_MATLAB_launch ()
665+ # Prepare ready file for the MATLAB process.
666+ self .create_logs_dir_for_MATLAB ()
731667
732668 # Configure the environment MATLAB needs to start
733669 matlab_env = await self .__setup_env_for_matlab ()
734670
735671 logger .debug (
736- "Prepared lock files and configured the environment for MATLAB startup"
672+ "Prepared ready file and configured the environment for MATLAB startup"
737673 )
738674
739- # If there's something wrong with setting up lock files or env setup for starting matlab, capture the error for logging
675+ # If there's something wrong with setting up files or env setup for starting matlab, capture the error for logging
740676 # and to pass to the front-end. Halt MATLAB process startup by returning early
741677 except Exception as err :
742678 self .error = err
743679 log_error (logger , err )
744680 # stop_matlab() does the teardown work by removing any residual files and processes created till now
745- # which is Xvfb process creation and preparing lock files for the MATLAB process.
681+ # which is Xvfb process creation and ready file for the MATLAB process.
746682 await self .stop_matlab ()
747683 return
748684
749685 # Start MATLAB Process
750- logger .debug (f "Starting MATLAB on port { self . matlab_port } " )
686+ logger .debug ("Starting MATLAB" )
751687
752688 matlab = await self .__start_matlab_process (matlab_env )
753689
@@ -830,9 +766,45 @@ async def matlab_stderr_reader():
830766 )
831767 await asyncio .sleep (1 )
832768
833- loop = util .get_event_loop ()
769+ async def update_matlab_port (delay : int ):
770+ """Task to populate matlab_port from the matlab ready file. Times out if max_duration is breached
771+
772+ Args:
773+ delay (int): time delay in seconds before retrying the file read operation
774+ """
775+ logger .debug (
776+ f'updating matlab_port information from { self .matlab_session_files ["matlab_ready_file" ]} '
777+ )
778+ try :
779+ await asyncio .wait_for (
780+ __read_matlab_ready_file (delay ),
781+ self .EMBEDDED_CONNECTOR_MAX_STARTUP_DURATION_IN_SECONDS ,
782+ )
783+ except asyncio .TimeoutError :
784+ logger .debug (
785+ "Timeout error received while updating matlab port, stopping matlab!"
786+ )
787+ await self .stop_matlab (force_quit = True )
788+ self .error = MatlabError (
789+ "Unable to start MATLAB because of a timeout. Try again by clicking Start MATLAB."
790+ )
791+
792+ async def __read_matlab_ready_file (delay ):
793+ # reads with delays from the file where connector has written its port information
794+ while not self .matlab_session_files ["matlab_ready_file" ].exists ():
795+ await asyncio .sleep (delay )
834796
797+ with open (self .matlab_session_files ["matlab_ready_file" ]) as f :
798+ self .matlab_port = int (f .read ())
799+ logger .debug (
800+ f"MATLAB Ready file successfully read, matlab_port set to: { self .matlab_port } "
801+ )
802+
803+ loop = util .get_event_loop ()
835804 self .tasks ["matlab_stderr_reader" ] = loop .create_task (matlab_stderr_reader ())
805+ self .tasks ["update_matlab_port" ] = loop .create_task (
806+ update_matlab_port (self .MATLAB_PORT_CHECK_DELAY_IN_SECONDS )
807+ )
836808
837809 """
838810 async def __send_terminate_integration_request(self):
@@ -1014,17 +986,18 @@ async def stop_matlab(self, force_quit=False):
1014986 for waiter in waiters :
1015987 await waiter
1016988
1017- stderr_reader_task = self . tasks . get ( "matlab_stderr_reader" )
1018- if stderr_reader_task is not None :
1019- try :
1020- stderr_reader_task . cancel ()
1021- await stderr_reader_task
1022- except asyncio . CancelledError :
1023- pass
1024-
1025- self . tasks . pop ( "matlab_stderr_reader " )
989+ # Canceling all the async tasks in the list
990+ for name , task in list ( self . tasks . items ()) :
991+ if task :
992+ try :
993+ task . cancel ()
994+ await task
995+ except asyncio . CancelledError :
996+ pass
997+ logger . debug ( f" { name } task stopped successfully " )
1026998
1027- logger .info ("matlab_stderr_reader() task: Stopped successfully." )
999+ # After stopping all the tasks, set self.tasks to empty dict
1000+ self .tasks = {}
10281001
10291002 # Clear logs if MATLAB stopped intentionally
10301003 logger .debug ("Clearing logs!" )
0 commit comments