Skip to content

Commit 9d3dab3

Browse files
krisctlPrabhakar Kumar
authored andcommitted
matlab-proxy derives port information for communication from spawned MATLAB process. Users will now see the MATLAB UI only after it has initialized fully.
1 parent 0f977c5 commit 9d3dab3

File tree

6 files changed

+162
-160
lines changed

6 files changed

+162
-160
lines changed

gui/src/components/Controls/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ MATLAB version: ${matlabVersion}%0D%0A`,
102102
onClick={() => callback(Confirmations.START)}
103103
disabled={!licensed || matlabStarting || matlabStopping || (authEnabled && !isAuthenticated)}
104104
data-for="control-button-tooltip"
105-
data-tip={`${matlabRunning ? 'Restart' : 'Start'} your MATLAB session`}
105+
data-tip={`${matlabRunning ? 'Restart' : 'Start'} MATLAB`}
106106
>
107107
<span className={`icon-custom-${matlabRunning ? 're' : ''}start`}></span>
108-
<span className='btn-label'>{`${matlabRunning ? 'Restart' : 'Start'} MATLAB Session`}</span>
108+
<span className='btn-label'>{`${matlabRunning ? 'Restart' : 'Start'} MATLAB`}</span>
109109
</button>
110110
<button
111111
id="stopMatlab"
@@ -114,10 +114,10 @@ MATLAB version: ${matlabVersion}%0D%0A`,
114114
onClick={() => callback(Confirmations.STOP)}
115115
disabled={!matlabRunning || (authEnabled && !isAuthenticated)}
116116
data-for="control-button-tooltip"
117-
data-tip="Stop your MATLAB session"
117+
data-tip="Stop MATLAB"
118118
>
119119
<span className='icon-custom-stop'></span>
120-
<span className='btn-label'>Stop MATLAB Session</span>
120+
<span className='btn-label'>Stop MATLAB</span>
121121
</button>
122122
<button
123123
id="unsetLicensing"

matlab_proxy/app_state.py

Lines changed: 77 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
# Copyright (c) 2020-2022 The MathWorks, Inc.
22

33
import asyncio
4-
import errno
54
import json
65
import logging
76
import os
8-
import socket
9-
import sys
107
import time
118
from collections import deque
129
from datetime import datetime, timedelta, timezone
1310

14-
import aiohttp
15-
1611
from matlab_proxy import util
1712
from matlab_proxy.util import mw, mwi, system, windows
1813
from matlab_proxy.util.mwi import environment_variables as mwi_env
@@ -28,6 +23,7 @@
2823
XvfbError,
2924
log_error,
3025
)
26+
from matlab_proxy.constants import CONNECTOR_SECUREPORT_FILENAME
3127

3228
logger = 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!")

matlab_proxy/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""This module defines project-level constants"""
2+
3+
CONNECTOR_SECUREPORT_FILENAME = "connector.securePort"

matlab_proxy/devel.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
from matlab_proxy import settings
1313
from matlab_proxy.util.mwi import environment_variables as mwi_env
14+
from matlab_proxy.constants import CONNECTOR_SECUREPORT_FILENAME
15+
from matlab_proxy.util.event_loop import *
1416

1517
desktop_html = b"""
1618
<h1>Fake MATLAB Web Desktop</h1>
@@ -27,23 +29,13 @@
2729
"""
2830

2931

30-
def wait_for_port(port):
31-
"""Waits for the given port to become available
32-
33-
Args:
34-
port (Integer): Port number to start fake matlab server.
35-
"""
36-
while True:
37-
print(f"Waiting for port {port} to be available")
38-
try:
39-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
40-
s.bind(("", port))
41-
except OSError:
42-
time.sleep(5)
43-
continue
44-
# Once successful, close the port and stop waiting
45-
s.close()
46-
break
32+
def assign_free_port():
33+
"""Finds an available free port"""
34+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
35+
s.bind(("", 0))
36+
port = s.getsockname()[1]
37+
s.close()
38+
return port
4739

4840

4941
async def web_handler(request):
@@ -176,10 +168,11 @@ async def fake_matlab_started(app):
176168

177169
# Real MATLAB always uses $MATLAB_LOG_DIR/connection.securePort as the ready file
178170
# We mock reading from the environment variable by calling the helper functions
179-
mwi_logs_dir = settings.get(dev=True)["mwi_logs_root_dir"] / str(app["port"])
180-
mwi_logs_dir.mkdir(parents=True, exist_ok=True)
171+
matlab_logs_dir = os.getenv(mwi_env.get_env_name_matlab_log_dir())
181172

182-
app["matlab_ready_file"] = mwi_logs_dir / "connector.securePort"
173+
app["matlab_ready_file"] = Path(
174+
f"{matlab_logs_dir}/{CONNECTOR_SECUREPORT_FILENAME}"
175+
)
183176

184177
ready_delay = app["ready_delay"]
185178
try:
@@ -188,6 +181,10 @@ async def fake_matlab_started(app):
188181
f"Creating fake MATLAB Embedded Connector ready file at {app['matlab_ready_file']}"
189182
)
190183
app["matlab_ready_file"].touch()
184+
185+
# Populate ready file with the embedded connector port information
186+
with open(app["matlab_ready_file"], "w") as f:
187+
f.write(str(app["port"]))
191188
except asyncio.CancelledError:
192189
pass
193190

@@ -223,8 +220,7 @@ def matlab(args):
223220
Args:
224221
args (Dict): Contains data on how to start web server.
225222
"""
226-
port = int(os.environ["MW_CONNECTOR_SECURE_PORT"])
227-
wait_for_port(port)
223+
port = assign_free_port()
228224
print(f"Serving fake MATLAB Embedded Connector at port {port}")
229225
app = web.Application()
230226
app["ready_delay"] = args.ready_delay

0 commit comments

Comments
 (0)