Skip to content

Commit e6ba25f

Browse files
krisctlprabhakk-mw
authored andcommittedSep 30, 2024
Includes featured off support for the MATLAB Proxy Manager.
1 parent df49558 commit e6ba25f

File tree

8 files changed

+772
-297
lines changed

8 files changed

+772
-297
lines changed
 

‎src/jupyter_matlab_kernel/__main__.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
# Copyright 2023-2024 The MathWorks, Inc.
22
# Use ipykernel infrastructure to launch the MATLAB Kernel.
3+
import os
4+
5+
6+
def is_fallback_kernel_enabled():
7+
"""
8+
Checks if the fallback kernel is enabled based on an environment variable.
9+
10+
Returns:
11+
bool: True if the fallback kernel is enabled, False otherwise.
12+
"""
13+
14+
# Get the env var toggle
15+
use_fallback_kernel = os.getenv("MWI_USE_FALLBACK_KERNEL", "TRUE")
16+
return use_fallback_kernel.lower().strip() == "true"
17+
318

419
if __name__ == "__main__":
520
from ipykernel.kernelapp import IPKernelApp
621
from jupyter_matlab_kernel import mwi_logger
7-
from jupyter_matlab_kernel.kernel import MATLABKernel
822

923
logger = mwi_logger.get(init=True)
24+
kernel_class = None
25+
26+
if is_fallback_kernel_enabled():
27+
from jupyter_matlab_kernel.jsp_kernel import MATLABKernelUsingJSP
28+
29+
kernel_class = MATLABKernelUsingJSP
30+
else:
31+
from jupyter_matlab_kernel.mpm_kernel import MATLABKernelUsingMPM
32+
33+
kernel_class = MATLABKernelUsingMPM
1034

11-
IPKernelApp.launch_instance(kernel_class=MATLABKernel, log=logger)
35+
IPKernelApp.launch_instance(kernel_class=kernel_class, log=logger)

‎src/jupyter_matlab_kernel/kernel.py renamed to ‎src/jupyter_matlab_kernel/base_kernel.py

