mod.rs

  1mod native_kernel;
  2use std::{fmt::Debug, future::Future, path::PathBuf};
  3
  4use futures::{channel::mpsc, future::Shared};
  5use gpui::{App, Entity, Task, Window};
  6use language::LanguageName;
  7use log;
  8pub use native_kernel::*;
  9
 10mod remote_kernels;
 11use project::{Project, ProjectPath, Toolchains, WorktreeId};
 12use remote::RemoteConnectionOptions;
 13pub use remote_kernels::*;
 14
 15mod ssh_kernel;
 16pub use ssh_kernel::*;
 17
 18mod wsl_kernel;
 19pub use wsl_kernel::*;
 20
 21use std::collections::HashMap;
 22
 23use anyhow::Result;
 24use futures::{FutureExt, StreamExt};
 25use gpui::{AppContext, AsyncWindowContext, Context};
 26use jupyter_protocol::{JupyterKernelspec, JupyterMessageContent};
 27use runtimelib::{
 28    ClientControlConnection, ClientIoPubConnection, ClientShellConnection, ClientStdinConnection,
 29    ExecutionState, JupyterMessage, KernelInfoReply,
 30};
 31use ui::{Icon, IconName, SharedString};
 32use util::rel_path::RelPath;
 33
 34pub(crate) const VENV_DIR_NAMES: &[&str] = &[".venv", "venv", ".env", "env"];
 35
 36// Build a POSIX shell script that attempts to find and exec the best Python binary to run with the given arguments.
 37pub(crate) fn build_python_exec_shell_script(
 38    python_args: &str,
 39    cd_command: &str,
 40    env_command: &str,
 41) -> String {
 42    let venv_dirs = VENV_DIR_NAMES.join(" ");
 43    format!(
 44        "set -e; \
 45         {cd_command}\
 46         {env_command}\
 47         for venv_dir in {venv_dirs}; do \
 48           if [ -f \"$venv_dir/pyvenv.cfg\" ] || [ -f \"$venv_dir/bin/activate\" ]; then \
 49             if [ -x \"$venv_dir/bin/python\" ]; then \
 50               exec \"$venv_dir/bin/python\" {python_args}; \
 51             elif [ -x \"$venv_dir/bin/python3\" ]; then \
 52               exec \"$venv_dir/bin/python3\" {python_args}; \
 53             fi; \
 54           fi; \
 55         done; \
 56         if command -v python3 >/dev/null 2>&1; then \
 57           exec python3 {python_args}; \
 58         elif command -v python >/dev/null 2>&1; then \
 59           exec python {python_args}; \
 60         else \
 61           echo 'Error: Python not found in virtual environment or PATH' >&2; \
 62           exit 127; \
 63         fi"
 64    )
 65}
 66
 67/// Build a POSIX shell script that outputs the best Python binary.
 68#[cfg(target_os = "windows")]
 69pub(crate) fn build_python_discovery_shell_script() -> String {
 70    let venv_dirs = VENV_DIR_NAMES.join(" ");
 71    format!(
 72        "for venv_dir in {venv_dirs}; do \
 73           if [ -f \"$venv_dir/pyvenv.cfg\" ] || [ -f \"$venv_dir/bin/activate\" ]; then \
 74             if [ -x \"$venv_dir/bin/python\" ]; then \
 75               echo \"$venv_dir/bin/python\"; exit 0; \
 76             elif [ -x \"$venv_dir/bin/python3\" ]; then \
 77               echo \"$venv_dir/bin/python3\"; exit 0; \
 78             fi; \
 79           fi; \
 80         done; \
 81         if command -v python3 >/dev/null 2>&1; then \
 82           echo python3; exit 0; \
 83         elif command -v python >/dev/null 2>&1; then \
 84           echo python; exit 0; \
 85         fi; \
 86         exit 1"
 87    )
 88}
 89
 90pub fn start_kernel_tasks<S: KernelSession + 'static>(
 91    session: Entity<S>,
 92    iopub_socket: ClientIoPubConnection,
 93    shell_socket: ClientShellConnection,
 94    control_socket: ClientControlConnection,
 95    stdin_socket: ClientStdinConnection,
 96    cx: &mut AsyncWindowContext,
 97) -> (
 98    futures::channel::mpsc::Sender<JupyterMessage>,
 99    futures::channel::mpsc::Sender<JupyterMessage>,
100) {
101    let (mut shell_send, shell_recv) = shell_socket.split();
102    let (mut control_send, control_recv) = control_socket.split();
103    let (mut stdin_send, stdin_recv) = stdin_socket.split();
104
105    let (request_tx, mut request_rx) = futures::channel::mpsc::channel::<JupyterMessage>(100);
106    let (stdin_tx, mut stdin_rx) = futures::channel::mpsc::channel::<JupyterMessage>(100);
107
108    let recv_task = cx.spawn({
109        let session = session.clone();
110        let mut iopub = iopub_socket;
111        let mut shell = shell_recv;
112        let mut control = control_recv;
113        let mut stdin = stdin_recv;
114
115        async move |cx| -> anyhow::Result<()> {
116            loop {
117                let (channel, result) = futures::select! {
118                    msg = iopub.read().fuse() => ("iopub", msg),
119                    msg = shell.read().fuse() => ("shell", msg),
120                    msg = control.read().fuse() => ("control", msg),
121                    msg = stdin.read().fuse() => ("stdin", msg),
122                };
123                match result {
124                    Ok(message) => {
125                        session
126                            .update_in(cx, |session, window, cx| {
127                                session.route(&message, window, cx);
128                            })
129                            .ok();
130                    }
131                    Err(
132                        ref err @ (runtimelib::RuntimeError::ParseError { .. }
133                        | runtimelib::RuntimeError::SerdeError(_)),
134                    ) => {
135                        let error_detail = format!("Kernel issue on {channel} channel\n\n{err}");
136                        log::warn!("kernel: {error_detail}");
137                        session
138                            .update_in(cx, |session, _window, cx| {
139                                session.kernel_errored(error_detail, cx);
140                                cx.notify();
141                            })
142                            .ok();
143                    }
144                    Err(err) => {
145                        log::warn!("kernel: error reading from {channel}: {err:?}");
146                        anyhow::bail!("{channel} recv: {err}");
147                    }
148                }
149            }
150        }
151    });
152
153    let routing_task = cx.background_spawn(async move {
154        while let Some(message) = request_rx.next().await {
155            match message.content {
156                JupyterMessageContent::DebugRequest(_)
157                | JupyterMessageContent::InterruptRequest(_)
158                | JupyterMessageContent::ShutdownRequest(_) => {
159                    control_send.send(message).await?;
160                }
161                _ => {
162                    shell_send.send(message).await?;
163                }
164            }
165        }
166        anyhow::Ok(())
167    });
168
169    let stdin_routing_task = cx.background_spawn(async move {
170        while let Some(message) = stdin_rx.next().await {
171            stdin_send.send(message).await?;
172        }
173        anyhow::Ok(())
174    });
175
176    cx.spawn({
177        async move |cx| {
178            async fn with_name(
179                name: &'static str,
180                task: Task<Result<()>>,
181            ) -> (&'static str, Result<()>) {
182                (name, task.await)
183            }
184
185            let mut tasks = futures::stream::FuturesUnordered::new();
186            tasks.push(with_name("recv task", recv_task));
187            tasks.push(with_name("routing task", routing_task));
188            tasks.push(with_name("stdin routing task", stdin_routing_task));
189
190            while let Some((name, result)) = tasks.next().await {
191                if let Err(err) = result {
192                    session.update(cx, |session, cx| {
193                        session.kernel_errored(format!("handling failed for {name}: {err}"), cx);
194                        cx.notify();
195                    });
196                }
197            }
198        }
199    })
200    .detach();
201
202    (request_tx, stdin_tx)
203}
204
205pub trait KernelSession: Sized {
206    fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>);
207    fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>);
208}
209
210#[derive(Debug, Clone)]
211pub struct PythonEnvKernelSpecification {
212    pub name: String,
213    pub path: PathBuf,
214    pub kernelspec: JupyterKernelspec,
215    pub has_ipykernel: bool,
216    /// Display label for the environment type: "venv", "Conda", "Pyenv", etc.
217    pub environment_kind: Option<String>,
218}
219
220impl PartialEq for PythonEnvKernelSpecification {
221    fn eq(&self, other: &Self) -> bool {
222        self.name == other.name && self.path == other.path
223    }
224}
225
226impl Eq for PythonEnvKernelSpecification {}
227
228impl PythonEnvKernelSpecification {
229    pub fn as_local_spec(&self) -> LocalKernelSpecification {
230        LocalKernelSpecification {
231            name: self.name.clone(),
232            path: self.path.clone(),
233            kernelspec: self.kernelspec.clone(),
234        }
235    }
236
237    pub fn is_uv(&self) -> bool {
238        matches!(
239            self.environment_kind.as_deref(),
240            Some("uv" | "uv (Workspace)")
241        )
242    }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub enum KernelSpecification {
247    JupyterServer(RemoteKernelSpecification),
248    Jupyter(LocalKernelSpecification),
249    PythonEnv(PythonEnvKernelSpecification),
250    SshRemote(SshRemoteKernelSpecification),
251    WslRemote(WslKernelSpecification),
252}
253
254#[derive(Debug, Clone)]
255pub struct SshRemoteKernelSpecification {
256    pub name: String,
257    pub path: SharedString,
258    pub kernelspec: JupyterKernelspec,
259}
260
261#[derive(Debug, Clone)]
262pub struct WslKernelSpecification {
263    pub name: String,
264    pub kernelspec: JupyterKernelspec,
265    pub distro: String,
266}
267
268impl PartialEq for SshRemoteKernelSpecification {
269    fn eq(&self, other: &Self) -> bool {
270        self.name == other.name
271            && self.kernelspec.argv == other.kernelspec.argv
272            && self.path == other.path
273            && self.kernelspec.display_name == other.kernelspec.display_name
274            && self.kernelspec.language == other.kernelspec.language
275            && self.kernelspec.interrupt_mode == other.kernelspec.interrupt_mode
276            && self.kernelspec.env == other.kernelspec.env
277            && self.kernelspec.metadata == other.kernelspec.metadata
278    }
279}
280
281impl Eq for SshRemoteKernelSpecification {}
282
283impl PartialEq for WslKernelSpecification {
284    fn eq(&self, other: &Self) -> bool {
285        self.name == other.name
286            && self.kernelspec.argv == other.kernelspec.argv
287            && self.kernelspec.display_name == other.kernelspec.display_name
288            && self.kernelspec.language == other.kernelspec.language
289            && self.kernelspec.interrupt_mode == other.kernelspec.interrupt_mode
290            && self.kernelspec.env == other.kernelspec.env
291            && self.kernelspec.metadata == other.kernelspec.metadata
292            && self.distro == other.distro
293    }
294}
295
296impl Eq for WslKernelSpecification {}
297
298impl KernelSpecification {
299    pub fn name(&self) -> SharedString {
300        match self {
301            Self::Jupyter(spec) => spec.name.clone().into(),
302            Self::PythonEnv(spec) => spec.name.clone().into(),
303            Self::JupyterServer(spec) => spec.name.clone().into(),
304            Self::SshRemote(spec) => spec.name.clone().into(),
305            Self::WslRemote(spec) => spec.kernelspec.display_name.clone().into(),
306        }
307    }
308
309    pub fn type_name(&self) -> SharedString {
310        match self {
311            Self::Jupyter(_) => "Jupyter".into(),
312            Self::PythonEnv(spec) => SharedString::from(
313                spec.environment_kind
314                    .clone()
315                    .unwrap_or_else(|| "Python Environment".to_string()),
316            ),
317            Self::JupyterServer(_) => "Jupyter Server".into(),
318            Self::SshRemote(_) => "SSH Remote".into(),
319            Self::WslRemote(_) => "WSL Remote".into(),
320        }
321    }
322
323    pub fn path(&self) -> SharedString {
324        SharedString::from(match self {
325            Self::Jupyter(spec) => spec.path.to_string_lossy().into_owned(),
326            Self::PythonEnv(spec) => spec.path.to_string_lossy().into_owned(),
327            Self::JupyterServer(spec) => spec.url.to_string(),
328            Self::SshRemote(spec) => spec.path.to_string(),
329            Self::WslRemote(spec) => spec.distro.clone(),
330        })
331    }
332
333    pub fn language(&self) -> SharedString {
334        SharedString::from(match self {
335            Self::Jupyter(spec) => spec.kernelspec.language.clone(),
336            Self::PythonEnv(spec) => spec.kernelspec.language.clone(),
337            Self::JupyterServer(spec) => spec.kernelspec.language.clone(),
338            Self::SshRemote(spec) => spec.kernelspec.language.clone(),
339            Self::WslRemote(spec) => spec.kernelspec.language.clone(),
340        })
341    }
342
343    pub fn has_ipykernel(&self) -> bool {
344        match self {
345            Self::Jupyter(_) | Self::JupyterServer(_) | Self::SshRemote(_) | Self::WslRemote(_) => {
346                true
347            }
348            Self::PythonEnv(spec) => spec.has_ipykernel,
349        }
350    }
351
352    pub fn environment_kind_label(&self) -> Option<SharedString> {
353        match self {
354            Self::PythonEnv(spec) => spec
355                .environment_kind
356                .as_ref()
357                .map(|kind| SharedString::from(kind.clone())),
358            Self::Jupyter(_) => Some("Jupyter".into()),
359            Self::JupyterServer(_) => Some("Jupyter Server".into()),
360            Self::SshRemote(_) => Some("SSH Remote".into()),
361            Self::WslRemote(_) => Some("WSL Remote".into()),
362        }
363    }
364
365    pub fn icon(&self, cx: &App) -> Icon {
366        let lang_name = match self {
367            Self::Jupyter(spec) => spec.kernelspec.language.clone(),
368            Self::PythonEnv(spec) => spec.kernelspec.language.clone(),
369            Self::JupyterServer(spec) => spec.kernelspec.language.clone(),
370            Self::SshRemote(spec) => spec.kernelspec.language.clone(),
371            Self::WslRemote(spec) => spec.kernelspec.language.clone(),
372        };
373
374        file_icons::FileIcons::get(cx)
375            .get_icon_for_type(&lang_name.to_lowercase(), cx)
376            .map(Icon::from_path)
377            .unwrap_or(Icon::new(IconName::ReplNeutral))
378    }
379}
380
381fn extract_environment_kind(toolchain_json: &serde_json::Value) -> Option<String> {
382    let kind_str = toolchain_json.get("kind")?.as_str()?;
383    let label = match kind_str {
384        "Conda" => "Conda",
385        "Pixi" => "pixi",
386        "Homebrew" => "Homebrew",
387        "Pyenv" => "global (Pyenv)",
388        "GlobalPaths" => "global",
389        "PyenvVirtualEnv" => "Pyenv",
390        "Pipenv" => "Pipenv",
391        "Poetry" => "Poetry",
392        "MacPythonOrg" => "global (Python.org)",
393        "MacCommandLineTools" => "global (Command Line Tools for Xcode)",
394        "LinuxGlobal" => "global",
395        "MacXCode" => "global (Xcode)",
396        "Venv" => "venv",
397        "VirtualEnv" => "virtualenv",
398        "VirtualEnvWrapper" => "virtualenvwrapper",
399        "WindowsStore" => "global (Windows Store)",
400        "WindowsRegistry" => "global (Windows Registry)",
401        "Uv" => "uv",
402        "UvWorkspace" => "uv (Workspace)",
403        _ => kind_str,
404    };
405    Some(label.to_string())
406}
407
408pub fn python_env_kernel_specifications(
409    project: &Entity<Project>,
410    worktree_id: WorktreeId,
411    cx: &mut App,
412) -> impl Future<Output = Result<Vec<KernelSpecification>>> + use<> {
413    let python_language = LanguageName::new_static("Python");
414    let is_remote = project.read(cx).is_remote();
415    let wsl_distro = project
416        .read(cx)
417        .remote_connection_options(cx)
418        .and_then(|opts| {
419            if let RemoteConnectionOptions::Wsl(wsl) = opts {
420                Some(wsl.distro_name)
421            } else {
422                None
423            }
424        });
425
426    let toolchains = project.read(cx).available_toolchains(
427        ProjectPath {
428            worktree_id,
429            path: RelPath::empty().into(),
430        },
431        python_language,
432        cx,
433    );
434    #[allow(unused)]
435    let worktree_root_path: Option<std::sync::Arc<std::path::Path>> = project
436        .read(cx)
437        .worktree_for_id(worktree_id, cx)
438        .map(|w| w.read(cx).abs_path());
439
440    let background_executor = cx.background_executor().clone();
441
442    async move {
443        let (toolchains, user_toolchains) = if let Some(Toolchains {
444            toolchains,
445            root_path: _,
446            user_toolchains,
447        }) = toolchains.await
448        {
449            (toolchains, user_toolchains)
450        } else {
451            return Ok(Vec::new());
452        };
453
454        let kernelspecs = user_toolchains
455            .into_values()
456            .flatten()
457            .chain(toolchains.toolchains)
458            .map(|toolchain| {
459                let wsl_distro = wsl_distro.clone();
460                background_executor.spawn(async move {
461                    // For remote projects, we assume python is available assuming toolchain is reported.
462                    // We can skip the `ipykernel` check or run it remotely.
463                    // For MVP, lets trust the toolchain existence or do the check if it's cheap.
464                    // `new_smol_command` runs locally. We need to run remotely if `is_remote`.
465
466                    if is_remote {
467                        let default_kernelspec = JupyterKernelspec {
468                            argv: vec![
469                                toolchain.path.to_string(),
470                                "-m".to_string(),
471                                "ipykernel_launcher".to_string(),
472                                "-f".to_string(),
473                                "{connection_file}".to_string(),
474                            ],
475                            display_name: toolchain.name.to_string(),
476                            language: "python".to_string(),
477                            interrupt_mode: None,
478                            metadata: None,
479                            env: None,
480                        };
481
482                        if let Some(distro) = wsl_distro {
483                            log::debug!(
484                                "python_env_kernel_specifications: returning WslRemote for toolchain {}",
485                                toolchain.name
486                            );
487                            return Some(KernelSpecification::WslRemote(WslKernelSpecification {
488                                name: toolchain.name.to_string(),
489                                kernelspec: default_kernelspec,
490                                distro,
491                            }));
492                        }
493
494                        log::debug!(
495                            "python_env_kernel_specifications: returning SshRemote for toolchain {}",
496                            toolchain.name
497                        );
498                        return Some(KernelSpecification::SshRemote(
499                            SshRemoteKernelSpecification {
500                                name: format!("Remote {}", toolchain.name),
501                                path: toolchain.path.clone(),
502                                kernelspec: default_kernelspec,
503                            },
504                        ));
505                    }
506
507                    let python_path = toolchain.path.to_string();
508                    let environment_kind = extract_environment_kind(&toolchain.as_json);
509
510                    let has_ipykernel = util::command::new_command(&python_path)
511                        .args(&["-c", "import ipykernel"])
512                        .output()
513                        .await
514                        .map(|output| output.status.success())
515                        .unwrap_or(false);
516
517                    let mut env = HashMap::new();
518                    if let Some(python_bin_dir) = PathBuf::from(&python_path).parent() {
519                        if let Some(path_var) = std::env::var_os("PATH") {
520                            let mut paths = std::env::split_paths(&path_var).collect::<Vec<_>>();
521                            paths.insert(0, python_bin_dir.to_path_buf());
522                            if let Ok(new_path) = std::env::join_paths(paths) {
523                                env.insert("PATH".to_string(), new_path.to_string_lossy().to_string());
524                            }
525                        }
526
527                        if let Some(venv_root) = python_bin_dir.parent() {
528                            env.insert("VIRTUAL_ENV".to_string(), venv_root.to_string_lossy().to_string());
529                        }
530                    }
531
532                    log::info!("Preparing Python kernel for toolchain: {}", toolchain.name);
533                    log::info!("Python path: {}", python_path);
534                    if let Some(path) = env.get("PATH") {
535                         log::info!("Kernel PATH: {}", path);
536                    } else {
537                         log::info!("Kernel PATH not set in env");
538                    }
539                    if let Some(venv) = env.get("VIRTUAL_ENV") {
540                         log::info!("Kernel VIRTUAL_ENV: {}", venv);
541                    }
542
543                    let kernelspec = JupyterKernelspec {
544                        argv: vec![
545                            python_path.clone(),
546                            "-m".to_string(),
547                            "ipykernel_launcher".to_string(),
548                            "-f".to_string(),
549                            "{connection_file}".to_string(),
550                        ],
551                        display_name: toolchain.name.to_string(),
552                        language: "python".to_string(),
553                        interrupt_mode: None,
554                        metadata: None,
555                        env: Some(env),
556                    };
557
558                    Some(KernelSpecification::PythonEnv(PythonEnvKernelSpecification {
559                        name: toolchain.name.to_string(),
560                        path: PathBuf::from(&python_path),
561                        kernelspec,
562                        has_ipykernel,
563                        environment_kind,
564                    }))
565                })
566            });
567
568        #[allow(unused_mut)]
569        let mut kernel_specs: Vec<KernelSpecification> = futures::stream::iter(kernelspecs)
570            .buffer_unordered(4)
571            .filter_map(|x| async move { x })
572            .collect::<Vec<_>>()
573            .await;
574
575        #[cfg(target_os = "windows")]
576        if kernel_specs.is_empty() && !is_remote {
577            if let Some(root_path) = worktree_root_path {
578                let root_path_str: std::borrow::Cow<str> = root_path.to_string_lossy();
579                let (distro, internal_path) =
580                    if let Some(path_without_prefix) = root_path_str.strip_prefix(r"\\wsl$\") {
581                        if let Some((distro, path)) = path_without_prefix.split_once('\\') {
582                            let replaced_path: String = path.replace('\\', "/");
583                            (Some(distro), Some(format!("/{}", replaced_path)))
584                        } else {
585                            (Some(path_without_prefix), Some("/".to_string()))
586                        }
587                    } else if let Some(path_without_prefix) =
588                        root_path_str.strip_prefix(r"\\wsl.localhost\")
589                    {
590                        if let Some((distro, path)) = path_without_prefix.split_once('\\') {
591                            let replaced_path: String = path.replace('\\', "/");
592                            (Some(distro), Some(format!("/{}", replaced_path)))
593                        } else {
594                            (Some(path_without_prefix), Some("/".to_string()))
595                        }
596                    } else {
597                        (None, None)
598                    };
599
600                if let (Some(distro), Some(internal_path)) = (distro, internal_path) {
601                    let discovery_script = build_python_discovery_shell_script();
602                    let script = format!(
603                        "cd {} && {}",
604                        shlex::try_quote(&internal_path)
605                            .unwrap_or(std::borrow::Cow::Borrowed(&internal_path)),
606                        discovery_script
607                    );
608                    let output = util::command::new_command("wsl")
609                        .arg("-d")
610                        .arg(distro)
611                        .arg("bash")
612                        .arg("-l")
613                        .arg("-c")
614                        .arg(&script)
615                        .output()
616                        .await;
617
618                    if let Ok(output) = output {
619                        if output.status.success() {
620                            let python_cmd =
621                                String::from_utf8_lossy(&output.stdout).trim().to_string();
622                            let (python_path, display_suffix) = if python_cmd.contains('/') {
623                                let venv_name = python_cmd.split('/').next().unwrap_or("venv");
624                                (
625                                    format!("{}/{}", internal_path, python_cmd),
626                                    format!("({})", venv_name),
627                                )
628                            } else {
629                                (python_cmd, "(System)".to_string())
630                            };
631
632                            let display_name = format!("WSL: {} {}", distro, display_suffix);
633                            let default_kernelspec = JupyterKernelspec {
634                                argv: vec![
635                                    python_path,
636                                    "-m".to_string(),
637                                    "ipykernel_launcher".to_string(),
638                                    "-f".to_string(),
639                                    "{connection_file}".to_string(),
640                                ],
641                                display_name: display_name.clone(),
642                                language: "python".to_string(),
643                                interrupt_mode: None,
644                                metadata: None,
645                                env: None,
646                            };
647
648                            kernel_specs.push(KernelSpecification::WslRemote(
649                                WslKernelSpecification {
650                                    name: display_name,
651                                    kernelspec: default_kernelspec,
652                                    distro: distro.to_string(),
653                                },
654                            ));
655                        }
656                    }
657                }
658            }
659        }
660
661        anyhow::Ok(kernel_specs)
662    }
663}
664
665pub trait RunningKernel: Send + Debug {
666    fn request_tx(&self) -> mpsc::Sender<JupyterMessage>;
667    fn stdin_tx(&self) -> mpsc::Sender<JupyterMessage>;
668    fn working_directory(&self) -> &PathBuf;
669    fn execution_state(&self) -> &ExecutionState;
670    fn set_execution_state(&mut self, state: ExecutionState);
671    fn kernel_info(&self) -> Option<&KernelInfoReply>;
672    fn set_kernel_info(&mut self, info: KernelInfoReply);
673    fn force_shutdown(&mut self, window: &mut Window, cx: &mut App) -> Task<anyhow::Result<()>>;
674    fn kill(&mut self);
675}
676
677#[derive(Debug, Clone)]
678pub enum KernelStatus {
679    Idle,
680    Busy,
681    Starting,
682    Error,
683    ShuttingDown,
684    Shutdown,
685    Restarting,
686}
687
688impl KernelStatus {
689    pub fn is_connected(&self) -> bool {
690        matches!(self, KernelStatus::Idle | KernelStatus::Busy)
691    }
692}
693
694impl ToString for KernelStatus {
695    fn to_string(&self) -> String {
696        match self {
697            KernelStatus::Idle => "Idle".to_string(),
698            KernelStatus::Busy => "Busy".to_string(),
699            KernelStatus::Starting => "Starting".to_string(),
700            KernelStatus::Error => "Error".to_string(),
701            KernelStatus::ShuttingDown => "Shutting Down".to_string(),
702            KernelStatus::Shutdown => "Shutdown".to_string(),
703            KernelStatus::Restarting => "Restarting".to_string(),
704        }
705    }
706}
707
708#[derive(Debug)]
709pub enum Kernel {
710    RunningKernel(Box<dyn RunningKernel>),
711    StartingKernel(Shared<Task<()>>),
712    ErroredLaunch(String),
713    ShuttingDown,
714    Shutdown,
715    Restarting,
716}
717
718impl From<&Kernel> for KernelStatus {
719    fn from(kernel: &Kernel) -> Self {
720        match kernel {
721            Kernel::RunningKernel(kernel) => match kernel.execution_state() {
722                ExecutionState::Idle => KernelStatus::Idle,
723                ExecutionState::Busy => KernelStatus::Busy,
724                ExecutionState::Unknown => KernelStatus::Error,
725                ExecutionState::Starting => KernelStatus::Starting,
726                ExecutionState::Restarting => KernelStatus::Restarting,
727                ExecutionState::Terminating => KernelStatus::ShuttingDown,
728                ExecutionState::AutoRestarting => KernelStatus::Restarting,
729                ExecutionState::Dead => KernelStatus::Error,
730                ExecutionState::Other(_) => KernelStatus::Error,
731            },
732            Kernel::StartingKernel(_) => KernelStatus::Starting,
733            Kernel::ErroredLaunch(_) => KernelStatus::Error,
734            Kernel::ShuttingDown => KernelStatus::ShuttingDown,
735            Kernel::Shutdown => KernelStatus::Shutdown,
736            Kernel::Restarting => KernelStatus::Restarting,
737        }
738    }
739}
740
741impl Kernel {
742    pub fn status(&self) -> KernelStatus {
743        self.into()
744    }
745
746    pub fn set_execution_state(&mut self, status: &ExecutionState) {
747        if let Kernel::RunningKernel(running_kernel) = self {
748            running_kernel.set_execution_state(status.clone());
749        }
750    }
751
752    pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) {
753        if let Kernel::RunningKernel(running_kernel) = self {
754            running_kernel.set_kernel_info(kernel_info.clone());
755        }
756    }
757
758    pub fn is_shutting_down(&self) -> bool {
759        match self {
760            Kernel::Restarting | Kernel::ShuttingDown => true,
761            Kernel::RunningKernel(_)
762            | Kernel::StartingKernel(_)
763            | Kernel::ErroredLaunch(_)
764            | Kernel::Shutdown => false,
765        }
766    }
767}