use crate::cli::Args;
use anyhow::Error;
use futures_util::TryFutureExt;
use http_body_util::{BodyExt, Either, Full};
use hyper::{
body::{Bytes, Incoming},
header::CONTENT_LENGTH,
http::uri::Authority,
service::service_fn,
Method, Request, Response,
};
use hyper_util::{
client::legacy::{connect::HttpConnector, Client},
rt::{TokioExecutor, TokioIo},
server::conn::auto,
};
use serde_json::{json, Value};
use std::path::PathBuf;
use std::process::Child;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::RwLock;
const TAURI_OPTIONS: &str = "tauri:options";
type ResponseBody = Either<Full<Bytes>, Incoming>;
struct PluginState {
app_process: Option<Child>,
}
impl PluginState {
fn new() -> Self {
Self { app_process: None }
}
}
async fn handle_plugin(
client: Client<HttpConnector, Full<Bytes>>,
req: Request<Incoming>,
args: Args,
state: Arc<RwLock<PluginState>>,
) -> Result<Response<ResponseBody>, Error> {
if let (&Method::POST, "/session") = (req.method(), req.uri().path()) {
let (mut parts, body) = req.into_parts();
let body = body.collect().await?.to_bytes().to_vec();
let json: Value = serde_json::from_slice(&body)?;
let Some((app_path, app_args)) = extract_app_path_and_args(&json) else {
return Ok(error_response(
"session not created",
"Missing tauri:options.application",
));
};
if !app_path.as_os_str().is_empty() {
let mut state = state.write().await;
if state.app_process.is_some() {
if let Some(ref mut proc) = state.app_process {
let _ = proc.kill();
}
}
let mut cmd = std::process::Command::new(&app_path);
cmd.env("TAURI_AUTOMATION", "true")
.env("TAURI_WEBVIEW_AUTOMATION", "true")
.args(&app_args);
let child = cmd.spawn();
match child {
Ok(proc) => {
state.app_process = Some(proc);
drop(state);
let ready = wait_for_plugin(&args.native_host, args.native_port, 30).await;
if !ready {
return Ok(error_response(
"session not created",
"Plugin server not ready after timeout",
));
}
}
Err(e) => {
return Ok(error_response(
"session not created",
&format!("Failed to launch Tauri app: {e}"),
));
}
}
}
parts.headers.insert(CONTENT_LENGTH, body.len().into());
let new_req = Request::from_parts(parts, Full::new(body.into()));
return client
.request(forward_to_plugin(new_req, &args)?)
.map_ok(|resp| resp.map(Either::Right))
.err_into()
.await;
}
let is_session_delete = req.method() == Method::DELETE && {
let path = req.uri().path();
let parts: Vec<&str> = path.split('/').collect();
parts.len() == 3 && path.starts_with("/session/")
};
let (parts, body) = req.into_parts();
let body = body.collect().await?.to_bytes().to_vec();
let new_req = Request::from_parts(parts, Full::new(body.into()));
let response = client
.request(forward_to_plugin(new_req, &args)?)
.map_ok(|resp| resp.map(Either::Right))
.err_into()
.await;
if is_session_delete {
let mut state = state.write().await;
if let Some(ref mut proc) = state.app_process {
let _ = proc.kill();
}
state.app_process = None;
}
response
}
fn extract_app_path_and_args(json: &Value) -> Option<(PathBuf, Vec<String>)> {
let capabilities = json.get("capabilities")?;
let always_match = capabilities.get("alwaysMatch")?;
let tauri_options = always_match.get(TAURI_OPTIONS)?;
let application = tauri_options.get("application")?.as_str()?;
let args = tauri_options
.get("args")
.and_then(|v| v.as_array())
.map_or_else(Vec::new, |arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(std::string::ToString::to_string)
.collect()
});
Some((PathBuf::from(application), args))
}
fn forward_to_plugin(
mut req: Request<Full<Bytes>>,
args: &Args,
) -> Result<Request<Full<Bytes>>, Error> {
let host: Authority = {
let headers = req.headers_mut();
headers.remove("host").expect("hyper request has host")
}
.to_str()?
.parse()?;
let path = req
.uri()
.path_and_query()
.expect("hyper request has uri")
.clone();
let uri = format!(
"http://{}:{}{}",
host.host(),
args.native_port,
path.as_str()
);
let (mut parts, body) = req.into_parts();
parts.uri = uri.parse()?;
Ok(Request::from_parts(parts, body))
}
async fn wait_for_plugin(host: &str, port: u16, timeout_secs: u64) -> bool {
let client: Client<HttpConnector, Full<Bytes>> =
Client::builder(TokioExecutor::new()).build_http();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
let uri: hyper::Uri = format!("http://{host}:{port}/status")
.parse()
.expect("valid uri");
while std::time::Instant::now() < deadline {
let req = Request::builder()
.method(Method::GET)
.uri(uri.clone())
.header("Host", format!("{host}:{port}"))
.body(Full::new(Bytes::new()));
if let Ok(req) = req {
if let Ok(resp) = client.request(req).await {
if resp.status().is_success() {
return true;
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
false
}
fn error_response(error: &str, message: &str) -> Response<ResponseBody> {
let body = json!({
"value": {
"error": error,
"message": message
}
});
let bytes = serde_json::to_vec(&body).unwrap_or_else(|_| b"internal error".to_vec());
Response::builder()
.status(500)
.header("Content-Type", "application/json; charset=utf-8")
.header(CONTENT_LENGTH, bytes.len())
.body(Either::Left(Full::new(bytes.into())))
.unwrap_or_else(|_| Response::new(Either::Left(Full::new(Bytes::from("internal error")))))
}
#[tokio::main(flavor = "current_thread")]
pub async fn run_plugin_mode(args: Args) -> Result<(), Error> {
let state = Arc::new(RwLock::new(PluginState::new()));
#[cfg(unix)]
let (signals_handle, signals_task) = {
use futures_util::StreamExt;
use signal_hook::consts::signal::{SIGINT, SIGQUIT, SIGTERM};
let signals = signal_hook_tokio::Signals::new([SIGTERM, SIGINT, SIGQUIT])?;
let signals_handle = signals.handle();
let state_for_signal = state.clone();
let signals_task = tokio::spawn(async move {
let mut signals = signals.fuse();
if signals.next().await.is_some() {
let mut state = state_for_signal.write().await;
if let Some(ref mut proc) = state.app_process {
let _ = proc.kill();
}
std::process::exit(0);
}
});
(signals_handle, signals_task)
};
#[cfg(windows)]
let ctrl_c_task = {
let state_for_signal = state.clone();
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
let mut state = state_for_signal.write().await;
if let Some(ref mut proc) = state.app_process {
let _ = proc.kill();
}
std::process::exit(0);
}
})
};
let address = std::net::SocketAddr::from(([127, 0, 0, 1], args.port));
let client = Client::builder(TokioExecutor::new())
.http1_preserve_header_case(true)
.http1_title_case_headers(true)
.retry_canceled_requests(false)
.build_http();
println!("tauri-webdriver running on port {}", args.port);
println!("Plugin expected on port {}", args.native_port);
let srv = async move {
if let Ok(listener) = TcpListener::bind(address).await {
loop {
let client = client.clone();
let args = args.clone();
let state = state.clone();
if let Ok((stream, _)) = listener.accept().await {
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
if let Err(err) = auto::Builder::new(TokioExecutor::new())
.http1()
.title_case_headers(true)
.preserve_header_case(true)
.serve_connection(
io,
service_fn(|request| {
handle_plugin(
client.clone(),
request,
args.clone(),
state.clone(),
)
}),
)
.await
{
println!("Error serving connection: {err:?}");
}
});
} else {
println!("accept new stream fail, ignore here");
}
}
} else {
println!("can not listen to address: {address:?}");
}
};
srv.await;
#[cfg(unix)]
{
signals_handle.close();
signals_task.await?;
}
#[cfg(windows)]
{
ctrl_c_task.abort();
}
Ok(())
}