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#[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#[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#[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#[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}