+164-252
Large diffs are not rendered by default.
+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
"""This module contains derived class implementation of MATLABKernel that uses
4+
Jupyter Server to manage interactions with matlab-proxy & MATLAB.
5+
"""
6+
7+
import asyncio
8+
import http
9+
import os
10+
11+
# Import Dependencies
12+
import aiohttp
13+
import aiohttp.client_exceptions
14+
import requests
15+
16+
from jupyter_matlab_kernel import base_kernel as base
17+
from jupyter_matlab_kernel import mwi_logger, test_utils
18+
from jupyter_matlab_kernel.mwi_comm_helpers import MWICommHelper
19+
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
20+
21+
_logger = mwi_logger.get()
22+
23+
24+
def _start_matlab_proxy_using_jupyter(url, headers, logger=_logger):
25+
"""
26+
Start matlab-proxy using jupyter server which started the current kernel
27+
process by sending HTTP request to the endpoint registered through
28+
jupyter-matlab-proxy.
29+
30+
Args:
31+
url (string): URL to send HTTP request
32+
headers (dict): HTTP headers required for the request
33+
34+
Returns:
35+
bool: True if jupyter server has successfully started matlab-proxy else False.
36+
"""
37+
# This is content that is present in the matlab-proxy index.html page which
38+
# can be used to validate a proper response.
39+
matlab_proxy_index_page_identifier = "MWI_MATLAB_PROXY_IDENTIFIER"
40+
41+
logger.debug(
42+
f"Sending request to jupyter to start matlab-proxy at {url} with headers: {headers}"
43+
)
44+
# send request to the matlab-proxy endpoint to make sure it is available.
45+
# If matlab-proxy is not started, jupyter-server starts it at this point.
46+
resp = requests.get(url, headers=headers, verify=False)
47+
logger.debug("Received status code: %s", resp.status_code)
48+
49+
return (
50+
resp.status_code == http.HTTPStatus.OK
51+
and matlab_proxy_index_page_identifier in resp.text
52+
)
53+
54+
55+
def start_matlab_proxy(logger=_logger):
56+
"""
57+
Start matlab-proxy registered with the jupyter server which started the
58+
current kernel process.
59+
60+
Raises:
61+
MATLABConnectionError: Occurs when kernel is not started by jupyter server.
62+
63+
Returns:
64+
Tuple (string, string, dict):
65+
url (string): Complete URL to send HTTP requests to matlab-proxy
66+
base_url (string): Complete base url for matlab-proxy provided by jupyter server
67+
headers (dict): HTTP headers required while sending HTTP requests to matlab-proxy
68+
"""
69+
70+
# If jupyter testing is enabled, then a standalone matlab-proxy server would be
71+
# launched by the tests and kernel would expect the configurations of this matlab-proxy
72+
# server which is provided through environment variables to 'start_matlab_proxy_for_testing'
73+
if test_utils.is_jupyter_testing_enabled():
74+
return test_utils.start_matlab_proxy_for_testing(logger)
75+
76+
nb_server_list = []
77+
78+
# The matlab-proxy server, if running, could have been started by either
79+
# "jupyter_server" or "notebook" package.
80+
try:
81+
from jupyter_server import serverapp
82+
83+
nb_server_list += list(serverapp.list_running_servers())
84+
85+
from notebook import notebookapp
86+
87+
nb_server_list += list(notebookapp.list_running_servers())
88+
except ImportError:
89+
pass
90+
91+
# Use parent process id of the kernel to filter Jupyter Server from the list.
92+
jupyter_server_pid = base._get_parent_pid()
93+
logger.debug(f"Resolved jupyter server pid: {jupyter_server_pid}")
94+
95+
nb_server = dict()
96+
found_nb_server = False
97+
for server in nb_server_list:
98+
if server["pid"] == jupyter_server_pid:
99+
logger.debug("Jupyter server associated with this MATLAB Kernel found.")
100+
found_nb_server = True
101+
nb_server = server
102+
# Stop iterating over the server list
103+
break
104+
105+
# Error out if the server is not found!
106+
if not found_nb_server:
107+
logger.error("Jupyter server associated with this MATLABKernel not found.")
108+
raise MATLABConnectionError(
109+
"""
110+
Error: MATLAB Kernel for Jupyter was unable to find the notebook server from which it was spawned!\n
111+
Resolution: Please relaunch kernel from JupyterLab or Classic Jupyter Notebook.
112+
"""
113+
)
114+
115+
# Verify that Password is disabled
116+
if nb_server["password"] is True:
117+
logger.error("Jupyter server uses password for authentication.")
118+
# TODO: To support passwords, we either need to acquire it from Jupyter or ask the user?
119+
raise MATLABConnectionError(
120+
"""
121+
Error: MATLAB Kernel could not communicate with MATLAB.\n
122+
Reason: There is a password set to access the Jupyter server.\n
123+
Resolution: Delete the cached Notebook password file, and restart the kernel.\n
124+
See https://jupyter-notebook.readthedocs.io/en/stable/public_server.html#securing-a-notebook-server for more information.
125+
"""
126+
)
127+
128+
# Using nb_server["url"] to construct matlab-proxy URL as it handles the following cases
129+
# 1. For normal usage of Jupyter, the URL returned by nb_server uses localhost
130+
# 2. For explicitly specified IP with Jupyter, the URL returned by nb_server
131+
# a. uses FQDN hostname when specified IP is 0.0.0.0
132+
# b. uses specified IP for all other cases
133+
matlab_proxy_url = "{jupyter_server_url}matlab".format(
134+
jupyter_server_url=nb_server["url"]
135+
)
136+
137+
available_tokens = {
138+
"jupyter_server": nb_server.get("token"),
139+
"jupyterhub": os.getenv("JUPYTERHUB_API_TOKEN"),
140+
"default": None,
141+
}
142+
143+
for token in available_tokens.values():
144+
if token:
145+
headers = {"Authorization": f"token {token}"}
146+
else:
147+
headers = None
148+
149+
if _start_matlab_proxy_using_jupyter(matlab_proxy_url, headers, logger):
150+
logger.debug(
151+
f"Started matlab-proxy using jupyter at {matlab_proxy_url} with headers: {headers}"
152+
)
153+
return matlab_proxy_url, nb_server["base_url"], headers
154+
155+
logger.error(
156+
"MATLABKernel could not communicate with matlab-proxy through Jupyter server"
157+
)
158+
logger.error(f"Jupyter server:\n{nb_server}")
159+
raise MATLABConnectionError(
160+
"""
161+
Error: MATLAB Kernel could not communicate with MATLAB.
162+
Reason: Possibly due to invalid jupyter security tokens.
163+
"""
164+
)
165+
166+
167+
class MATLABKernelUsingJSP(base.BaseMATLABKernel):
168+
def __init__(self, *args, **kwargs):
169+
super().__init__(*args, **kwargs)
170+
171+
try:
172+
# Start matlab-proxy using the jupyter-matlab-proxy registered endpoint.
173+
murl, self.jupyter_base_url, headers = start_matlab_proxy(self.log)
174+
175+
# Using asyncio.get_event_loop for shell_loop as io_loop variable is
176+
# not yet initialized because start() is called after the __init__
177+
# is completed.
178+
shell_loop = asyncio.get_event_loop()
179+
control_loop = self.control_thread.io_loop.asyncio_loop
180+
self.mwi_comm_helper = MWICommHelper(
181+
self.kernel_id, murl, shell_loop, control_loop, headers, self.log
182+
)
183+
shell_loop.run_until_complete(self.mwi_comm_helper.connect())
184+
except MATLABConnectionError as err:
185+
self.startup_error = err
186+
187+
async def do_shutdown(self, restart):
188+
self.log.debug("Received shutdown request from Jupyter")
189+
try:
190+
await self.mwi_comm_helper.send_shutdown_request_to_matlab()
191+
await self.mwi_comm_helper.disconnect()
192+
except (
193+
MATLABConnectionError,
194+
aiohttp.client_exceptions.ClientResponseError,
195+
) as e:
196+
self.log.error(
197+
f"Exception occurred while sending shutdown request to MATLAB:\n{e}"
198+
)
199+
200+
return super().do_shutdown(restart)
201+
202+
async def perform_startup_checks(self):
203+
"""Overriding base function to provide a different iframe source"""
204+
iframe_src: str = f'{self.jupyter_base_url + "matlab"}'
205+
await super().perform_startup_checks(iframe_src)
+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
"""This module contains derived class implementation of MATLABKernel that uses
4+
MATLAB Proxy Manager to manage interactions with matlab-proxy & MATLAB.
5+
"""
6+
7+
from logging import Logger
8+
9+
import matlab_proxy_manager.lib.api as mpm_lib
10+
from requests.exceptions import HTTPError
11+
12+
from jupyter_matlab_kernel import base_kernel as base
13+
from jupyter_matlab_kernel.mwi_comm_helpers import MWICommHelper
14+
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
15+
16+
17+
class MATLABKernelUsingMPM(base.BaseMATLABKernel):
18+
def __init__(self, *args, **kwargs):
19+
super().__init__(*args, **kwargs)
20+
21+
# Used to detect if this Kernel has been assigned a MATLAB-proxy server or not
22+
self.is_matlab_assigned = False
23+
24+
# Serves as the auth token to secure communication between Jupyter Server and MATLAB proxy manager
25+
self.mpm_auth_token = None
26+
27+
# There might be multiple instances of Jupyter servers or VScode servers running on a
28+
# single machine. This attribute serves as the context provider and backend MATLAB proxy
29+
# processes are filtered using this attribute during start and shutdown of MATLAB proxy
30+
self.parent_pid = base._get_parent_pid()
31+
32+
# Required for performing licensing using Jupyter Server
33+
self.jupyter_base_url = base._fetch_jupyter_base_url(self.parent_pid, self.log)
34+
35+
# ipykernel Interface API
36+
# https://ipython.readthedocs.io/en/stable/development/wrapperkernels.html
37+
38+
async def do_execute(
39+
self,
40+
code,
41+
silent,
42+
store_history=True,
43+
user_expressions=None,
44+
allow_stdin=False,
45+
*,
46+
cell_id=None,
47+
):
48+
"""
49+
Used by ipykernel infrastructure for execution. For more info, look at
50+
https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute
51+
"""
52+
self.log.debug(f"Received execution request from Jupyter with code:\n{code}")
53+
54+
# Starts the matlab proxy process if this kernel hasn't yet been assigned a
55+
# matlab proxy and sets the attributes on kernel to talk to the correct backend.
56+
if not self.is_matlab_assigned:
57+
self.log.debug("Starting matlab-proxy")
58+
await self._start_matlab_proxy_and_comm_helper()
59+
self.is_matlab_assigned = True
60+
61+
return await super().do_execute(
62+
code=code,
63+
silent=silent,
64+
store_history=store_history,
65+
user_expressions=user_expressions,
66+
allow_stdin=allow_stdin,
67+
cell_id=cell_id,
68+
)
69+
70+
async def do_shutdown(self, restart):
71+
self.log.debug("Received shutdown request from Jupyter")
72+
if self.is_matlab_assigned:
73+
try:
74+
# Cleans up internal live editor state, client session
75+
await self.mwi_comm_helper.send_shutdown_request_to_matlab()
76+
await self.mwi_comm_helper.disconnect()
77+
78+
except (MATLABConnectionError, HTTPError) as e:
79+
self.log.error(
80+
f"Exception occurred while sending shutdown request to MATLAB:\n{e}"
81+
)
82+
except Exception as e:
83+
self.log.debug("Exception during shutdown", e)
84+
finally:
85+
# Shuts down matlab assigned to this Kernel (based on satisfying certain criteria)
86+
await mpm_lib.shutdown(
87+
self.parent_pid, self.kernel_id, self.mpm_auth_token
88+
)
89+
self.is_matlab_assigned = False
90+
91+
return super().do_shutdown(restart)
92+
93+
async def perform_startup_checks(self):
94+
"""Overriding base function to provide a different iframe source"""
95+
iframe_src: str = (
96+
f'{self.jupyter_base_url}{self.matlab_proxy_base_url.lstrip("/")}/'
97+
)
98+
await super().perform_startup_checks(iframe_src)
99+
100+
# Helper functions
101+
102+
async def _start_matlab_proxy_and_comm_helper(self) -> None:
103+
"""
104+
Starts the MATLAB proxy using the proxy manager and fetches its status.
105+
"""
106+
try:
107+
(
108+
murl,
109+
self.matlab_proxy_base_url,
110+
headers,
111+
self.mpm_auth_token,
112+
) = await self._initialize_matlab_proxy_with_mpm(self.log)
113+
114+
await self._initialize_mwi_comm_helper(murl, headers)
115+
except MATLABConnectionError as err:
116+
self.startup_error = err
117+
118+
async def _initialize_matlab_proxy_with_mpm(self, _logger: Logger):
119+
"""
120+
Initializes the MATLAB proxy process using the Proxy Manager (MPM) library.
121+
122+
Calls proxy manager to start the MATLAB proxy process for this kernel.
123+
124+
Args:
125+
logger (Logger): The logger instance
126+
127+
Returns:
128+
tuple: A tuple containing:
129+
- server_url (str): Absolute URL for the MATLAB proxy backend (includes base URL)
130+
- base_url (str): The base URL of the MATLAB proxy server
131+
- headers (dict): The headers required for communication with the MATLAB proxy
132+
- mpm_auth_token (str): Token for authentication between kernel and proxy manager
133+
134+
Raises:
135+
MATLABConnectionError: If the MATLAB proxy process could not be started
136+
"""
137+
try:
138+
response = await mpm_lib.start_matlab_proxy_for_kernel(
139+
caller_id=self.kernel_id,
140+
parent_id=self.parent_pid,
141+
is_isolated_matlab=False,
142+
)
143+
return (
144+
response.get("absolute_url"),
145+
response.get("mwi_base_url"),
146+
response.get("headers"),
147+
response.get("mpm_auth_token"),
148+
)
149+
except Exception as e:
150+
_logger.error(
151+
f"MATLAB Kernel could not start matlab-proxy using proxy manager with error: {e}"
152+
)
153+
raise MATLABConnectionError(
154+
"""
155+
Error: MATLAB Kernel could not start the MATLAB proxy process via proxy manager.
156+
"""
157+
) from e
158+
159+
async def _initialize_mwi_comm_helper(self, murl, headers):
160+
"""
161+
Initializes the MWICommHelper for managing communication with a specified URL.
162+
163+
This method sets up the MWICommHelper instance with the given
164+
message URL and headers, utilizing the shell and control event loops. It then
165+
initiates a connection by creating and awaiting a task on the shell event loop.
166+
167+
Parameters:
168+
- murl (str): The message URL used for communication.
169+
- headers (dict): A dictionary of headers to include in the communication setup.
170+
"""
171+
shell_loop = self.io_loop.asyncio_loop
172+
control_loop = self.control_thread.io_loop.asyncio_loop
173+
self.mwi_comm_helper = MWICommHelper(
174+
self.kernel_id, murl, shell_loop, control_loop, headers, self.log
175+
)
176+
await self.mwi_comm_helper.connect()
177+
178+
def _process_children(self):
179+
"""Overrides the _process_children in kernelbase class to not return the list of children
180+
so that the child process termination can be managed at proxy manager layer
181+
"""
182+
return []
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2023-2024 The MathWorks, Inc.
2+
3+
import os
4+
from jupyter_matlab_kernel import mwi_logger
5+
6+
_logger = mwi_logger.get()
7+
8+
9+
def is_jupyter_testing_enabled():
10+
"""
11+
Checks if testing mode is enabled
12+
13+
Returns:
14+
bool: True if MWI_JUPYTER_TEST environment variable is set to 'true'
15+
else False
16+
"""
17+
18+
return os.environ.get("MWI_JUPYTER_TEST", "false").lower() == "true"
19+
20+
21+
def start_matlab_proxy_for_testing(logger=_logger):
22+
"""
23+
Only used for testing purposes. Gets the matlab-proxy server configuration
24+
from environment variables and mocks the 'start_matlab_proxy' function
25+
26+
Returns:
27+
Tuple (string, string, dict):
28+
url (string): Complete URL to send HTTP requests to matlab-proxy
29+
base_url (string): Complete base url for matlab-proxy obtained from tests
30+
headers (dict): Empty dictionary
31+
"""
32+
33+
import matlab_proxy.util.mwi.environment_variables as mwi_env
34+
35+
# These environment variables are being set by tests, using dictionary lookup
36+
# instead of '.getenv' to make sure that the following line fails with the
37+
# Exception 'KeyError' in case the environment variables are not set
38+
matlab_proxy_base_url = os.environ[mwi_env.get_env_name_base_url()]
39+
matlab_proxy_app_port = os.environ[mwi_env.get_env_name_app_port()]
40+
41+
logger.debug("Creating matlab-proxy URL for MATLABKernel testing.")
42+
43+
# '127.0.0.1' is used instead 'localhost' for testing since Windows machines consume
44+
# some time to resolve 'localhost' hostname
45+
url = "{protocol}://127.0.0.1:{port}{base_url}".format(
46+
protocol="http",
47+
port=matlab_proxy_app_port,
48+
base_url=matlab_proxy_base_url,
49+
)
50+
headers = {}
51+
52+
logger.debug("matlab-proxy URL: %s", url)
53+
logger.debug("headers: %s", headers)
54+
55+
return url, matlab_proxy_base_url, headers

