22# Implementation of MATLAB Kernel
33
44# Import Python Standard Library
5+ import asyncio
6+ import http
57import os
68import sys
79import time
810
9- # Import Third-Party Dependencies
11+ # Import Dependencies
12+ import aiohttp
13+ import aiohttp .client_exceptions
1014import ipykernel .kernelbase
1115import psutil
1216import 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
1419
15- # Import Dependencies
16- from jupyter_matlab_kernel import mwi_comm_helpers , mwi_logger
20+ from jupyter_matlab_kernel import mwi_logger
1721from jupyter_matlab_kernel .magic_execution_engine import (
1822 MagicExecutionEngine ,
1923 get_completion_result_for_magics ,
2024)
25+ from jupyter_matlab_kernel .mwi_comm_helpers import MWICommHelper
2126from 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
2427
2528_MATLAB_STARTUP_TIMEOUT = mwi_settings .get_process_startup_timeout ()
2629_logger = mwi_logger .get ()
@@ -101,7 +104,7 @@ def _start_matlab_proxy_using_jupyter(url, headers, logger=_logger):
101104 logger .debug (f"Received status code: { resp .status_code } " )
102105
103106 return (
104- resp .status_code == requests . codes .OK
107+ resp .status_code == http . HTTPStatus .OK
105108 and matlab_proxy_index_page_identifier in resp .text
106109 )
107110
@@ -223,7 +226,7 @@ def start_matlab_proxy(logger=_logger):
223226 """
224227 Error: MATLAB Kernel could not communicate with MATLAB.
225228 Reason: Possibly due to invalid jupyter security tokens.
226- """
229+ """
227230 )
228231
229232
@@ -242,12 +245,8 @@ class MATLABKernel(ipykernel.kernelbase.Kernel):
242245 }
243246
244247 # MATLAB Kernel state
245- murl = ""
246- is_matlab_licensed : bool = False
247- matlab_status = ""
248- matlab_proxy_has_error : bool = False
248+ kernel_id = ""
249249 server_base_url = ""
250- headers = dict ()
251250 startup_error = None
252251 startup_checks_completed : bool = False
253252
@@ -257,21 +256,26 @@ def __init__(self, *args, **kwargs):
257256
258257 # Update log instance with kernel id. This helps in identifying logs from
259258 # 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+
262263 # Initialize the Magic Execution Engine.
263264 self .magic_engine = MagicExecutionEngine (self .log )
264265
265266 try :
266267 # 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
274277 )
278+ shell_loop .run_until_complete (self .mwi_comm_helper .connect ())
275279 except MATLABConnectionError as err :
276280 self .startup_error = err
277281
@@ -286,9 +290,7 @@ async def interrupt_request(self, stream, ident, parent):
286290 self .log .debug ("Received interrupt request from Jupyter" )
287291 try :
288292 # 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 ()
292294
293295 # Set the response to interrupt request.
294296 content = {"status" : "ok" }
@@ -329,7 +331,7 @@ def handle_magic_output(self, output, outputs=None):
329331 # Storing the magic outputs to display them after startup_check completes.
330332 outputs .append (output )
331333
332- def do_execute (
334+ async def do_execute (
333335 self ,
334336 code ,
335337 silent ,
@@ -360,7 +362,7 @@ def do_execute(
360362 # Blocking call, returns after MATLAB is started.
361363 if not skip_cell_execution :
362364 if not self .startup_checks_completed :
363- self .perform_startup_checks ()
365+ await self .perform_startup_checks ()
364366 self .display_output (
365367 {
366368 "type" : "stream" ,
@@ -383,8 +385,8 @@ def do_execute(
383385
384386 # Perform execution and categorization of outputs in MATLAB. Blocks
385387 # 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
388390 )
389391
390392 if performed_startup_checks and not accumulated_magic_outputs :
@@ -414,9 +416,12 @@ def do_execute(
414416 self .log .error (
415417 f"Exception occurred while processing execution request:\n { e } "
416418 )
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
420425 # meaningful error message to the user
421426 e = MATLABConnectionError ()
422427
@@ -446,7 +451,7 @@ def do_execute(
446451 "user_expressions" : {},
447452 }
448453
449- def do_complete (self , code , cursor_pos ):
454+ async def do_complete (self , code , cursor_pos ):
450455 """
451456 Used by ipykernel infrastructure for tab completion. For more info, look
452457 at https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion
@@ -482,12 +487,17 @@ def do_complete(self, code, cursor_pos):
482487 completion_results = magic_completion_results
483488 else :
484489 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+ )
487494 )
488- except (MATLABConnectionError , HTTPError ) as e :
495+ except (
496+ MATLABConnectionError ,
497+ aiohttp .client_exceptions .ClientResponseError ,
498+ ) as e :
489499 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 } "
491501 )
492502
493503 self .log .debug (
@@ -504,15 +514,15 @@ def do_complete(self, code, cursor_pos):
504514 },
505515 }
506516
507- def do_is_complete (self , code ):
517+ async def do_is_complete (self , code ):
508518 # TODO: Seems like indentation rules. https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness
509519 return super ().do_is_complete (code )
510520
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 = ...):
512522 # TODO: Implement Shift+Tab functionality. Can be used to provide any contextual information.
513523 return super ().do_inspect (code , cursor_pos , detail_level , omit_sections )
514524
515- def do_history (
525+ async def do_history (
516526 self ,
517527 hist_access_type ,
518528 output ,
@@ -530,13 +540,15 @@ def do_history(
530540 hist_access_type , output , raw , session , start , stop , n , pattern , unique
531541 )
532542
533- def do_shutdown (self , restart ):
543+ async def do_shutdown (self , restart ):
534544 self .log .debug ("Received shutdown request from Jupyter" )
535545 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 :
540552 self .log .error (
541553 f"Exception occurred while sending shutdown request to MATLAB:\n { e } "
542554 )
@@ -545,13 +557,13 @@ def do_shutdown(self, restart):
545557
546558 # Helper functions
547559
548- def perform_startup_checks (self ):
560+ async def perform_startup_checks (self ):
549561 """
550562 One time checks triggered during the first execution request. Displays
551563 login window if matlab is not licensed using matlab-proxy.
552564
553565 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
555567 communicate with MATLAB.
556568 """
557569 self .log .debug ("Performing startup checks" )
@@ -561,10 +573,10 @@ def perform_startup_checks(self):
561573 raise self .startup_error
562574
563575 (
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 ( )
568580
569581 # Display iframe containing matlab-proxy to show login window if MATLAB
570582 # is not licensed using matlab-proxy. The iframe is removed after MATLAB
@@ -576,7 +588,7 @@ def perform_startup_checks(self):
576588 # as other browser based Jupyter clients.
577589 #
578590 # 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 :
580592 self .log .debug (
581593 "MATLAB is not licensed. Displaying HTML output to enable licensing."
582594 )
@@ -596,11 +608,11 @@ def perform_startup_checks(self):
596608 self .log .debug ("Waiting until MATLAB is started" )
597609 timeout = 0
598610 while (
599- self . matlab_status != "up"
611+ matlab_status != "up"
600612 and timeout != _MATLAB_STARTUP_TIMEOUT
601- and not self . matlab_proxy_has_error
613+ and not matlab_proxy_has_error
602614 ):
603- if self . is_matlab_licensed :
615+ if is_matlab_licensed :
604616 if timeout == 0 :
605617 self .log .debug ("Licensing completed. Clearing output area" )
606618 self .display_output (
@@ -618,10 +630,10 @@ def perform_startup_checks(self):
618630 timeout += 1
619631 time .sleep (1 )
620632 (
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 ( )
625637
626638 # If MATLAB is not available after 15 seconds of licensing information
627639 # being available either through user input or through matlab-proxy cache,
@@ -632,7 +644,7 @@ def perform_startup_checks(self):
632644 )
633645 raise MATLABConnectionError
634646
635- if self . matlab_proxy_has_error :
647+ if matlab_proxy_has_error :
636648 self .log .error ("matlab-proxy encountered error." )
637649 raise MATLABConnectionError
638650
0 commit comments