mod server;
use devtools_core::aggregator::Aggregator;
use devtools_core::bridge_layer::BridgeLayer;
use devtools_core::layer::Layer;
use devtools_core::server::wire::tauri::tauri_server::TauriServer;
use devtools_core::server::{Server, ServerHandle};
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::{Manager, Runtime};
use tokio::sync::mpsc;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer as _;
#[cfg(target_os = "ios")]
mod ios {
use cocoa::base::id;
use objc::*;
const UTF8_ENCODING: usize = 4;
pub struct NSString(pub id);
impl NSString {
pub fn new(s: &str) -> Self {
NSString(unsafe {
let ns_string: id = msg_send![class!(NSString), alloc];
let ns_string: id = msg_send![ns_string,
initWithBytes:s.as_ptr()
length:s.len()
encoding:UTF8_ENCODING];
let _: () = msg_send![ns_string, autorelease];
ns_string
})
}
}
swift_rs::swift!(pub fn devtools_log(
level: u8, message: *const std::ffi::c_void
));
}
pub struct Devtools {
pub connection: ConnectionInfo,
pub server_handle: ServerHandle,
}
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, _api| {
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(),
},
);
let server_handle = server.handle();
app_handle.manage(Devtools {
connection: connection_info(&addr),
server_handle,
});
#[cfg(not(target_os = "ios"))]
print_link(&addr);
#[cfg(target_os = "ios")]
{
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(3));
print_link(&addr);
});
}
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,
bridge_layer: BridgeLayer,
}
impl Default for Builder {
fn default() -> Self {
Self {
#[cfg(any(target_os = "ios", target_os = "android"))]
host: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
#[cfg(not(any(target_os = "ios", target_os = "android")))]
host: IpAddr::V4(Ipv4Addr::LOCALHOST),
port: 3033,
publish_interval: Duration::from_millis(200),
strict_port: false,
bridge_layer: BridgeLayer::new(Vec::new()),
}
}
}
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 attach_logger(&mut self, logger: Box<dyn log::Log>) -> &mut Self {
self.bridge_layer.add_logger(logger);
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))
.with(
self.bridge_layer
.with_filter(tracing_subscriber::filter::LevelFilter::TRACE),
)
.try_init()
.map_err(devtools_core::Error::from)?;
let mut port = self.port;
if !self.strict_port && !port_is_available(&self.host, port) {
port = (1025..65535)
.find(|port| port_is_available(&self.host, *port))
.ok_or(Error::NoFreePorts)?;
}
let addr = SocketAddr::new(self.host, port);
let plugin = init_plugin(addr, self.publish_interval, aggregator, cmd_tx);
Ok(plugin)
}
}
fn port_is_available(host: &IpAddr, port: u16) -> bool {
TcpListener::bind(SocketAddr::new(*host, port)).is_ok()
}
pub struct ConnectionInfo {
pub host: IpAddr,
pub port: u16,
}
fn connection_info(addr: &SocketAddr) -> ConnectionInfo {
ConnectionInfo {
host: if addr.ip() == Ipv4Addr::UNSPECIFIED {
#[cfg(target_os = "ios")]
{
local_ip_address::list_afinet_netifas()
.and_then(|ifas| {
ifas.into_iter()
.find_map(|(name, addr)| {
if name == "en0" && !addr.is_loopback() && addr.is_ipv4() {
Some(addr)
} else {
None
}
})
.ok_or(local_ip_address::Error::LocalIpAddressNotFound)
})
.unwrap_or_else(|_| local_ip_address::local_ip().unwrap_or_else(|_| addr.ip()))
}
#[cfg(not(target_os = "ios"))]
{
local_ip_address::local_ip().unwrap_or_else(|_| addr.ip())
}
} else {
addr.ip()
},
port: addr.port(),
}
}
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 connection = connection_info(addr);
let url = format!("{url}{}/{}", connection.host, connection.port);
#[cfg(target_os = "ios")]
unsafe {
ios::devtools_log(
3,
ios::NSString::new(
format!(
r"
{} {}{}
{} Local: {}
",
"Tauri Devtools",
"v",
env!("CARGO_PKG_VERSION"),
"->",
url
)
.as_str(),
)
.0 as _,
);
}
#[cfg(not(target_os = "ios"))]
{
use colored::Colorize;
println!(
r"
{} {}{}
{} Local: {}
",
"Tauri Devtools".bright_purple(),
"v".purple(),
env!("CARGO_PKG_VERSION").purple(),
"→".bright_purple(),
url.underline().blue()
);
}
}