mod server;
use colored::Colorize;
use devtools_core::aggregator::Aggregator;
use devtools_core::layer::Layer;
use devtools_core::server::wire::tauri::tauri_server::TauriServer;
use devtools_core::server::Server;
use devtools_core::Command;
pub use devtools_core::Error;
use devtools_core::{Result, Shared};
use futures::FutureExt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use tauri::Runtime;
use tokio::sync::mpsc;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer as _;
fn init_plugin<R: Runtime>(
addr: SocketAddr,
publish_interval: Duration,
aggregator: Aggregator,
cmd_tx: mpsc::Sender<Command>,
) -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("probe")
.setup(move |app_handle| {
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
health_reporter
.set_serving::<TauriServer<server::TauriService<R>>>()
.now_or_never()
.unwrap();
let server = Server::new(
cmd_tx,
health_reporter,
health_service,
server::TauriService {
app_handle: app_handle.clone(),
},
server::MetaService {
app_handle: app_handle.clone(),
},
server::SourcesService {
app_handle: app_handle.clone(),
},
);
thread::spawn(move || {
use tracing_subscriber::EnvFilter;
let s = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.finish();
let _subscriber_guard = tracing::subscriber::set_default(s);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let aggregator = tokio::spawn(aggregator.run(publish_interval));
server.run(addr).await.unwrap();
aggregator.abort();
});
});
Ok(())
})
.build()
}
#[must_use = "This function returns a TauriPlugin that needs to be added to the Tauri app in order to properly instrument it."]
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
Builder::default().init()
}
#[must_use = "This function returns a TauriPlugin that needs to be added to the Tauri app in order to properly instrument it."]
pub fn try_init<R: Runtime>() -> Result<tauri::plugin::TauriPlugin<R>> {
Builder::default().try_init()
}
pub struct Builder {
host: IpAddr,
port: u16,
publish_interval: Duration,
strict_port: bool,
}
impl Default for Builder {
fn default() -> Self {
Self {
host: IpAddr::V4(Ipv4Addr::LOCALHOST),
port: 3033,
publish_interval: Duration::from_millis(200),
strict_port: false,
}
}
}
impl Builder {
pub fn host(&mut self, host: IpAddr) -> &mut Self {
self.host = host;
self
}
pub fn port(&mut self, port: u16) -> &mut Self {
self.port = port;
self
}
pub fn strict_port(&mut self, strict: bool) -> &mut Self {
self.strict_port = strict;
self
}
pub fn publish_interval(&mut self, interval: Duration) -> &mut Self {
self.publish_interval = interval;
self
}
#[must_use = "This function returns a TauriPlugin that needs to be added to the Tauri app in order to properly instrument it."]
pub fn init<R: Runtime>(self) -> tauri::plugin::TauriPlugin<R> {
self.try_init().unwrap()
}
#[must_use = "This function returns a TauriPlugin that needs to be added to the Tauri app in order to properly instrument it."]
pub fn try_init<R: Runtime>(self) -> Result<tauri::plugin::TauriPlugin<R>> {
let shared = Arc::new(Shared::default());
let (event_tx, event_rx) = mpsc::channel(512);
let (cmd_tx, cmd_rx) = mpsc::channel(256);
let layer = Layer::new(shared.clone(), event_tx);
let aggregator = Aggregator::new(shared, event_rx, cmd_rx);
tracing_subscriber::registry()
.with(layer.with_filter(tracing_subscriber::filter::LevelFilter::TRACE))
.try_init()?;
let mut port = self.port;
if !self.strict_port && !port_is_available(port) {
port = (1025..65535)
.find(|port| port_is_available(*port))
.ok_or(Error::NoFreePorts)?;
}
let addr = SocketAddr::new(self.host, port);
print_link(&addr);
let plugin = init_plugin(addr, self.publish_interval, aggregator, cmd_tx);
Ok(plugin)
}
}
fn port_is_available(port: u16) -> bool {
TcpListener::bind(("127.0.0.1", port)).is_ok()
}
fn print_link(addr: &SocketAddr) {
let url = if option_env!("__DEVTOOLS_LOCAL_DEVELOPMENT").is_some() {
"http://localhost:5173/dash/"
} else {
"https://devtools.crabnebula.dev/dash/"
};
let url = format!("{url}{}/{}", addr.ip(), addr.port());
println!(
r#"
{} {}{}
{} Local: {}
"#,
"Tauri Devtools".bright_purple(),
"v".purple(),
env!("CARGO_PKG_VERSION").purple(),
"→".bright_purple(),
url.underline().blue()
);
}