1
- # Copyright 2023 The MathWorks, Inc.
1
+ # Copyright 2023-2024 The MathWorks, Inc.
2
2
# Implementation of MATLAB Kernel
3
3
4
4
# Import Python Standard Library
10
10
import ipykernel .kernelbase
11
11
import psutil
12
12
import 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
17
14
from matlab_proxy import settings as mwi_settings
18
-
15
+ from matlab_proxy import util as mwi_util
16
+ from requests .exceptions import HTTPError
19
17
20
18
_MATLAB_STARTUP_TIMEOUT = mwi_settings .get_process_startup_timeout ()
19
+ _logger = mwi_logger .get ()
21
20
22
21
23
22
class MATLABConnectionError (Exception ):
@@ -46,7 +45,7 @@ def is_jupyter_testing_enabled():
46
45
return os .environ .get ("MWI_JUPYTER_TEST" , "false" ).lower () == "true"
47
46
48
47
49
- def start_matlab_proxy_for_testing ():
48
+ def start_matlab_proxy_for_testing (logger = _logger ):
50
49
"""
51
50
Only used for testing purposes. Gets the matlab-proxy server configuration
52
51
from environment variables and mocks the 'start_matlab_proxy' function
@@ -66,6 +65,8 @@ def start_matlab_proxy_for_testing():
66
65
matlab_proxy_base_url = os .environ [mwi_env .get_env_name_base_url ()]
67
66
matlab_proxy_app_port = os .environ [mwi_env .get_env_name_app_port ()]
68
67
68
+ logger .debug ("Creating matlab-proxy URL for MATLABKernel testing." )
69
+
69
70
# '127.0.0.1' is used instead 'localhost' for testing since Windows machines consume
70
71
# some time to resolve 'localhost' hostname
71
72
url = "{protocol}://127.0.0.1:{port}{base_url}" .format (
@@ -74,10 +75,14 @@ def start_matlab_proxy_for_testing():
74
75
base_url = matlab_proxy_base_url ,
75
76
)
76
77
headers = {}
78
+
79
+ logger .debug (f"matlab-proxy URL: { url } " )
80
+ logger .debug (f"headers: { headers } " )
81
+
77
82
return url , matlab_proxy_base_url , headers
78
83
79
84
80
- def _start_matlab_proxy_using_jupyter (url , headers ):
85
+ def _start_matlab_proxy_using_jupyter (url , headers , logger = _logger ):
81
86
"""
82
87
Start matlab-proxy using jupyter server which started the current kernel
83
88
process by sending HTTP request to the endpoint registered through
@@ -94,16 +99,21 @@ def _start_matlab_proxy_using_jupyter(url, headers):
94
99
# can be used to validate a proper response.
95
100
matlab_proxy_index_page_identifier = "MWI_MATLAB_PROXY_IDENTIFIER"
96
101
102
+ logger .debug (
103
+ f"Sending request to jupyter to start matlab-proxy at { url } with headers: { headers } "
104
+ )
97
105
# send request to the matlab-proxy endpoint to make sure it is available.
98
106
# If matlab-proxy is not started, jupyter-server starts it at this point.
99
107
resp = requests .get (url , headers = headers , verify = False )
108
+ logger .debug (f"Received status code: { resp .status_code } " )
109
+
100
110
return (
101
111
resp .status_code == requests .codes .OK
102
112
and matlab_proxy_index_page_identifier in resp .text
103
113
)
104
114
105
115
106
- def start_matlab_proxy ():
116
+ def start_matlab_proxy (logger = _logger ):
107
117
"""
108
118
Start matlab-proxy registered with the jupyter server which started the
109
119
current kernel process.
@@ -122,7 +132,7 @@ def start_matlab_proxy():
122
132
# launched by the tests and kernel would expect the configurations of this matlab-proxy
123
133
# server which is provided through environment variables to 'start_matlab_proxy_for_testing'
124
134
if is_jupyter_testing_enabled ():
125
- return start_matlab_proxy_for_testing ()
135
+ return start_matlab_proxy_for_testing (logger )
126
136
127
137
nb_server_list = []
128
138
@@ -150,17 +160,21 @@ def start_matlab_proxy():
150
160
if mwi_util .system .is_windows () and is_virtual_env :
151
161
jupyter_server_pid = psutil .Process (jupyter_server_pid ).ppid ()
152
162
163
+ logger .debug (f"Resolved jupyter server pid: { jupyter_server_pid } " )
164
+
153
165
nb_server = dict ()
154
166
found_nb_server = False
155
167
for server in nb_server_list :
156
168
if server ["pid" ] == jupyter_server_pid :
169
+ logger .debug ("Jupyter server associated with this MATLAB Kernel found." )
157
170
found_nb_server = True
158
171
nb_server = server
159
172
# Stop iterating over the server list
160
173
break
161
174
162
175
# Error out if the server is not found!
163
176
if found_nb_server == False :
177
+ logger .error ("Jupyter server associated with this MATLABKernel not found." )
164
178
raise MATLABConnectionError (
165
179
"""
166
180
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():
170
184
171
185
# Verify that Password is disabled
172
186
if nb_server ["password" ] is True :
187
+ logger .error ("Jupyter server uses password for authentication." )
173
188
# TODO: To support passwords, we either need to acquire it from Jupyter or ask the user?
174
189
raise MATLABConnectionError (
175
190
"""
@@ -198,9 +213,16 @@ def start_matlab_proxy():
198
213
else :
199
214
headers = None
200
215
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
+ )
202
220
return url , nb_server ["base_url" ], headers
203
221
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 } " )
204
226
raise MATLABConnectionError (
205
227
"""
206
228
Error: MATLAB Kernel could not communicate with MATLAB.
@@ -236,14 +258,22 @@ class MATLABKernel(ipykernel.kernelbase.Kernel):
236
258
def __init__ (self , * args , ** kwargs ):
237
259
# Call superclass constructor to initialize ipykernel infrastructure
238
260
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
+
239
267
try :
240
268
# 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 )
242
270
(
243
271
self .is_matlab_licensed ,
244
272
self .matlab_status ,
245
273
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
+ )
247
277
except MATLABConnectionError as err :
248
278
self .startup_error = err
249
279
@@ -255,14 +285,20 @@ async def interrupt_request(self, stream, ident, parent):
255
285
Custom handling of interrupt request sent by Jupyter. For more info, look at
256
286
https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt
257
287
"""
288
+ self .log .debug ("Received interrupt request from Jupyter" )
258
289
try :
259
290
# 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
+ )
261
294
262
295
# Set the response to interrupt request.
263
296
content = {"status" : "ok" }
264
297
except Exception as e :
265
298
# 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
+ )
266
302
content = {
267
303
"status" : "error" ,
268
304
"ename" : str (type (e ).__name__ ),
@@ -286,6 +322,7 @@ def do_execute(
286
322
Used by ipykernel infrastructure for execution. For more info, look at
287
323
https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute
288
324
"""
325
+ self .log .debug (f"Received execution request from Jupyter with code:\n { code } " )
289
326
try :
290
327
# Complete one-time startup checks before sending request to MATLAB.
291
328
# Blocking call, returns after MATLAB is started.
@@ -305,20 +342,30 @@ def do_execute(
305
342
# Perform execution and categorization of outputs in MATLAB. Blocks
306
343
# until execution results are received from MATLAB.
307
344
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"
309
350
)
310
351
311
352
# Clear the output area of the current cell. This removes any previous
312
353
# outputs before publishing new outputs.
313
354
self .display_output ({"type" : "clear_output" , "content" : {"wait" : False }})
314
355
315
356
# 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
+
317
361
# Ignore empty values returned from MATLAB.
318
362
if not data :
319
363
continue
320
364
self .display_output (data )
321
365
except Exception as e :
366
+ self .log .error (
367
+ f"Exception occurred while processing execution request:\n { e } "
368
+ )
322
369
if isinstance (e , HTTPError ):
323
370
# If exception is an HTTPError, it means MATLAB is unavailable.
324
371
# Replace the HTTPError with MATLABConnectionError to give
@@ -356,6 +403,9 @@ def do_complete(self, code, cursor_pos):
356
403
user. For example, if matlab-proxy is not licensed, we cannot show
357
404
the licensing window.
358
405
"""
406
+ self .log .debug (
407
+ f"Received completion request from Jupyter with cursor position { cursor_pos } and code:\n { code } "
408
+ )
359
409
# Default completion results. It is modelled after ipkernel.py#do_complete
360
410
# implementation to provide metadata for JupyterLab.
361
411
completion_results = {
@@ -369,12 +419,16 @@ def do_complete(self, code, cursor_pos):
369
419
# results are received from MATLAB or communication with MATLAB fails.
370
420
try :
371
421
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 } "
373
427
)
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
+ )
378
432
379
433
return {
380
434
"status" : "ok" ,
@@ -413,14 +467,15 @@ def do_history(
413
467
)
414
468
415
469
def do_shutdown (self , restart ):
470
+ self .log .debug ("Received shutdown request from Jupyter" )
416
471
try :
417
472
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 } "
419
478
)
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
424
479
425
480
return super ().do_shutdown (restart )
426
481
@@ -435,8 +490,10 @@ def perform_startup_checks(self):
435
490
HTTPError, MATLABConnectionError: Occurs when matlab-proxy is not started or kernel cannot
436
491
communicate with MATLAB.
437
492
"""
493
+ self .log .debug ("Performing startup checks" )
438
494
# Incase an error occurred while kernel initialization, display it to the user.
439
495
if self .startup_error is not None :
496
+ self .log .error (f"Found a startup error: { self .startup_error } " )
440
497
raise self .startup_error
441
498
442
499
(
@@ -456,6 +513,9 @@ def perform_startup_checks(self):
456
513
#
457
514
# TODO: Find a workaround for users to be able to use our Jupyter kernel in VS Code.
458
515
if not self .is_matlab_licensed :
516
+ self .log .debug (
517
+ "MATLAB is not licensed. Displaying HTML output to enable licensing."
518
+ )
459
519
self .display_output (
460
520
{
461
521
"type" : "display_data" ,
@@ -469,6 +529,7 @@ def perform_startup_checks(self):
469
529
)
470
530
471
531
# Wait until MATLAB is started before sending requests.
532
+ self .log .debug ("Waiting until MATLAB is started" )
472
533
timeout = 0
473
534
while (
474
535
self .matlab_status != "up"
@@ -477,6 +538,7 @@ def perform_startup_checks(self):
477
538
):
478
539
if self .is_matlab_licensed :
479
540
if timeout == 0 :
541
+ self .log .debug ("Licensing completed. Clearing output area" )
480
542
self .display_output (
481
543
{"type" : "clear_output" , "content" : {"wait" : False }}
482
544
)
@@ -500,9 +562,18 @@ def perform_startup_checks(self):
500
562
# If MATLAB is not available after 15 seconds of licensing information
501
563
# being available either through user input or through matlab-proxy cache,
502
564
# 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." )
504
573
raise MATLABConnectionError
505
574
575
+ self .log .debug ("MATLAB is running, startup checks completed." )
576
+
506
577
def display_output (self , out ):
507
578
"""
508
579
Common function to send execution outputs to Jupyter UI.
0 commit comments