runtimelib/
kernelspec.rs

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