1- # Copyright 2023 The MathWorks, Inc.
1+ # Copyright 2023-2024 The MathWorks, Inc.
22# Implementation of MATLAB Kernel
33
44# Import Python Standard Library
1010import ipykernel .kernelbase
1111import psutil
1212import requests
13- from requests .exceptions import HTTPError
14-
15- from jupyter_matlab_kernel import mwi_comm_helpers
16- from matlab_proxy import util as mwi_util
13+ from jupyter_matlab_kernel import mwi_comm_helpers , mwi_logger
1714from matlab_proxy import settings as mwi_settings
18-
15+ from matlab_proxy import util as mwi_util
16+ from requests .exceptions import HTTPError
1917
2018_MATLAB_STARTUP_TIMEOUT = mwi_settings .get_process_startup_timeout ()
19+ _logger = mwi_logger .get ()
2120
2221
2322class MATLABConnectionError (Exception ):
@@ -46,7 +45,7 @@ def is_jupyter_testing_enabled():
4645 return os .environ .get ("MWI_JUPYTER_TEST" , "false" ).lower () == "true"
4746
4847
49- def start_matlab_proxy_for_testing ():
48+ def start_matlab_proxy_for_testing (logger = _logger ):
5049 """
5150 Only used for testing purposes. Gets the matlab-proxy server configuration
5251 from environment variables and mocks the 'start_matlab_proxy' function
@@ -66,6 +65,8 @@ def start_matlab_proxy_for_testing():
6665 matlab_proxy_base_url = os .environ [mwi_env .get_env_name_base_url ()]
6766 matlab_proxy_app_port = os .environ [mwi_env .get_env_name_app_port ()]
6867
68+ logger .debug ("Creating matlab-proxy URL for MATLABKernel testing." )
69+
6970 # '127.0.0.1' is used instead 'localhost' for testing since Windows machines consume
7071 # some time to resolve 'localhost' hostname
7172 url = "{protocol}://127.0.0.1:{port}{base_url}" .format (
@@ -74,10 +75,14 @@ def start_matlab_proxy_for_testing():
7475 base_url = matlab_proxy_base_url ,
7576 )
7677 headers = {}
78+
79+ logger .debug (f"matlab-proxy URL: { url } " )
80+ logger .debug (f"headers: { headers } " )
81+
7782 return url , matlab_proxy_base_url , headers
7883
7984
80- def _start_matlab_proxy_using_jupyter (url , headers ):
85+ def _start_matlab_proxy_using_jupyter (url , headers , logger = _logger ):
8186 """
8287 Start matlab-proxy using jupyter server which started the current kernel
8388 process by sending HTTP request to the endpoint registered through
@@ -94,16 +99,21 @@ def _start_matlab_proxy_using_jupyter(url, headers):
9499 # can be used to validate a proper response.
95100 matlab_proxy_index_page_identifier = "MWI_MATLAB_PROXY_IDENTIFIER"
96101
102+ logger .debug (
103+ f"Sending request to jupyter to start matlab-proxy at { url } with headers: { headers } "
104+ )
97105 # send request to the matlab-proxy endpoint to make sure it is available.
98106 # If matlab-proxy is not started, jupyter-server starts it at this point.
99107 resp = requests .get (url , headers = headers , verify = False )
108+ logger .debug (f"Received status code: { resp .status_code } " )
109+
100110 return (
101111 resp .status_code == requests .codes .OK
102112 and matlab_proxy_index_page_identifier in resp .text
103113 )
104114
105115
106- def start_matlab_proxy ():
116+ def start_matlab_proxy (logger = _logger ):
107117 """
108118 Start matlab-proxy registered with the jupyter server which started the
109119 current kernel process.
@@ -122,7 +132,7 @@ def start_matlab_proxy():
122132 # launched by the tests and kernel would expect the configurations of this matlab-proxy
123133 # server which is provided through environment variables to 'start_matlab_proxy_for_testing'
124134 if is_jupyter_testing_enabled ():
125- return start_matlab_proxy_for_testing ()
135+ return start_matlab_proxy_for_testing (logger )
126136
127137 nb_server_list = []
128138
@@ -150,17 +160,21 @@ def start_matlab_proxy():
150160 if mwi_util .system .is_windows () and is_virtual_env :
151161 jupyter_server_pid = psutil .Process (jupyter_server_pid ).ppid ()
152162
163+ logger .debug (f"Resolved jupyter server pid: { jupyter_server_pid } " )
164+
153165 nb_server = dict ()
154166 found_nb_server = False
155167 for server in nb_server_list :
156168 if server ["pid" ] == jupyter_server_pid :
169+ logger .debug ("Jupyter server associated with this MATLAB Kernel found." )
157170 found_nb_server = True
158171 nb_server = server
159172 # Stop iterating over the server list
160173 break
161174
162175 # Error out if the server is not found!
163176 if found_nb_server == False :
177+ logger .error ("Jupyter server associated with this MATLABKernel not found." )
164178 raise MATLABConnectionError (
165179 """
166180 Error: MATLAB Kernel for Jupyter was unable to find the notebook server from which it was spawned!\n
@@ -170,6 +184,7 @@ def start_matlab_proxy():
170184
171185 # Verify that Password is disabled
172186 if nb_server ["password" ] is True :
187+ logger .error ("Jupyter server uses password for authentication." )
173188 # TODO: To support passwords, we either need to acquire it from Jupyter or ask the user?
174189 raise MATLABConnectionError (
175190 """
@@ -198,9 +213,16 @@ def start_matlab_proxy():
198213 else :
199214 headers = None
200215
201- if _start_matlab_proxy_using_jupyter (url , headers ):
216+ if _start_matlab_proxy_using_jupyter (url , headers , logger ):
217+ logger .debug (
218+ f"Started matlab-proxy using jupyter at { url } with headers: { headers } "
219+ )
202220 return url , nb_server ["base_url" ], headers
203221
222+ logger .error (
223+ f"MATLABKernel could not communicate with matlab-proxy through Jupyter server"
224+ )
225+ logger .error (f"Jupyter server:\n { nb_server } " )
204226 raise MATLABConnectionError (
205227 """
206228 Error: MATLAB Kernel could not communicate with MATLAB.
@@ -236,14 +258,22 @@ class MATLABKernel(ipykernel.kernelbase.Kernel):
236258 def __init__ (self , * args , ** kwargs ):
237259 # Call superclass constructor to initialize ipykernel infrastructure
238260 super (MATLABKernel , self ).__init__ (* args , ** kwargs )
261+
262+ # Update log instance with kernel id. This helps in identifying logs from
263+ # multiple kernels which are running simultaneously
264+ self .log .debug (f"Initializing kernel with id: { self .ident } " )
265+ self .log = self .log .getChild (f"{ self .ident } " )
266+
239267 try :
240268 # Start matlab-proxy using the jupyter-matlab-proxy registered endpoint.
241- self .murl , self .server_base_url , self .headers = start_matlab_proxy ()
269+ self .murl , self .server_base_url , self .headers = start_matlab_proxy (self . log )
242270 (
243271 self .is_matlab_licensed ,
244272 self .matlab_status ,
245273 self .matlab_proxy_has_error ,
246- ) = mwi_comm_helpers .fetch_matlab_proxy_status (self .murl , self .headers )
274+ ) = mwi_comm_helpers .fetch_matlab_proxy_status (
275+ self .murl , self .headers , self .log
276+ )
247277 except MATLABConnectionError as err :
248278 self .startup_error = err
249279
@@ -255,14 +285,20 @@ async def interrupt_request(self, stream, ident, parent):
255285 Custom handling of interrupt request sent by Jupyter. For more info, look at
256286 https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt
257287 """
288+ self .log .debug ("Received interrupt request from Jupyter" )
258289 try :
259290 # Send interrupt request to MATLAB
260- mwi_comm_helpers .send_interrupt_request_to_matlab (self .murl , self .headers )
291+ mwi_comm_helpers .send_interrupt_request_to_matlab (
292+ self .murl , self .headers , self .log
293+ )
261294
262295 # Set the response to interrupt request.
263296 content = {"status" : "ok" }
264297 except Exception as e :
265298 # Set the exception information as response to interrupt request
299+ self .log .error (
300+ f"Exception occurred while sending interrupt request to MATLAB: { e } "
301+ )
266302 content = {
267303 "status" : "error" ,
268304 "ename" : str (type (e ).__name__ ),
@@ -286,6 +322,7 @@ def do_execute(
286322 Used by ipykernel infrastructure for execution. For more info, look at
287323 https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute
288324 """
325+ self .log .debug (f"Received execution request from Jupyter with code:\n { code } " )
289326 try :
290327 # Complete one-time startup checks before sending request to MATLAB.
291328 # Blocking call, returns after MATLAB is started.
@@ -305,20 +342,30 @@ def do_execute(
305342 # Perform execution and categorization of outputs in MATLAB. Blocks
306343 # until execution results are received from MATLAB.
307344 outputs = mwi_comm_helpers .send_execution_request_to_matlab (
308- self .murl , self .headers , code , self .ident
345+ self .murl , self .headers , code , self .ident , self .log
346+ )
347+
348+ self .log .debug (
349+ "Received outputs after execution in MATLAB. Clearing output area"
309350 )
310351
311352 # Clear the output area of the current cell. This removes any previous
312353 # outputs before publishing new outputs.
313354 self .display_output ({"type" : "clear_output" , "content" : {"wait" : False }})
314355
315356 # Display all the outputs produced during the execution of code.
316- for data in outputs :
357+ for idx in range (len (outputs )):
358+ data = outputs [idx ]
359+ self .log .debug (f"Displaying output { idx + 1 } :\n { data } " )
360+
317361 # Ignore empty values returned from MATLAB.
318362 if not data :
319363 continue
320364 self .display_output (data )
321365 except Exception as e :
366+ self .log .error (
367+ f"Exception occurred while processing execution request:\n { e } "
368+ )
322369 if isinstance (e , HTTPError ):
323370 # If exception is an HTTPError, it means MATLAB is unavailable.
324371 # Replace the HTTPError with MATLABConnectionError to give
@@ -356,6 +403,9 @@ def do_complete(self, code, cursor_pos):
356403 user. For example, if matlab-proxy is not licensed, we cannot show
357404 the licensing window.
358405 """
406+ self .log .debug (
407+ f"Received completion request from Jupyter with cursor position { cursor_pos } and code:\n { code } "
408+ )
359409 # Default completion results. It is modelled after ipkernel.py#do_complete
360410 # implementation to provide metadata for JupyterLab.
361411 completion_results = {
@@ -369,12 +419,16 @@ def do_complete(self, code, cursor_pos):
369419 # results are received from MATLAB or communication with MATLAB fails.
370420 try :
371421 completion_results = mwi_comm_helpers .send_completion_request_to_matlab (
372- self .murl , self .headers , code , cursor_pos
422+ self .murl , self .headers , code , cursor_pos , self .log
423+ )
424+ except (MATLABConnectionError , HTTPError ) as e :
425+ self .log .error (
426+ f"Exception occurred while sending shutdown request to MATLAB:\n { e } "
373427 )
374- except ( MATLABConnectionError , HTTPError ):
375- # Jupyter doesn't show the error messages to the user for this request.
376- # Hence, we'll currently do nothing when an error occurs here.
377- pass
428+
429+ self . log . debug (
430+ f"Received completion results from MATLAB: \n { completion_results } "
431+ )
378432
379433 return {
380434 "status" : "ok" ,
@@ -413,14 +467,15 @@ def do_history(
413467 )
414468
415469 def do_shutdown (self , restart ):
470+ self .log .debug ("Received shutdown request from Jupyter" )
416471 try :
417472 mwi_comm_helpers .send_shutdown_request_to_matlab (
418- self .murl , self .headers , self .ident
473+ self .murl , self .headers , self .ident , self .log
474+ )
475+ except (MATLABConnectionError , HTTPError ) as e :
476+ self .log .error (
477+ f"Exception occurred while sending shutdown request to MATLAB:\n { e } "
419478 )
420- except (MATLABConnectionError , HTTPError ):
421- # Jupyter doesn't show the error messages to the user for this request.
422- # Hence, we'll currently do nothing when an error occurs here.
423- pass
424479
425480 return super ().do_shutdown (restart )
426481
@@ -435,8 +490,10 @@ def perform_startup_checks(self):
435490 HTTPError, MATLABConnectionError: Occurs when matlab-proxy is not started or kernel cannot
436491 communicate with MATLAB.
437492 """
493+ self .log .debug ("Performing startup checks" )
438494 # Incase an error occurred while kernel initialization, display it to the user.
439495 if self .startup_error is not None :
496+ self .log .error (f"Found a startup error: { self .startup_error } " )
440497 raise self .startup_error
441498
442499 (
@@ -456,6 +513,9 @@ def perform_startup_checks(self):
456513 #
457514 # TODO: Find a workaround for users to be able to use our Jupyter kernel in VS Code.
458515 if not self .is_matlab_licensed :
516+ self .log .debug (
517+ "MATLAB is not licensed. Displaying HTML output to enable licensing."
518+ )
459519 self .display_output (
460520 {
461521 "type" : "display_data" ,
@@ -469,6 +529,7 @@ def perform_startup_checks(self):
469529 )
470530
471531 # Wait until MATLAB is started before sending requests.
532+ self .log .debug ("Waiting until MATLAB is started" )
472533 timeout = 0
473534 while (
474535 self .matlab_status != "up"
@@ -477,6 +538,7 @@ def perform_startup_checks(self):
477538 ):
478539 if self .is_matlab_licensed :
479540 if timeout == 0 :
541+ self .log .debug ("Licensing completed. Clearing output area" )
480542 self .display_output (
481543 {"type" : "clear_output" , "content" : {"wait" : False }}
482544 )
@@ -500,9 +562,18 @@ def perform_startup_checks(self):
500562 # If MATLAB is not available after 15 seconds of licensing information
501563 # being available either through user input or through matlab-proxy cache,
502564 # then display connection error to the user.
503- if timeout == _MATLAB_STARTUP_TIMEOUT or self .matlab_proxy_has_error :
565+ if timeout == _MATLAB_STARTUP_TIMEOUT :
566+ self .log .error (
567+ f"MATLAB has not started after { _MATLAB_STARTUP_TIMEOUT } seconds."
568+ )
569+ raise MATLABConnectionError
570+
571+ if self .matlab_proxy_has_error :
572+ self .log .error ("matlab-proxy encountered error." )
504573 raise MATLABConnectionError
505574
575+ self .log .debug ("MATLAB is running, startup checks completed." )
576+
506577 def display_output (self , out ):
507578 """
508579 Common function to send execution outputs to Jupyter UI.
0 commit comments