22
33from __future__ import annotations
44
5+ import json
6+ import subprocess
57import typing
68
79from marimo ._config .settings import GLOBAL_SETTINGS
8- from marimo ._messaging .types import KernelMessage
910from marimo ._runtime .requests import AppMetadata
1011from marimo ._server .model import SessionMode
1112from marimo ._server .sessions import KernelManager
12- from marimo ._utils .typed_connection import TypedConnection
13+
14+ from marimo_lsp .zeromq .queue_manager import encode_connection_info
1315
1416if typing .TYPE_CHECKING :
1517 from marimo ._config .manager import MarimoConfigManager
16- from marimo ._server .sessions import QueueManager
1718
1819 from marimo_lsp .app_file_manager import LspAppFileManager
19-
20-
21- def launch_kernel (* args ) -> None : # noqa: ANN002
22- """Launch the marimo kernel with the correct Python environment.
23-
24- Runs inside a `multiprocessing.Process` spawned with `ctx.set_executable()`.
25-
26- However, multiprocessing reconstructs the parent's `sys.path`, overriding the
27- venv's paths. We fix this by querying sys.executable (which IS correctly set)
28- for its natural `sys.path` and replacing ours before importing marimo.
29- """
30- import json # noqa: PLC0415
31- import subprocess # noqa: PLC0415
32- import sys # noqa: PLC0415
33-
34- # Get the natural sys.path from the venv's Python interpreter
35- # sys.executable is correctly set to the venv's Python thanks to set_executable()
36- result = subprocess .run ( # noqa: S603
37- [sys .executable , "-c" , "import sys, json; print(json.dumps(sys.path))" ],
38- capture_output = True ,
20+ from marimo_lsp .zeromq .queue_manager import ConnectionInfo , ZeroMqQueueManager
21+
22+
23+ def launch_kernel_subprocess (
24+ executable : str ,
25+ connection_info : ConnectionInfo ,
26+ configs : dict ,
27+ app_metadata : AppMetadata ,
28+ config_manager : MarimoConfigManager ,
29+ * ,
30+ virtual_files_supported : bool ,
31+ redirect_console_to_browser : bool ,
32+ profile_path : str | None ,
33+ ) -> subprocess .Popen :
34+ """Launch kernel as a subprocess with ZeroMQ IPC."""
35+
36+ # Prepare kernel arguments
37+ kernel_args = {
38+ "configs" : configs ,
39+ "app_metadata" : {
40+ "query_params" : app_metadata .query_params ,
41+ "filename" : app_metadata .filename ,
42+ "cli_args" : app_metadata .cli_args ,
43+ "argv" : app_metadata .argv ,
44+ "app_config" : app_metadata .app_config ,
45+ },
46+ "user_config" : config_manager .get_config (hide_secrets = False ),
47+ "virtual_files_supported" : virtual_files_supported ,
48+ "redirect_console_to_browser" : redirect_console_to_browser ,
49+ "profile_path" : profile_path ,
50+ "log_level" : GLOBAL_SETTINGS .LOG_LEVEL ,
51+ }
52+
53+ # Launch kernel subprocess
54+ kernel_cmd = [
55+ executable ,
56+ "-m" ,
57+ "marimo_lsp.zeromq.kernel_server" ,
58+ ]
59+
60+ process = subprocess .Popen (
61+ kernel_cmd ,
62+ stdin = subprocess .PIPE ,
63+ stdout = subprocess .PIPE ,
64+ stderr = subprocess .PIPE ,
3965 text = True ,
40- check = True ,
4166 )
4267
43- # Replace the inherited (wrong) sys.path with the venv's natural paths
44- sys .path = json .loads (result .stdout )
68+ assert process .stdin , "Expected stdin"
4569
46- # Now we can import marimo from the correct environment
47- from marimo ._runtime import runtime # noqa: PLC0415
70+ process .stdin .write (encode_connection_info (connection_info ) + "\n " )
71+ process .stdin .write (json .dumps (kernel_args ) + "\n " )
72+ process .stdin .flush ()
73+ process .stdin .close ()
4874
49- runtime . launch_kernel ( * args )
75+ return process
5076
5177
5278class LspKernelManager (KernelManager ):
@@ -56,7 +82,8 @@ def __init__(
5682 self ,
5783 * ,
5884 executable : str ,
59- queue_manager : QueueManager ,
85+ connection_info : ConnectionInfo ,
86+ queue_manager : ZeroMqQueueManager ,
6087 app_file_manager : LspAppFileManager ,
6188 config_manager : MarimoConfigManager ,
6289 ) -> None :
@@ -75,52 +102,26 @@ def __init__(
75102 redirect_console_to_browser = False ,
76103 virtual_files_supported = False ,
77104 )
105+ self .kernel_process = None
78106 self .executable = executable
107+ self .connection_info = connection_info
79108
80109 def start_kernel (self ) -> None :
81- """Start an instance of the marimo kernel."""
82- import multiprocessing as mp # noqa: PLC0415
83- from multiprocessing import connection # noqa: PLC0415
84-
85- # We use a process in edit mode so that we can interrupt the app
86- # with a SIGINT; we don't mind the additional memory consumption,
87- # since there's only one client sess
88- is_edit_mode = self .mode == SessionMode .EDIT
89-
90- # Need to use a socket for windows compatibility
91- listener = connection .Listener (family = "AF_INET" )
92-
93- ctx = mp .get_context ("spawn" )
94- ctx .set_executable (self .executable )
95-
96- kernel_task = ctx .Process (
97- target = launch_kernel ,
98- args = (
99- self .queue_manager .control_queue ,
100- self .queue_manager .set_ui_element_queue ,
101- self .queue_manager .completion_queue ,
102- self .queue_manager .input_queue ,
103- # stream queue unused
104- None ,
105- listener .address ,
106- is_edit_mode ,
107- self .configs ,
108- self .app_metadata ,
109- self .config_manager .get_config (hide_secrets = False ),
110- self ._virtual_files_supported ,
111- self .redirect_console_to_browser ,
112- self .queue_manager .win32_interrupt_queue ,
113- self .profile_path ,
114- GLOBAL_SETTINGS .LOG_LEVEL ,
115- ),
116- # The process can't be a daemon, because daemonic processes
117- # can't create children
118- # https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.daemon # noqa: E501
119- daemon = False ,
110+ """Start an instance of the marimo kernel using ZeroMQ IPC."""
111+ # Launch kernel subprocess with ZeroMQ
112+ self .kernel_process = launch_kernel_subprocess (
113+ executable = self .executable ,
114+ connection_info = self .connection_info ,
115+ configs = self .configs ,
116+ app_metadata = self .app_metadata ,
117+ config_manager = self .config_manager ,
118+ virtual_files_supported = self ._virtual_files_supported ,
119+ redirect_console_to_browser = self .redirect_console_to_browser ,
120+ profile_path = getattr (self , "profile_path" , None ),
120121 )
121122
122- kernel_task .start ()
123+ # Create IOPub connection for receiving kernel messages
124+ self ._read_conn = self .queue_manager .create_iopub_connection (for_kernel = False )
123125
124- self .kernel_task = kernel_task
125- # First thing kernel does is connect to the socket, so it's safe to call accept
126- self ._read_conn = TypedConnection [KernelMessage ].of (listener .accept ())
126+ # Store process handle (compatible with mp.Process interface)
127+ self .kernel_task = self .kernel_process
0 commit comments