use tauri::{
path::BaseDirectory,
plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime,
};
#[cfg(desktop)]
mod desktop;
#[cfg(mobile)]
mod mobile;
mod commands;
mod error;
mod models;
use async_py::{self, PyRunner};
pub use error::{Error, Result};
use models::*;
use std::{
collections::HashSet,
path::PathBuf,
sync::{atomic::AtomicBool, Mutex},
time::Duration,
};
const DEFAULT_TIMEOUT_SECS: u64 = 300;
const PY_STDIO_GUARD: &str = r#"import sys
class _TauriSafeStream:
def __init__(self, real):
self._real = real
def write(self, data):
try:
if self._real is not None:
return self._real.write(data)
except Exception:
pass
return 0
def flush(self):
try:
if self._real is not None:
self._real.flush()
except Exception:
pass
def isatty(self):
try:
return bool(self._real is not None and self._real.isatty())
except Exception:
return False
def __getattr__(self, name):
return getattr(self._real, name)
sys.stdout = _TauriSafeStream(getattr(sys, "stdout", None))
sys.stderr = _TauriSafeStream(getattr(sys, "stderr", None))
"#;
fn build_runner() -> PyRunner {
let runner = PyRunner::new();
match std::env::var("TAURI_PLUGIN_PYTHON_TIMEOUT_SECS")
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
{
Some(0) => runner,
Some(secs) => runner.with_timeout(Duration::from_secs(secs)),
None => runner.with_timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)),
}
}
#[cfg(desktop)]
use desktop::Python;
#[cfg(mobile)]
use mobile::Python;
#[derive(Default)]
struct PluginState {
init_blocked: AtomicBool,
function_map: Mutex<HashSet<String>>,
}
fn py_context<T, E: Into<Error>>(
result: std::result::Result<T, E>,
context: impl FnOnce() -> String,
) -> crate::Result<T> {
result.map_err(|err| {
let msg = format!("{}: {}", context(), err.into());
#[cfg(debug_assertions)]
eprintln!("[tauri-plugin-python] {msg}");
Error::String(msg)
})
}
#[async_trait::async_trait]
pub trait PythonExt<R: Runtime> {
fn python(&self) -> &Python<R>;
fn runner(&self) -> &PyRunner;
async fn run_python(&self, payload: StringRequest) -> crate::Result<StringResponse>;
async fn register_function(&self, payload: RegisterRequest) -> crate::Result<StringResponse>;
async fn call_function(&self, payload: RunRequest) -> crate::Result<StringResponse>;
async fn read_variable(&self, payload: StringRequest) -> crate::Result<StringResponse>;
}
#[async_trait::async_trait]
impl<R: Runtime, T: Manager<R> + Sync> crate::PythonExt<R> for T {
fn python(&self) -> &Python<R> {
self.state::<Python<R>>().inner()
}
fn runner(&self) -> &PyRunner {
self.state::<PyRunner>().inner()
}
async fn run_python(&self, payload: StringRequest) -> crate::Result<StringResponse> {
py_context(self.runner().run(&payload.value).await, || {
"Error running Python code (runPython)".into()
})?;
Ok(StringResponse { value: "Ok".into() })
}
async fn register_function(&self, payload: RegisterRequest) -> crate::Result<StringResponse> {
let state = self.state::<PluginState>().inner();
if state
.init_blocked
.load(std::sync::atomic::Ordering::Relaxed)
{
return Err("Cannot register after function called".into());
}
let _tmp = py_context(
self.runner()
.read_variable(&payload.python_function_call)
.await,
|| {
format!(
"Cannot register '{}': not found in Python (is it defined/imported in main.py?)",
payload.python_function_call
)
},
)?;
if let Some(num_args) = payload.number_of_args {
let py_analyze_sig = format!(
r#"
try:
from inspect import signature
_tauri_param_count = len(signature({0}).parameters)
except Exception:
_tauri_param_count = None
if _tauri_param_count is not None and _tauri_param_count != {1}:
raise Exception("Function parameters don't match in 'registerFunction'")
"#,
&payload.python_function_call, num_args
);
self.runner().run(&py_analyze_sig).await.map_err(|_| {
Error::String(format!(
"Function parameters don't match signature of {}.",
payload.python_function_call
))
})?;
};
state
.function_map
.lock()
.unwrap()
.insert(payload.python_function_call.clone());
Ok(StringResponse { value: "Ok".into() })
}
async fn call_function(&self, payload: RunRequest) -> crate::Result<StringResponse> {
let state = self.state::<PluginState>().inner();
state
.init_blocked
.store(true, std::sync::atomic::Ordering::Relaxed);
let function_name = payload.function_name;
if state
.function_map
.lock()
.unwrap()
.get(&function_name)
.is_none()
{
return Err(Error::String(format!(
"Function {function_name} has not been registered yet"
)));
}
let py_res = py_context(
self.runner()
.call_function(&function_name, payload.args)
.await,
|| format!("Error calling Python function '{function_name}'"),
)?;
let value = match py_res.as_str() {
Some(s) => s.to_string(),
None => py_res.to_string(),
};
Ok(StringResponse { value })
}
async fn read_variable(&self, payload: StringRequest) -> crate::Result<StringResponse> {
let py_res = py_context(self.runner().read_variable(&payload.value).await, || {
format!("Error reading Python variable '{}'", payload.value)
})?;
Ok(StringResponse {
value: py_res.to_string(),
})
}
}
fn get_resource_dir<R: Runtime>(app: &AppHandle<R>) -> PathBuf {
app.path()
.resolve("src-python", BaseDirectory::Resource)
.unwrap_or_default()
}
fn get_src_python_dir() -> PathBuf {
std::env::current_dir().unwrap().join("src-python")
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
init_and_register(vec![])
}
fn cleanup_path_for_python(path: &PathBuf) -> String {
dunce::canonicalize(path)
.unwrap()
.to_string_lossy()
.replace("\\", "/")
}
fn print_path_for_python(path: &PathBuf) -> String {
#[cfg(not(target_os = "windows"))]
{
format!("\"{}\"", cleanup_path_for_python(path))
}
#[cfg(target_os = "windows")]
{
format!("r\"{}\"", cleanup_path_for_python(path))
}
}
async fn init_python(runner: &PyRunner, dir: PathBuf) {
runner
.run(PY_STDIO_GUARD)
.await
.expect("ERROR: Error initializing python stdio");
let sys_pyth_dir = print_path_for_python(&dir);
let path_import = format!(
r#"import sys
sys.path = sys.path + [{}]
"#,
sys_pyth_dir,
);
runner
.run(&path_import)
.await
.expect("ERROR: Error setting python path");
#[cfg(feature = "venv")]
{
let venv_dir = dir.join(".venv").join("lib");
if venv_dir.exists() {
runner
.set_venv(venv_dir.as_path())
.await
.expect("ERROR: Error setting venv for python");
}
}
}
pub fn init_and_register<R: Runtime>(python_functions: Vec<&'static str>) -> TauriPlugin<R> {
Builder::new("python")
.invoke_handler(tauri::generate_handler![
commands::run_python,
commands::register_function,
commands::call_function,
commands::read_variable
])
.setup(|app, api| {
#[cfg(mobile)]
let python = mobile::init(app, api)?;
#[cfg(desktop)]
let python = desktop::init(app, api)?;
app.manage(python);
let runner = build_runner();
app.manage(runner);
app.manage(PluginState::default());
let mut dir = get_resource_dir(app);
let mut main_py = dir.join("main.py");
if !main_py.exists() {
println!(
"Warning: 'src-tauri/main.py' seems not to be registered in 'tauri.conf.json'"
);
dir = get_src_python_dir();
main_py = dir.join("main.py");
}
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let runner = app.state::<PyRunner>().inner();
init_python(runner, dir.to_path_buf()).await;
runner
.run_file(main_py.as_path())
.await
.expect("ERROR: Error running 'src-tauri/main.py'");
register_python_functions(
app,
python_functions.iter().map(|s| s.to_string()).collect(),
)
.await;
let functions = runner
.read_variable("_tauri_plugin_functions")
.await
.unwrap_or_default();
if let Ok(python_functions) = serde_json::from_value(functions) {
register_python_functions(app, python_functions).await;
}
});
Ok(())
})
.build()
}
async fn register_python_functions<R: Runtime>(app: &AppHandle<R>, python_functions: Vec<String>) {
for function_name in python_functions {
app.register_function(RegisterRequest {
python_function_call: function_name.clone(),
number_of_args: None,
})
.await
.unwrap();
}
}
#[cfg(test)]
mod tests;