‎src/jupyter_matlab_proxy/__init__.py

+88-42
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
# Copyright 2020-2024 The MathWorks, Inc.
22

33
import os
4+
import secrets
45
from pathlib import Path
56

7+
import matlab_proxy
8+
from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
69
from matlab_proxy.util.mwi import environment_variables as mwi_env
10+
from matlab_proxy.util.mwi import logger as mwi_logger
711
from matlab_proxy.util.mwi import token_auth as mwi_token_auth
812

913
from jupyter_matlab_proxy.jupyter_config import config
1014

15+
_MPM_AUTH_TOKEN: str = secrets.token_hex(32)
16+
_JUPYTER_SERVER_PID: str = str(os.getpid())
17+
_USE_FALLBACK_KERNEL: bool = (
18+
os.getenv("MWI_USE_FALLBACK_KERNEL", "TRUE").lower().strip() == "true"
19+
)
20+
1121

1222
def _get_auth_token():
1323
"""
@@ -48,24 +58,34 @@ def _get_env(port, base_url):
4858
Returns:
4959
[Dict]: Containing environment settings to launch the MATLAB Desktop.
5060
"""
61+
env = {}
62+
if _USE_FALLBACK_KERNEL:
63+
env = {
64+
mwi_env.get_env_name_app_port(): str(port),
65+
mwi_env.get_env_name_base_url(): f"{base_url}matlab",
66+
mwi_env.get_env_name_app_host(): "127.0.0.1",
67+
}
5168

