runtimelib/
kernelspec.rs

1use serde::{Deserialize, Serialize};
2use std::ffi::OsStr;
3use std::path::{Path, PathBuf};
4use std::process::Stdio;
5
6use jupyter_protocol::JupyterKernelspec;
7
8use crate::{Result, RuntimeError};
9
10#[cfg(feature = "tokio-runtime")]
11use tokio::{fs, io::AsyncReadExt, process::Command};
12
13#[cfg(feature = "async-dispatcher-runtime")]
14use smol::process::Command;
15
16/// A pointer to a kernelspec directory, with name and specification
17#[derive(Serialize, Deserialize, Clone, Debug)]
18pub struct KernelspecDir {
19    pub kernel_name: String,
20    pub path: PathBuf,
21    pub kernelspec: JupyterKernelspec,
22}
23
24impl KernelspecDir {
25    pub fn command(
26        self,
27        connection_path: &Path,
28        stderr: Option<Stdio>,
29        stdout: Option<Stdio>,
30    ) -> Result<Command> {
31        let kernel_name = &self.kernel_name;
32
33        let argv = self.kernelspec.argv;
34        if argv.is_empty() {
35            return Err(RuntimeError::EmptyArgv {
36                kernel_name: kernel_name.to_owned(),
37            });
38        }
39
40        let mut cmd_builder = Command::new(&argv[0]);
41
42        let stdout = stdout.unwrap_or(Stdio::null());
43        let stderr = stderr.unwrap_or(Stdio::null());
44        cmd_builder
45            .stdin(Stdio::null())
46            .stdout(stdout)
47            .stderr(stderr);
48
49        for arg in &argv[1..] {
50            cmd_builder.arg(if arg == "{connection_file}" {
51                connection_path.as_os_str()
52            } else {
53                OsStr::new(arg)
54            });
55        }
56        if let Some(env) = self.kernelspec.env {
57            cmd_builder.envs(env);
58        }
59
60        Ok(cmd_builder)
61    }
62}
63
64// We look for files of the sort:
65//    `<datadir>/kernels/<kernel_name>/kernel.json`
66// But we must check through all the possible <datadir> to figure that out.
67//
68// For now, just use a combination of the standard system and user data directories.
69#[cfg(feature = "tokio-runtime")]
70pub async fn list_kernelspecs() -> Vec<KernelspecDir> {
71    let mut kernelspecs = Vec::new();
72    let data_dirs = crate::dirs::data_dirs();
73    for data_dir in data_dirs {
74        let mut specs = read_kernelspec_jsons(&data_dir).await;
75        kernelspecs.append(&mut specs);
76    }
77    kernelspecs
78}
79
80// Design choice here is to not report any errors, keep going if possible,
81// and skip any paths that don't have a kernels subdirectory.
82#[cfg(feature = "tokio-runtime")]
83pub async fn list_kernelspec_names_at(data_dir: &Path) -> Vec<String> {
84    let mut kernelspecs = Vec::new();
85    let kernels_dir = data_dir.join("kernels");
86    if let Ok(mut entries) = fs::read_dir(kernels_dir).await {
87        while let Ok(Some(entry)) = entries.next_entry().await {
88            if entry.path().is_dir() {
89                if let Some(kernel_name) = entry.file_name().to_str() {
90                    kernelspecs.push(kernel_name.to_string());
91                }
92            }
93        }
94    }
95    kernelspecs
96}
97
98// For a given data directory, return all the parsed kernelspecs and corresponding directories
99#[cfg(feature = "tokio-runtime")]
100pub async fn read_kernelspec_jsons(data_dir: &Path) -> Vec<KernelspecDir> {
101    let mut kernelspecs = Vec::new();
102    let kernel_names = list_kernelspec_names_at(data_dir).await;
103    for kernel_name in kernel_names {
104        let kernel_path = data_dir.join("kernels").join(&kernel_name);
105        if let Ok(jupyter_runtime) = read_kernelspec_json(&kernel_path.join("kernel.json")).await {
106            kernelspecs.push(KernelspecDir {
107                kernel_name,
108                path: kernel_path,
109                kernelspec: jupyter_runtime,
110            });
111        }
112    }
113    kernelspecs
114}
115
116#[cfg(feature = "tokio-runtime")]
117async fn read_kernelspec_json(json_file_path: &Path) -> Result<JupyterKernelspec> {
118    let mut file = fs::File::open(json_file_path).await?;
119    let mut contents = vec![];
120
121    file.read_to_end(&mut contents).await?;
122    let jupyter_runtime: JupyterKernelspec = serde_json::from_slice(&contents)?;
123    Ok(jupyter_runtime)
124}
125
126#[cfg(all(test, feature = "tokio-runtime"))]
127mod tests {
128    use super::*;
129
130    #[tokio::test]
131    async fn test_read_jupyter_runtime_config() {
132        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
133        d.push("tests/kernels/ir/kernel.json");
134        let jupyter_runtime = read_kernelspec_json(&d).await.unwrap();
135        assert_eq!(jupyter_runtime.display_name, "R");
136        assert_eq!(jupyter_runtime.language, "R");
137        assert!(jupyter_runtime
138            .env
139            .as_ref()
140            .unwrap()
141            .contains_key("R_LIBS_USER"));
142        assert_eq!(jupyter_runtime.env.as_ref().unwrap().len(), 1);
143        assert!(jupyter_runtime.metadata.is_none());
144        assert_eq!(jupyter_runtime.argv.len(), 6);
145        assert_eq!(jupyter_runtime.interrupt_mode, Some("signal".to_string()));
146    }
147
148    #[tokio::test]
149    async fn test_read_missing_config() {
150        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
151        d.push("tests/kernels/NONEXISTENT/kernel.json");
152        let jupyter_runtime = read_kernelspec_json(&d).await;
153        assert!(jupyter_runtime.is_err());
154    }
155
156    #[tokio::test]
157    async fn test_list_kernelspec_jsons() {
158        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
159        d.push("tests");
160        let kernelspecs = list_kernelspec_names_at(&d).await;
161        assert_eq!(kernelspecs.len(), 3);
162        assert!(kernelspecs.contains(&"ir".to_string()));
163        assert!(kernelspecs.contains(&"python3".to_string()));
164        assert!(kernelspecs.contains(&"rust".to_string()));
165    }
166
167    #[tokio::test]
168    async fn test_read_kernelspec_jsons() {
169        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
170        d.push("tests");
171        let kernels = read_kernelspec_jsons(&d).await;
172        assert_eq!(kernels.len(), 3);
173        let mut r_count = 0;
174        let mut python_count = 0;
175        let mut rust_count = 0;
176        for kerneldir in kernels {
177            let kernelspec = &kerneldir.kernelspec;
178            match kernelspec.display_name.as_str() {
179                "R" => {
180                    assert_eq!(kernelspec.language, "R");
181                    assert_eq!(kernelspec.argv.len(), 6);
182                    assert_eq!(kernelspec.interrupt_mode, Some("signal".to_string()));
183                    r_count += 1;
184                }
185                "Python 3" => {
186                    assert_eq!(kernelspec.language, "python");
187                    assert_eq!(kernelspec.argv.len(), 5);
188                    assert_eq!(kernelspec.interrupt_mode, None);
189                    python_count += 1;
190                }
191                "Rust" => {
192                    assert_eq!(kernelspec.language, "rust");
193                    assert_eq!(kernelspec.argv.len(), 3);
194                    assert_eq!(kernelspec.interrupt_mode, Some("message".to_string()));
195                    rust_count += 1;
196                }
197                _ => panic!("Unexpected kernelspec found: {}", &kernelspec.display_name),
198            }
199        }
200        assert_eq!(r_count, 1);
201        assert_eq!(python_count, 1);
202        assert_eq!(rust_count, 1);
203    }
204
205    #[tokio::test]
206    async fn list_nonexistent_kernelspec_datadir() {
207        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
208        d.push("tests/NOTHINGHERE");
209        let kernels = list_kernelspec_names_at(&d).await;
210        assert_eq!(kernels.len(), 0);
211    }
212}