use std::io;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use frust_devtools_protocol::{FrameStats, format_discovery_line};
use tokio::net::TcpListener;
use tokio::runtime::Builder;
use tokio::sync::watch;
use crate::backend::{AppInfo, DevtoolsBackend};
use crate::dispatch::SessionCtx;
use crate::frame_stats::{self, FrameStatsBus};
use crate::hop::spawn_backend_thread;
use crate::server::accept_loop;
use crate::token;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceConfig {
pub backend_timeout: Duration,
pub frame_stats_capacity: usize,
pub max_clients: usize,
pub backend_queue_depth: usize,
pub require_token: bool,
}
impl Default for ServiceConfig {
fn default() -> Self {
Self {
backend_timeout: Duration::from_millis(1_000),
frame_stats_capacity: frame_stats::DEFAULT_CAPACITY,
max_clients: 4,
backend_queue_depth: 16,
require_token: true,
}
}
}
pub struct Service;
impl Service {
pub fn start<B: DevtoolsBackend>(backend: B, app: AppInfo) -> io::Result<ServiceHandle> {
Service::start_with_config(backend, app, ServiceConfig::default())
}
pub fn start_with_config<B: DevtoolsBackend>(
backend: B,
app: AppInfo,
config: ServiceConfig,
) -> io::Result<ServiceHandle> {
let handshake = backend.handshake_info(&app);
let runtime = Builder::new_current_thread()
.enable_io()
.enable_time()
.thread_name("frust-devtools")
.build()?;
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let listener = runtime.block_on(TcpListener::bind(addr))?;
let port = listener.local_addr()?.port();
let token = config.require_token.then(token::generate);
log::info!("{}", format_discovery_line(port, token.as_deref()));
let bus = Arc::new(FrameStatsBus::new(config.frame_stats_capacity));
let backend_client =
spawn_backend_thread(backend, config.backend_queue_depth, config.backend_timeout);
let ctx = Arc::new(SessionCtx {
handshake,
backend: backend_client,
bus: Arc::clone(&bus),
token: token.clone(),
});
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let max_clients = config.max_clients;
let driver = std::thread::Builder::new()
.name("frust-devtools".to_string())
.spawn(move || {
runtime.block_on(accept_loop(listener, ctx, shutdown_rx, max_clients));
})?;
Ok(ServiceHandle {
port,
token,
bus,
shutdown_tx,
driver: Some(driver),
})
}
}
pub struct ServiceHandle {
port: u16,
token: Option<String>,
bus: Arc<FrameStatsBus>,
shutdown_tx: watch::Sender<bool>,
driver: Option<std::thread::JoinHandle<()>>,
}
impl ServiceHandle {
pub fn port(&self) -> u16 {
self.port
}
pub fn token(&self) -> Option<&str> {
self.token.as_deref()
}
pub fn publish_frame_stats(&self, stats: FrameStats) {
self.bus.publish(stats);
}
pub fn shutdown(mut self) {
self.shutdown_inner();
}
fn shutdown_inner(&mut self) {
let _ = self.shutdown_tx.send(true);
if let Some(driver) = self.driver.take()
&& driver.join().is_err()
{
log::error!("frust-devtools: the service thread panicked");
}
}
}
impl Drop for ServiceHandle {
fn drop(&mut self) {
self.shutdown_inner();
}
}
impl std::fmt::Debug for ServiceHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceHandle")
.field("port", &self.port)
.finish_non_exhaustive()
}
}