52-
env = {
53-
mwi_env.get_env_name_app_port(): str(port),
54-
mwi_env.get_env_name_base_url(): f"{base_url}matlab",
55-
mwi_env.get_env_name_app_host(): "127.0.0.1",
56-
}
57-
58-
# Add token authentication related information to the environment variables
59-
# dictionary passed to the matlab-proxy process if token authentication is
60-
# not explicitly disabled.
61-
if _mwi_auth_token:
62-
env.update(
63-
{
64-
mwi_env.get_env_name_enable_mwi_auth_token(): "True",
65-
mwi_env.get_env_name_mwi_auth_token(): _mwi_auth_token.get("token"),
66-
}
67-
)
68-
69+
# Add token authentication related information to the environment variables
70+
# dictionary passed to the matlab-proxy process if token authentication is
71+
# not explicitly disabled.
72+
if _mwi_auth_token:
73+
env.update(
74+
{
75+
mwi_env.get_env_name_enable_mwi_auth_token(): "True",
76+
mwi_env.get_env_name_mwi_auth_token(): _mwi_auth_token.get("token"),
77+
}
78+
)
79+
80+
else:
81+
# case when we are using matlab proxy manager
82+
import matlab_proxy_manager.utils.environment_variables as mpm_env
83+
84+
env = {
85+
mpm_env.get_env_name_mwi_mpm_port(): str(port),
86+
mpm_env.get_env_name_mwi_mpm_auth_token(): _MPM_AUTH_TOKEN,
87+
mpm_env.get_env_name_mwi_mpm_parent_pid(): _JUPYTER_SERVER_PID,
88+
}
6989
return env
7090

