Skip to content

Commit 789bbdd

Browse files
committed
Support for TMPDIR and Update to v0.3.2
1 parent 1bee67b commit 789bbdd

File tree

10 files changed

+120
-86
lines changed

10 files changed

+120
-86
lines changed

Advanced-Usage.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ The following table describes all the environment variables that you can set to
1818
| **MWI_BASE_URL** | string | `"/matlab"` | Set to control the base URL of the app. MWI_BASE_URL should start with `/` or be `empty`. |
1919
| **MWI_APP_PORT** | integer | `8080` | Specify the port for the HTTP server to listen on. |
2020
| **MWI_LOG_LEVEL** | string | `"CRITICAL"` | Specify the Python log level to be one of the following `NOTSET`, `DEBUG`, `INFO`, `WARN`, `ERROR`, or `CRITICAL`. For more information on Python log levels, see [Logging Levels](https://docs.python.org/3/library/logging.html#logging-levels) .<br />The default value is `INFO`. |
21-
| **MWI_LOG_FILE** | string | `"/tmp/logs.txt"` | Specify the full path to the file where you want the logs to be written. |
21+
| **MWI_LOG_FILE** | string | `"/tmp/logs.txt"` | Specify the full path to the file where you want debug logs from this integration to be written. |
2222
| **MWI_WEB_LOGGING_ENABLED** | string | `"True"` | Set this value to `"true"` to see additional web server logs. |
2323
| **MWI_CUSTOM_HTTP_HEADERS** | string |`'{"Content-Security-Policy": "frame-ancestors *.example.com:*"}'`<br /> OR <br />`"/path/to/your/custom/http-headers.json"` |Specify valid HTTP headers as JSON data in a string format. <br /> Alternatively, specify the full path to the JSON file containing valid HTTP headers instead. These headers are injected into the HTTP response sent to the browser. </br> For more information, see the [Custom HTTP Headers](#custom-http-headers) section.|
24+
| **TMPDIR** or **TMP** | string | `"/path/for/MATLAB/to/use/as/tmp"` | Set either one of these variables to control the temporary folder used by MATLAB. `TMPDIR` takes precedence over `TMP` and if neither variable is set, `/tmp` is the default value used by MATLAB. |
2425

2526
## Usage outside of Jupyter environment
2627

jupyter_matlab_proxy/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async def start_matlab(req):
8383
state = req.app["state"]
8484

8585
# Start MATLAB
86-
await state.start_matlab(restart=True)
86+
await state.start_matlab(restart_matlab=True)
8787

8888
return create_status_response(req.app)
8989

@@ -119,7 +119,7 @@ async def set_licensing_info(req):
119119
if state.is_licensed() is True and not isinstance(state.error, LicensingError):
120120

121121
# Start MATLAB
122-
await state.start_matlab(restart=True)
122+
await state.start_matlab(restart_matlab=True)
123123

124124
return create_status_response(req.app)
125125

jupyter_matlab_proxy/app_state.py

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,23 @@
22

33
import asyncio
44
from jupyter_matlab_proxy import mwi_environment_variables as mwi_env
5-
import xml.etree.ElementTree as ET
5+
from jupyter_matlab_proxy import mwi_embedded_connector as mwi_connector
66
import os
77
import json
88
import pty
99
import logging
1010
from datetime import datetime, timezone, timedelta
1111
from pathlib import Path
12-
import tempfile
1312
import socket
1413
import errno
1514
from collections import deque
16-
from .util import mw, mwi_logger, mwi_validators
15+
from .util import mw, mwi_logger
1716
from .util.mwi_exceptions import (
1817
LicensingError,
1918
InternalError,
2019
OnlineLicensingError,
2120
EntitlementError,
2221
MatlabInstallError,
23-
NetworkLicensingError,
2422
log_error,
2523
)
2624

@@ -33,6 +31,7 @@ def __init__(self, settings):
3331
self.settings = settings
3432
self.processes = {"matlab": None, "xvfb": None}
3533
self.matlab_port = None
34+
self.matlab_ready_file = None
3635
self.licensing = None
3736
self.tasks = {}
3837
self.logs = {
@@ -101,9 +100,10 @@ async def init_licensing(self):
101100
# If NLM connection string is not present, then look for persistent LNU info
102101
elif self.__get_cached_licensing_file().exists():
103102
with open(self.__get_cached_licensing_file(), "r") as f:
104-
licensing = json.loads(f.read())
105103
logger.info("Found cached licensing information...")
106104
try:
105+
# Load can throw if the file is empty for some reason.
106+
licensing = json.loads(f.read())
107107
if licensing["type"] == "nlm":
108108
# Note: Only NLM settings entered in browser were cached.
109109
self.licensing = {
@@ -163,7 +163,7 @@ def get_matlab_state(self):
163163
elif xvfb.returncode is not None:
164164
return "down"
165165
# MATLAB processes started and MATLAB Embedded Connector ready file present
166-
elif self.settings["matlab_ready_file"].exists():
166+
elif self.matlab_ready_file.exists():
167167
return "up"
168168
# MATLAB processes started, but MATLAB Embedded Connector not ready
169169
return "starting"
@@ -315,17 +315,17 @@ def persist_licensing(self):
315315
with open(cached_licensing_file, "w") as f:
316316
f.write(json.dumps(self.licensing))
317317

318-
def reserve_matlab_port(self):
319-
"""Reserve a free port for MATLAB Embedded Connector in the allowed range."""
320-
318+
def get_free_matlab_port(self):
319+
"""Returns a free port for MATLAB Embedded Connector in the allowed range."""
320+
# NOTE It is not guranteed that the port will remain free!
321321
# FIXME Because of https://github.com/http-party/node-http-proxy/issues/1342 the
322322
# node application in development mode always uses port 31515 to bypass the
323323
# reverse proxy. Once this is addressed, remove this special case.
324324
if (
325325
mwi_env.is_development_mode_enabled()
326326
and not mwi_env.is_testing_mode_enabled()
327327
):
328-
self.matlab_port = 31515
328+
return 31515
329329
else:
330330

331331
# TODO If MATLAB Connector is enhanced to allow any port, then the
@@ -337,18 +337,18 @@ def reserve_matlab_port(self):
337337
try:
338338
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
339339
s.bind(("", port))
340-
self.matlab_port = port
341340
s.close()
342341
break
343342
except socket.error as e:
344343
if e.errno != errno.EADDRINUSE:
345344
raise e
345+
return port
346346

347-
async def start_matlab(self, restart=False):
347+
async def start_matlab(self, restart_matlab=False):
348348
"""Start MATLAB."""
349349

350350
# FIXME
351-
if self.get_matlab_state() != "down" and restart is False:
351+
if self.get_matlab_state() != "down" and restart_matlab is False:
352352
raise Exception("MATLAB already running/starting!")
353353

354354
# FIXME
@@ -377,15 +377,14 @@ async def start_matlab(self, restart=False):
377377
self.logs["matlab"].clear()
378378

379379
# Reserve a port for MATLAB Embedded Connector
380-
self.reserve_matlab_port()
380+
self.matlab_port = self.get_free_matlab_port()
381381

382-
# The presence of matlab_ready_file indicates if MATLAB Embedded Connector is
383-
# ready to receive connections, but this could be leftover from a terminated
384-
# MATLAB, so ensure it is cleaned up before starting MATLAB
385-
try:
386-
self.settings["matlab_ready_file"].unlink()
387-
except FileNotFoundError:
388-
pass
382+
# Create a folder to hold the matlab_ready_file that will be created by MATLAB to signal readiness
383+
self.matlab_ready_file, matlab_log_dir = mwi_connector.get_matlab_ready_file(
384+
self.matlab_port
385+
)
386+
logger.info(f"MATLAB_LOG_DIR:{str(matlab_log_dir)}")
387+
logger.info(f"MATLAB_READY_FILE:{str(self.matlab_ready_file)}")
389388

390389
# Configure the environment MATLAB needs to start
391390
matlab_env = os.environ.copy()
@@ -398,8 +397,8 @@ async def start_matlab(self, restart=False):
398397
self.settings["matlab_path"] / "ui" / "webgui" / "src"
399398
)
400399
matlab_env["MWAPIKEY"] = self.settings["mwapikey"]
401-
# TODO Make this configurable (impacts the matlab ready file)
402-
matlab_env["MATLAB_LOG_DIR"] = "/tmp"
400+
# The matlab ready file is written into this location by MATLAB
401+
matlab_env["MATLAB_LOG_DIR"] = str(matlab_log_dir)
403402
matlab_env["MW_CD_ANYWHERE_ENABLED"] = "true"
404403
if self.licensing["type"] == "mhlm":
405404
matlab_env["MLM_WEB_LICENSE"] = "true"
@@ -455,13 +454,10 @@ async def start_matlab(self, restart=False):
455454
logger.debug(f"Started MATLAB (PID={matlab.pid})")
456455

457456
async def matlab_stderr_reader():
458-
logger.info("Starting task to save error logs from MATLAB")
459457
while not self.processes["matlab"].stderr.at_eof():
460-
logger.info("Checking for any error logs from MATLAB to save...")
461458
line = await self.processes["matlab"].stderr.readline()
462459
if line is None:
463460
break
464-
logger.info("Saving error logs from MATLAB.")
465461
self.logs["matlab"].append(line)
466462
await self.handle_matlab_output()
467463

@@ -488,11 +484,11 @@ async def stop_matlab(self):
488484

489485
# Clean up matlab_ready_file
490486
try:
491-
with open(self.settings["matlab_ready_file"], "r") as mrf:
492-
port_in_matlab_ready_file = mrf.read()
493-
if str(self.matlab_port) == port_in_matlab_ready_file:
494-
logger.info("Cleaning up matlab_ready_file...")
495-
self.settings["matlab_ready_file"].unlink()
487+
if self.matlab_ready_file is not None:
488+
logger.info(
489+
f"Cleaning up matlab_ready_file...{str(self.matlab_ready_file)}"
490+
)
491+
self.matlab_ready_file.unlink()
496492
except FileNotFoundError:
497493
# Some other process deleted this file
498494
pass
@@ -509,15 +505,14 @@ async def handle_matlab_output(self):
509505
matlab = self.processes["matlab"]
510506

511507
# Wait for MATLAB process to exit
512-
logger.info("handle_matlab_output Waiting for MATLAB to exit...")
508+
logger.info("Waiting for MATLAB to exit...")
513509
await matlab.wait()
514510

515511
rc = self.processes["matlab"].returncode
516-
logger.info(f"handle_matlab_output MATLAB has exited with errorcode: {rc}")
512+
logger.info(f"MATLAB has exited with errorcode: {rc}")
517513

518514
# Look for errors if MATLAB was not intentionally stopped and had an error code
519515
if len(self.logs["matlab"]) > 0 and self.processes["matlab"].returncode != 0:
520-
logger.info(f"handle_matlab_output Some error was found!")
521516
err = None
522517
logs = [log.decode().rstrip() for log in self.logs["matlab"]]
523518

jupyter_matlab_proxy/devel.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Copyright 2020-2021 The MathWorks, Inc.
22

33
# Development specific functions
4-
import asyncio, aiohttp
4+
import asyncio
55
from aiohttp import web
66
import socket, time, os, sys
77
from jupyter_matlab_proxy import mwi_environment_variables as mwi_env
8+
from jupyter_matlab_proxy import mwi_embedded_connector as mwi_connector
89

910
desktop_html = b"""
1011
<h1>Fake MATLAB Web Desktop</h1>
@@ -79,32 +80,39 @@ async def fake_matlab_started(app):
7980
print("Diagnostic Information", file=sys.stderr)
8081
sys.exit(1)
8182

82-
ready_file = app["ready_file"]
83+
# Real MATLAB always uses $MATLAB_LOG_DIR/connection.securePort as the ready file
84+
# We mock reading from the environment variable by calling the helper functions
85+
matlab_ready_file, matlab_log_dir = mwi_connector.get_matlab_ready_file(app["port"])
86+
8387
ready_delay = app["ready_delay"]
8488
try:
8589
await asyncio.sleep(ready_delay)
86-
print(f"Creating fake MATLAB Embedded Connector ready file at {ready_file}")
87-
ready_file.touch()
90+
print(
91+
f"Creating fake MATLAB Embedded Connector ready file at {matlab_ready_file}"
92+
)
93+
matlab_ready_file.touch()
8894
except asyncio.CancelledError:
8995
pass
9096

9197

9298
async def start_background_tasks(app):
93-
app["ready_file_writer"] = asyncio.create_task(fake_matlab_started(app))
99+
await fake_matlab_started(app)
94100

95101

96102
async def cleanup_background_tasks(app):
97-
app["ready_file_writer"].cancel()
98-
await app["ready_file_writer"]
103+
# Delete ready file on tear down
104+
# NOTE MATLAB does not delete this file on shutdown.
105+
matlab_ready_file, matlab_log_dir = mwi_connector.get_matlab_ready_file(app["port"])
106+
matlab_ready_file.unlink()
99107

100108

101109
def matlab(args):
102110
port = int(os.environ["MW_CONNECTOR_SECURE_PORT"])
103111
wait_for_port(port)
104112
print(f"Serving fake MATLAB Embedded Connector at port {port}")
105113
app = web.Application()
106-
app["ready_file"] = args.ready_file
107114
app["ready_delay"] = args.ready_delay
115+
app["port"] = port
108116

109117
app.router.add_route("GET", "/index-jsd-cr.html", web_handler)
110118

@@ -129,18 +137,12 @@ def matlab(args):
129137

130138
if __name__ == "__main__":
131139
import argparse
132-
import tempfile
133140
from pathlib import Path
134141

135142
parser = argparse.ArgumentParser()
136143
subparsers = parser.add_subparsers(dest="cmd", required=True)
137144
matlab_parser = subparsers.add_parser("matlab")
138-
matlab_parser.add_argument(
139-
"--ready-file",
140-
default=Path(tempfile.gettempdir()) / "connector.securePort",
141-
type=Path,
142-
)
143-
matlab_parser.add_argument("--ready-delay", default=10, type=int)
145+
matlab_parser.add_argument("--ready-delay", default=2, type=int)
144146
matlab_parser.set_defaults(func=matlab)
145147
args = parser.parse_args()
146148
args.func(args)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2020-2021 The MathWorks, Inc.
2+
"""Functions to related to embedded connector access and configuration"""
3+
4+
from jupyter_matlab_proxy import mwi_environment_variables as mwi_env
5+
from jupyter_matlab_proxy import settings as mwi_settings
6+
from pathlib import Path
7+
8+
# TODO Write tests
9+
10+
11+
def get_matlab_ready_file(connector_port):
12+
"""Returns the name and location of the file that is used by MATLAB
13+
embedded connector to signal its readiness to begin serving content"""
14+
ready_file_dir = __create_folder_to_hold_matlab_ready_file(connector_port)
15+
ready_file = ready_file_dir / "connector.securePort"
16+
return ready_file, ready_file_dir
17+
18+
19+
def __create_folder_to_hold_matlab_ready_file(connector_port):
20+
"""MWI creates the location into which the spawned MATLAB connector can create the ready file"""
21+
22+
if mwi_env.is_development_mode_enabled():
23+
return mwi_settings.get_test_temp_dir()
24+
25+
matlab_tempdir = Path(mwi_settings.get_matlab_tempdir())
26+
matlab_ready_file_dir = matlab_tempdir / "MWI" / str(connector_port)
27+
matlab_ready_file_dir.mkdir(parents=True, exist_ok=True)
28+
return matlab_ready_file_dir

jupyter_matlab_proxy/mwi_environment_variables.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ def get_env_name_development():
7272
return "MWI_DEV"
7373

7474

75+
def get_env_name_matlab_tempdir():
76+
"""The environment variables used to control the temp directory used by MATLAB on POSIX systems"""
77+
# Order matters, MATLAB checks TMPDIR first and then TMP
78+
return ["TMPDIR", "TMP"]
79+
80+
7581
def is_development_mode_enabled():
7682
"""Returns true if the app is in development mode."""
7783
return os.environ.get(get_env_name_development(), "false").lower() == "true"

0 commit comments

Comments
 (0)