tauri_plugin_python/
lib.rs1use tauri::{
7 path::BaseDirectory,
8 plugin::{Builder, TauriPlugin},
9 AppHandle, Manager, Runtime,
10};
11
12#[cfg(desktop)]
13mod desktop;
14#[cfg(mobile)]
15mod mobile;
16
17mod commands;
18mod error;
19mod models;
20use async_py::{self, PyRunner};
21
22pub use error::{Error, Result};
23use models::*;
24use std::{
25 collections::HashSet,
26 path::PathBuf,
27 sync::{atomic::AtomicBool, Mutex},
28 time::Duration,
29};
30
31const DEFAULT_TIMEOUT_SECS: u64 = 300;
37
38const PY_STDIO_GUARD: &str = r#"import sys
47
48class _TauriSafeStream:
49 def __init__(self, real):
50 self._real = real
51 def write(self, data):
52 try:
53 if self._real is not None:
54 return self._real.write(data)
55 except Exception:
56 pass
57 return 0
58 def flush(self):
59 try:
60 if self._real is not None:
61 self._real.flush()
62 except Exception:
63 pass
64 def isatty(self):
65 try:
66 return bool(self._real is not None and self._real.isatty())
67 except Exception:
68 return False
69 def __getattr__(self, name):
70 return getattr(self._real, name)
71
72sys.stdout = _TauriSafeStream(getattr(sys, "stdout", None))
73sys.stderr = _TauriSafeStream(getattr(sys, "stderr", None))
74"#;
75
76fn build_runner() -> PyRunner {
79 let runner = PyRunner::new();
80 match std::env::var("TAURI_PLUGIN_PYTHON_TIMEOUT_SECS")
81 .ok()
82 .and_then(|v| v.trim().parse::<u64>().ok())
83 {
84 Some(0) => runner,
85 Some(secs) => runner.with_timeout(Duration::from_secs(secs)),
86 None => runner.with_timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)),
87 }
88}
89
90#[cfg(desktop)]
91use desktop::Python;
92#[cfg(mobile)]
93use mobile::Python;
94
95#[derive(Default)]
96struct PluginState {
97 init_blocked: AtomicBool,
98 function_map: Mutex<HashSet<String>>,
99}
100
101fn py_context<T, E: Into<Error>>(
107 result: std::result::Result<T, E>,
108 context: impl FnOnce() -> String,
109) -> crate::Result<T> {
110 result.map_err(|err| {
111 let msg = format!("{}: {}", context(), err.into());
112 #[cfg(debug_assertions)]
113 eprintln!("[tauri-plugin-python] {msg}");
114 Error::String(msg)
115 })
116}
117
118#[async_trait::async_trait]
121pub trait PythonExt<R: Runtime> {
122 fn python(&self) -> &Python<R>;
123 fn runner(&self) -> &PyRunner;
124 async fn run_python(&self, payload: StringRequest) -> crate::Result<StringResponse>;
125 async fn register_function(&self, payload: RegisterRequest) -> crate::Result<StringResponse>;
126 async fn call_function(&self, payload: RunRequest) -> crate::Result<StringResponse>;
127 async fn read_variable(&self, payload: StringRequest) -> crate::Result<StringResponse>;
128}
129
130#[async_trait::async_trait]
131impl<R: Runtime, T: Manager<R> + Sync> crate::PythonExt<R> for T {
132 fn python(&self) -> &Python<R> {
133 self.state::<Python<R>>().inner()
134 }
135 fn runner(&self) -> &PyRunner {
136 self.state::<PyRunner>().inner()
137 }
138 async fn run_python(&self, payload: StringRequest) -> crate::Result<StringResponse> {
139 py_context(self.runner().run(&payload.value).await, || {
140 "Error running Python code (runPython)".into()
141 })?;
142 Ok(StringResponse { value: "Ok".into() })
143 }
144
145 async fn register_function(&self, payload: RegisterRequest) -> crate::Result<StringResponse> {
146 let state = self.state::<PluginState>().inner();
147 if state
148 .init_blocked
149 .load(std::sync::atomic::Ordering::Relaxed)
150 {
151 return Err("Cannot register after function called".into());
152 }
153 let _tmp = py_context(
154 self.runner()
155 .read_variable(&payload.python_function_call)
156 .await,
157 || {
158 format!(
159 "Cannot register '{}': not found in Python (is it defined/imported in main.py?)",
160 payload.python_function_call
161 )
162 },
163 )?;
164 if let Some(num_args) = payload.number_of_args {
165 let py_analyze_sig = format!(
172 r#"
173try:
174 from inspect import signature
175 _tauri_param_count = len(signature({0}).parameters)
176except Exception:
177 _tauri_param_count = None
178if _tauri_param_count is not None and _tauri_param_count != {1}:
179 raise Exception("Function parameters don't match in 'registerFunction'")
180"#,
181 &payload.python_function_call, num_args
182 );
183 self.runner().run(&py_analyze_sig).await.map_err(|_| {
184 Error::String(format!(
185 "Function parameters don't match signature of {}.",
186 payload.python_function_call
187 ))
188 })?;
189 };
190 state
191 .function_map
192 .lock()
193 .unwrap()
194 .insert(payload.python_function_call.clone());
195 Ok(StringResponse { value: "Ok".into() })
196 }
197
198 async fn call_function(&self, payload: RunRequest) -> crate::Result<StringResponse> {
199 let state = self.state::<PluginState>().inner();
200 state
201 .init_blocked
202 .store(true, std::sync::atomic::Ordering::Relaxed);
203 let function_name = payload.function_name;
204 if state
205 .function_map
206 .lock()
207 .unwrap()
208 .get(&function_name)
209 .is_none()
210 {
211 return Err(Error::String(format!(
212 "Function {function_name} has not been registered yet"
213 )));
214 }
215 let py_res = py_context(
216 self.runner()
217 .call_function(&function_name, payload.args)
218 .await,
219 || format!("Error calling Python function '{function_name}'"),
220 )?;
221 let value = match py_res.as_str() {
222 Some(s) => s.to_string(),
223 None => py_res.to_string(),
224 };
225 Ok(StringResponse { value })
226 }
227
228 async fn read_variable(&self, payload: StringRequest) -> crate::Result<StringResponse> {
229 let py_res = py_context(self.runner().read_variable(&payload.value).await, || {
230 format!("Error reading Python variable '{}'", payload.value)
231 })?;
232 Ok(StringResponse {
233 value: py_res.to_string(),
234 })
235 }
236}
237
238fn get_resource_dir<R: Runtime>(app: &AppHandle<R>) -> PathBuf {
239 app.path()
240 .resolve("src-python", BaseDirectory::Resource)
241 .unwrap_or_default()
242}
243
244fn get_src_python_dir() -> PathBuf {
245 std::env::current_dir().unwrap().join("src-python")
246}
247
248pub fn init<R: Runtime>() -> TauriPlugin<R> {
250 init_and_register(vec![])
251}
252
253fn cleanup_path_for_python(path: &PathBuf) -> String {
254 dunce::canonicalize(path)
255 .unwrap()
256 .to_string_lossy()
257 .replace("\\", "/")
258}
259
260fn print_path_for_python(path: &PathBuf) -> String {
261 #[cfg(not(target_os = "windows"))]
262 {
263 format!("\"{}\"", cleanup_path_for_python(path))
264 }
265 #[cfg(target_os = "windows")]
266 {
267 format!("r\"{}\"", cleanup_path_for_python(path))
268 }
269}
270
271async fn init_python(runner: &PyRunner, dir: PathBuf) {
272 runner
274 .run(PY_STDIO_GUARD)
275 .await
276 .expect("ERROR: Error initializing python stdio");
277 let sys_pyth_dir = print_path_for_python(&dir);
278 let path_import = format!(
279 r#"import sys
280sys.path = sys.path + [{}]
281"#,
282 sys_pyth_dir,
283 );
284 runner
285 .run(&path_import)
286 .await
287 .expect("ERROR: Error setting python path");
288 #[cfg(feature = "venv")]
289 {
290 let venv_dir = dir.join(".venv").join("lib");
291 if venv_dir.exists() {
292 runner
293 .set_venv(venv_dir.as_path())
294 .await
295 .expect("ERROR: Error setting venv for python");
296 }
297 }
298}
299
300pub fn init_and_register<R: Runtime>(python_functions: Vec<&'static str>) -> TauriPlugin<R> {
302 Builder::new("python")
303 .invoke_handler(tauri::generate_handler![
304 commands::run_python,
305 commands::register_function,
306 commands::call_function,
307 commands::read_variable
308 ])
309 .setup(|app, api| {
310 #[cfg(mobile)]
311 let python = mobile::init(app, api)?;
312 #[cfg(desktop)]
313 let python = desktop::init(app, api)?;
314 app.manage(python);
315 let runner = build_runner();
316 app.manage(runner);
317 app.manage(PluginState::default());
318
319 let mut dir = get_resource_dir(app);
320 let mut main_py = dir.join("main.py");
321 if !main_py.exists() {
322 println!(
323 "Warning: 'src-tauri/main.py' seems not to be registered in 'tauri.conf.json'"
324 );
325 dir = get_src_python_dir();
326 main_py = dir.join("main.py");
327 }
328 tokio::runtime::Runtime::new()
329 .unwrap()
330 .block_on(async move {
331 let runner = app.state::<PyRunner>().inner();
332 init_python(runner, dir.to_path_buf()).await;
333 runner
334 .run_file(main_py.as_path())
335 .await
336 .expect("ERROR: Error running 'src-tauri/main.py'");
337 register_python_functions(
338 app,
339 python_functions.iter().map(|s| s.to_string()).collect(),
340 )
341 .await;
342 let functions = runner
343 .read_variable("_tauri_plugin_functions")
344 .await
345 .unwrap_or_default();
346 if let Ok(python_functions) = serde_json::from_value(functions) {
347 register_python_functions(app, python_functions).await;
348 }
349 });
350
351 Ok(())
352 })
353 .build()
354}
355
356async fn register_python_functions<R: Runtime>(app: &AppHandle<R>, python_functions: Vec<String>) {
357 for function_name in python_functions {
358 app.register_function(RegisterRequest {
359 python_function_call: function_name.clone(),
360 number_of_args: None,
361 })
362 .await
363 .unwrap();
364 }
365}
366
367#[cfg(test)]
368mod tests;