7191

@@ -76,36 +96,62 @@ def setup_matlab():
7696
[Dict]: Containing information to launch the MATLAB Desktop.
7797
"""
7898

79-
import matlab_proxy
80-
from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
81-
from matlab_proxy.util.mwi import logger as mwi_logger
82-
8399
logger = mwi_logger.get(init=True)
84100
logger.info("Initializing Jupyter MATLAB Proxy")
85101

102+
jsp_config = _get_jsp_config(logger=logger)
103+
104+
return jsp_config
105+
106+
107+
def _get_jsp_config(logger):
86108
icon_path = Path(__file__).parent / "icon_open_matlab.svg"
87-
logger.debug(f"Icon_path: {icon_path}")
88-
logger.debug(f"Launch Command: {matlab_proxy.get_executable_name()}")
89-
logger.debug(f"Extension Name: {config['extension_name']}")
90-
91-
jsp_config = {
92-
"command": [
93-
matlab_proxy.get_executable_name(),
94-
"--config",
95-
config["extension_name"],
96-
],
97-
"timeout": 100,
98-
"environment": _get_env,
99-
"absolute_url": True,
100-
"launcher_entry": {"title": "Open MATLAB", "icon_path": icon_path},
101-
}
102-
103-
# Add token_hash information to the request_headers_override option to
104-
# ensure requests from jupyter to matlab-proxy are automatically authenticated.
105-
# We are using token_hash instead of raw token for better security.
106-
if _mwi_auth_token:
109+
logger.debug("Icon_path: %s", icon_path)
110+
jsp_config = {}
111+
112+
if _USE_FALLBACK_KERNEL:
113+
jsp_config = {
114+
"command": [
115+
matlab_proxy.get_executable_name(),
116+
"--config",
117+
config["extension_name"],
118+
],
119+
"timeout": 100,
120+
"environment": _get_env,
121+
"absolute_url": True,
122+
"launcher_entry": {"title": "Open MATLAB", "icon_path": icon_path},
123+
}
124+
logger.debug("Launch Command: %s", jsp_config.get("command"))
125+
126+
# Add token_hash information to the request_headers_override option to
127+
# ensure requests from jupyter to matlab-proxy are automatically authenticated.
128+
# We are using token_hash instead of raw token for better security.
129+
if _mwi_auth_token:
130+
jsp_config["request_headers_override"] = {
131+
MWI_AUTH_TOKEN_NAME_FOR_HTTP: _mwi_auth_token.get("token_hash")
132+
}
133+
else:
134+
import matlab_proxy_manager
135+
from matlab_proxy_manager.utils import constants
136+
137+
# JSP config for when we are using matlab proxy manager
138+
jsp_config = {
139+
# Starts proxy manager process which in turn starts a shared matlab proxy instance
140+
# if not already started. This gets invoked on clicking `Open MATLAB` button and would
141+
# always take the user to the default (shared) matlab-proxy instance.
142+
"command": [matlab_proxy_manager.get_executable_name()],
143+
"timeout": 100, # timeout in seconds
144+
"environment": _get_env,
145+
"absolute_url": True,
146+
"launcher_entry": {"title": "Open MATLAB", "icon_path": icon_path},
147+
}
148+
logger.debug("Launch Command: %s", jsp_config.get("command"))
149+
150+
# Add jupyter server pid and mpm_auth_token to the request headers for resource
151+
# filtering and Jupyter to proxy manager authentication
107152
jsp_config["request_headers_override"] = {
108-
MWI_AUTH_TOKEN_NAME_FOR_HTTP: _mwi_auth_token.get("token_hash")
153+
constants.HEADER_MWI_MPM_CONTEXT: _JUPYTER_SERVER_PID,
154+
constants.HEADER_MWI_MPM_AUTH_TOKEN: _MPM_AUTH_TOKEN,
109155
}
110156

111157
return jsp_config

‎tests/unit/jupyter_matlab_kernel/test_kernel.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# This file contains tests for jupyter_matlab_kernel.kernel
44
import mocks.mock_jupyter_server as MockJupyterServer
55
import pytest
6-
from jupyter_matlab_kernel.kernel import start_matlab_proxy
6+
from jupyter_matlab_kernel.jsp_kernel import start_matlab_proxy
77
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
88
from jupyter_server import serverapp
99
from mocks.mock_jupyter_server import MockJupyterServerFixture

‎tests/unit/test_jupyter_server_proxy.py

+51
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
import jupyter_matlab_proxy
88
import matlab_proxy
9+
import matlab_proxy_manager
910
from jupyter_matlab_proxy.jupyter_config import config
1011
from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
1112
from matlab_proxy.util.mwi import environment_variables as mwi_env
13+
from matlab_proxy_manager.utils import constants
14+
from matlab_proxy_manager.utils import environment_variables as mpm_env
1215

1316

1417
def test_get_auth_token():
@@ -55,6 +58,19 @@ def test_get_env_with_token_auth_disabled(monkeypatch):
5558
assert r.get(mwi_env.get_env_name_mwi_auth_token()) == None
5659

5760

61+
def test_get_env_with_proxy_manager(monkeypatch):
62+
"""Tests if _get_env() method returns the expected environment settings as a dict."""
63+
# Setup
64+
monkeypatch.setattr("jupyter_matlab_proxy._USE_FALLBACK_KERNEL", False)
65+
monkeypatch.setattr("jupyter_matlab_proxy._MPM_AUTH_TOKEN", "secret")
66+
monkeypatch.setattr("jupyter_matlab_proxy._JUPYTER_SERVER_PID", "123")
67+
mpm_port = 10000
68+
r = jupyter_matlab_proxy._get_env(mpm_port, None)
69+
assert r.get(mpm_env.get_env_name_mwi_mpm_port()) == str(mpm_port)
70+
assert r.get(mpm_env.get_env_name_mwi_mpm_auth_token()) == "secret"
71+
assert r.get(mpm_env.get_env_name_mwi_mpm_parent_pid()) == "123"
72+
73+
5874
def test_setup_matlab():
5975
"""Tests for a valid Server Process Configuration Dictionary
6076
@@ -91,6 +107,41 @@ def test_setup_matlab():
91107
assert os.path.isfile(actual_matlab_setup["launcher_entry"]["icon_path"])
92108

