2
2
# Implementation of MATLAB Kernel
3
3
4
4
# Import Python Standard Library
5
+ import asyncio
6
+ import http
5
7
import os
6
8
import sys
7
9
import time
8
10
9
- # Import Third-Party Dependencies
11
+ # Import Dependencies
12
+ import aiohttp
13
+ import aiohttp .client_exceptions
10
14
import ipykernel .kernelbase
11
15
import psutil
12
16
import requests
13
- from requests .exceptions import HTTPError
17
+ from matlab_proxy import settings as mwi_settings
18
+ from matlab_proxy import util as mwi_util
14
19
15
- # Import Dependencies
16
- from jupyter_matlab_kernel import mwi_comm_helpers , mwi_logger
20
+ from jupyter_matlab_kernel import mwi_logger
17
21
from jupyter_matlab_kernel .magic_execution_engine import (
18
22
MagicExecutionEngine ,
19
23
get_completion_result_for_magics ,
20
24
)
25
+ from jupyter_matlab_kernel .mwi_comm_helpers import MWICommHelper
21
26
from jupyter_matlab_kernel .mwi_exceptions import MATLABConnectionError
22
- from matlab_proxy import settings as mwi_settings
23
- from matlab_proxy import util as mwi_util
24
27
25
28
_MATLAB_STARTUP_TIMEOUT = mwi_settings .get_process_startup_timeout ()
26
29
_logger = mwi_logger .get ()
@@ -101,7 +104,7 @@ def _start_matlab_proxy_using_jupyter(url, headers, logger=_logger):
101
104
logger .debug (f"Received status code: { resp .status_code } " )
102
105
103
106
return (
104
- resp .status_code == requests . codes .OK
107
+ resp .status_code == http . HTTPStatus .OK
105
108
and matlab_proxy_index_page_identifier in resp .text
106
109
)
107
110
@@ -223,7 +226,7 @@ def start_matlab_proxy(logger=_logger):
223
226
"""
224
227
Error: MATLAB Kernel could not communicate with MATLAB.
225
228
Reason: Possibly due to invalid jupyter security tokens.
226
- """
229
+ """
227
230
)
228
231
229
232
@@ -242,12 +245,8 @@ class MATLABKernel(ipykernel.kernelbase.Kernel):
242
245
}
243
246
244
247
# MATLAB Kernel state
245
- murl = ""
246
- is_matlab_licensed : bool = False
247
- matlab_status = ""
248
- matlab_proxy_has_error : bool = False
248
+ kernel_id = ""
249
249
server_base_url = ""
250
- headers = dict ()
251
250
startup_error = None
252
251
startup_checks_completed : bool = False
253
252
@@ -257,21 +256,26 @@ def __init__(self, *args, **kwargs):
257
256
258
257
# Update log instance with kernel id. This helps in identifying logs from
259
258
# multiple kernels which are running simultaneously
260
- self .log .debug (f"Initializing kernel with id: { self .ident } " )
261
- self .log = self .log .getChild (f"{ self .ident } " )
259
+ self .kernel_id = self .ident
260
+ self .log .debug (f"Initializing kernel with id: { self .kernel_id } " )
261
+ self .log = self .log .getChild (f"{ self .kernel_id } " )
262
+
262
263
# Initialize the Magic Execution Engine.
263
264
self .magic_engine = MagicExecutionEngine (self .log )
264
265
265
266
try :
266
267
# Start matlab-proxy using the jupyter-matlab-proxy registered endpoint.
267
- self .murl , self .server_base_url , self .headers = start_matlab_proxy (self .log )
268
- (
269
- self .is_matlab_licensed ,
270
- self .matlab_status ,
271
- self .matlab_proxy_has_error ,
272
- ) = mwi_comm_helpers .fetch_matlab_proxy_status (
273
- self .murl , self .headers , self .log
268
+ murl , self .server_base_url , headers = start_matlab_proxy (self .log )
269
+
270
+ # Using asyncio.get_event_loop for shell_loop as io_loop variable is
271
+ # not yet initialized because start() is called after the __init__
272
+ # is completed.
273
+ shell_loop = asyncio .get_event_loop ()
274
+ control_loop = self .control_thread .io_loop .asyncio_loop
275
+ self .mwi_comm_helper = MWICommHelper (
276
+ self .kernel_id , murl , shell_loop , control_loop , headers , self .log
274
277
)
278
+ shell_loop .run_until_complete (self .mwi_comm_helper .connect ())
275
279
except MATLABConnectionError as err :
276
280
self .startup_error = err
277
281
@@ -286,9 +290,7 @@ async def interrupt_request(self, stream, ident, parent):
286
290
self .log .debug ("Received interrupt request from Jupyter" )
287
291
try :
288
292
# Send interrupt request to MATLAB
289
- mwi_comm_helpers .send_interrupt_request_to_matlab (
290
- self .murl , self .headers , self .log
291
- )
293
+ await self .mwi_comm_helper .send_interrupt_request_to_matlab ()
292
294
293
295
# Set the response to interrupt request.
294
296
content = {"status" : "ok" }
@@ -329,7 +331,7 @@ def handle_magic_output(self, output, outputs=None):
329
331
# Storing the magic outputs to display them after startup_check completes.
330
332
outputs .append (output )
331
333
332
- def do_execute (
334
+ async def do_execute (
333
335
self ,
334
336
code ,
335
337
silent ,
@@ -360,7 +362,7 @@ def do_execute(
360
362
# Blocking call, returns after MATLAB is started.
361
363
if not skip_cell_execution :
362
364
if not self .startup_checks_completed :
363
- self .perform_startup_checks ()
365
+ await self .perform_startup_checks ()
364
366
self .display_output (
365
367
{
366
368
"type" : "stream" ,
@@ -383,8 +385,8 @@ def do_execute(
383
385
384
386
# Perform execution and categorization of outputs in MATLAB. Blocks
385
387
# until execution results are received from MATLAB.
386
- outputs = mwi_comm_helpers .send_execution_request_to_matlab (
387
- self . murl , self . headers , code , self . ident , self . log
388
+ outputs = await self . mwi_comm_helper .send_execution_request_to_matlab (
389
+ code
388
390
)
389
391
390
392
if performed_startup_checks and not accumulated_magic_outputs :
@@ -414,9 +416,12 @@ def do_execute(
414
416
self .log .error (
415
417
f"Exception occurred while processing execution request:\n { e } "
416
418
)
417
- if isinstance (e , HTTPError ):
418
- # If exception is an HTTPError, it means MATLAB is unavailable.
419
- # Replace the HTTPError with MATLABConnectionError to give
419
+ if isinstance (e , aiohttp .client_exceptions .ClientError ):
420
+ # Log the ClientError for debugging
421
+ self .log .error (e )
422
+
423
+ # If exception is an ClientError, it means MATLAB is unavailable.
424
+ # Replace the ClientError with MATLABConnectionError to give
420
425
# meaningful error message to the user
421
426
e = MATLABConnectionError ()
422
427
@@ -446,7 +451,7 @@ def do_execute(
446
451
"user_expressions" : {},
447
452
}
448
453
449
- def do_complete (self , code , cursor_pos ):
454
+ async def do_complete (self , code , cursor_pos ):
450
455
"""
451
456
Used by ipykernel infrastructure for tab completion. For more info, look
452
457
at https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion
@@ -482,12 +487,17 @@ def do_complete(self, code, cursor_pos):
482
487
completion_results = magic_completion_results
483
488
else :
484
489
try :
485
- completion_results = mwi_comm_helpers .send_completion_request_to_matlab (
486
- self .murl , self .headers , code , cursor_pos , self .log
490
+ completion_results = (
491
+ await self .mwi_comm_helper .send_completion_request_to_matlab (
492
+ code , cursor_pos
493
+ )
487
494
)
488
- except (MATLABConnectionError , HTTPError ) as e :
495
+ except (
496
+ MATLABConnectionError ,
497
+ aiohttp .client_exceptions .ClientResponseError ,
498
+ ) as e :
489
499
self .log .error (
490
- f"Exception occurred while sending shutdown request to MATLAB:\n { e } "
500
+ f"Exception occurred while sending completion request to MATLAB:\n { e } "
491
501
)
492
502
493
503
self .log .debug (
@@ -504,15 +514,15 @@ def do_complete(self, code, cursor_pos):
504
514
},
505
515
}
506
516
507
- def do_is_complete (self , code ):
517
+ async def do_is_complete (self , code ):
508
518
# TODO: Seems like indentation rules. https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness
509
519
return super ().do_is_complete (code )
510
520
511
- def do_inspect (self , code , cursor_pos , detail_level = 0 , omit_sections = ...):
521
+ async def do_inspect (self , code , cursor_pos , detail_level = 0 , omit_sections = ...):
512
522
# TODO: Implement Shift+Tab functionality. Can be used to provide any contextual information.
513
523
return super ().do_inspect (code , cursor_pos , detail_level , omit_sections )
514
524
515
- def do_history (
525
+ async def do_history (
516
526
self ,
517
527
hist_access_type ,
518
528
output ,
@@ -530,13 +540,15 @@ def do_history(
530
540
hist_access_type , output , raw , session , start , stop , n , pattern , unique
531
541
)
532
542
533
- def do_shutdown (self , restart ):
543
+ async def do_shutdown (self , restart ):
534
544
self .log .debug ("Received shutdown request from Jupyter" )
535
545
try :
536
- mwi_comm_helpers .send_shutdown_request_to_matlab (
537
- self .murl , self .headers , self .ident , self .log
538
- )
539
- except (MATLABConnectionError , HTTPError ) as e :
546
+ await self .mwi_comm_helper .send_shutdown_request_to_matlab ()
547
+ await self .mwi_comm_helper .disconnect ()
548
+ except (
549
+ MATLABConnectionError ,
550
+ aiohttp .client_exceptions .ClientResponseError ,
551
+ ) as e :
540
552
self .log .error (
541
553
f"Exception occurred while sending shutdown request to MATLAB:\n { e } "
542
554
)
@@ -545,13 +557,13 @@ def do_shutdown(self, restart):
545
557
546
558
# Helper functions
547
559
548
- def perform_startup_checks (self ):
560
+ async def perform_startup_checks (self ):
549
561
"""
550
562
One time checks triggered during the first execution request. Displays
551
563
login window if matlab is not licensed using matlab-proxy.
552
564
553
565
Raises:
554
- HTTPError , MATLABConnectionError: Occurs when matlab-proxy is not started or kernel cannot
566
+ ClientError , MATLABConnectionError: Occurs when matlab-proxy is not started or kernel cannot
555
567
communicate with MATLAB.
556
568
"""
557
569
self .log .debug ("Performing startup checks" )
@@ -561,10 +573,10 @@ def perform_startup_checks(self):
561
573
raise self .startup_error
562
574
563
575
(
564
- self . is_matlab_licensed ,
565
- self . matlab_status ,
566
- self . matlab_proxy_has_error ,
567
- ) = mwi_comm_helpers . fetch_matlab_proxy_status ( self . murl , self .headers )
576
+ is_matlab_licensed ,
577
+ matlab_status ,
578
+ matlab_proxy_has_error ,
579
+ ) = await self .mwi_comm_helper . fetch_matlab_proxy_status ( )
568
580
569
581
# Display iframe containing matlab-proxy to show login window if MATLAB
570
582
# is not licensed using matlab-proxy. The iframe is removed after MATLAB
@@ -576,7 +588,7 @@ def perform_startup_checks(self):
576
588
# as other browser based Jupyter clients.
577
589
#
578
590
# TODO: Find a workaround for users to be able to use our Jupyter kernel in VS Code.
579
- if not self . is_matlab_licensed :
591
+ if not is_matlab_licensed :
580
592
self .log .debug (
581
593
"MATLAB is not licensed. Displaying HTML output to enable licensing."
582
594
)
@@ -596,11 +608,11 @@ def perform_startup_checks(self):
596
608
self .log .debug ("Waiting until MATLAB is started" )
597
609
timeout = 0
598
610
while (
599
- self . matlab_status != "up"
611
+ matlab_status != "up"
600
612
and timeout != _MATLAB_STARTUP_TIMEOUT
601
- and not self . matlab_proxy_has_error
613
+ and not matlab_proxy_has_error
602
614
):
603
- if self . is_matlab_licensed :
615
+ if is_matlab_licensed :
604
616
if timeout == 0 :
605
617
self .log .debug ("Licensing completed. Clearing output area" )
606
618
self .display_output (
@@ -618,10 +630,10 @@ def perform_startup_checks(self):
618
630
timeout += 1
619
631
time .sleep (1 )
620
632
(
621
- self . is_matlab_licensed ,
622
- self . matlab_status ,
623
- self . matlab_proxy_has_error ,
624
- ) = mwi_comm_helpers . fetch_matlab_proxy_status ( self . murl , self .headers )
633
+ is_matlab_licensed ,
634
+ matlab_status ,
635
+ matlab_proxy_has_error ,
636
+ ) = await self .mwi_comm_helper . fetch_matlab_proxy_status ( )
625
637
626
638
# If MATLAB is not available after 15 seconds of licensing information
627
639
# being available either through user input or through matlab-proxy cache,
@@ -632,7 +644,7 @@ def perform_startup_checks(self):
632
644
)
633
645
raise MATLABConnectionError
634
646
635
- if self . matlab_proxy_has_error :
647
+ if matlab_proxy_has_error :
636
648
self .log .error ("matlab-proxy encountered error." )
637
649
raise MATLABConnectionError
638
650
0 commit comments