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