93109

110+
def test_setup_matlab_with_proxy_manager(monkeypatch):
111+
"""Tests for a valid Server Process Configuration Dictionary
112+
113+
This test checks if the jupyter proxy returns the expected Server Process Configuration
114+
Dictionary for the Matlab process.
115+
"""
116+
117+
# Setup
118+
monkeypatch.setattr("jupyter_matlab_proxy._USE_FALLBACK_KERNEL", False)
119+
monkeypatch.setattr("jupyter_matlab_proxy._MPM_AUTH_TOKEN", "secret")
120+
monkeypatch.setattr("jupyter_matlab_proxy._JUPYTER_SERVER_PID", "123")
121+
package_path = Path(inspect.getfile(jupyter_matlab_proxy)).parent
122+
icon_path = package_path / "icon_open_matlab.svg"
123+
124+
expected_matlab_setup = {
125+
"command": [matlab_proxy_manager.get_executable_name()],
126+
"timeout": 100,
127+
"environment": jupyter_matlab_proxy._get_env,
128+
"absolute_url": True,
129+
"launcher_entry": {
130+
"title": "Open MATLAB",
131+
"icon_path": icon_path,
132+
},
133+
"request_headers_override": {
134+
constants.HEADER_MWI_MPM_CONTEXT: "123",
135+
constants.HEADER_MWI_MPM_AUTH_TOKEN: "secret",
136+
},
137+
}
138+
139+
actual_matlab_setup = jupyter_matlab_proxy.setup_matlab()
140+
141+
assert expected_matlab_setup == actual_matlab_setup
142+
assert os.path.isfile(actual_matlab_setup["launcher_entry"]["icon_path"])
143+
144+
94145
def test_setup_matlab_with_token_auth_disabled(monkeypatch):
95146
"""Tests for a valid Server Process Configuration Dictionary
96147

0 commit comments

Comments
 (0)
Please sign in to comment.