tail-fin-daemon 0.7.8

Long-running browser-session daemon for tail-fin (tfd binary). Keeps Chrome tabs warm across invocations via a Unix-socket protocol; registers Site implementations through a runtime Arc<dyn Site> registry.
Documentation
use clap::Parser;
use night_fury_daemon_core::cli::make_req;
use night_fury_daemon_core::client;
use serde_json::json;
use tail_fin_daemon::cli::{expand_home, Cli, Commands, DaemonCmd, SessionCmd};
use tail_fin_daemon::driver::SiteEntry;
use tail_fin_daemon::handlers::{
    bloomberg::BloombergHandler, grok::GrokHandler, sa::SaHandler, twitter::TwitterHandler,
};
use tail_fin_daemon::spawn::ensure_daemon_safe;

fn register_all_sites() {
    // Register all known sites so the launcher can look them up by id.
    // Adding a new site to the daemon = add one line here.
    tail_fin_daemon::driver::register_sites(vec![
        SiteEntry::new(tail_fin_sa::SeekingAlphaSite, |s| {
            Box::new(SaHandler::new(s))
        }),
        SiteEntry::new(tail_fin_twitter::TwitterSite, |s| {
            Box::new(TwitterHandler::new(s))
        }),
        SiteEntry::new(tail_fin_grok::GrokSite, |s| Box::new(GrokHandler::new(s))),
        SiteEntry::stateless(tail_fin_reddit::RedditSite),
        SiteEntry::stateless(tail_fin_instagram::InstagramSite),
        SiteEntry::stateless(tail_fin_xhs::XhsSite),
        SiteEntry::stateless(tail_fin_coupang::CoupangSite),
        // Shopee — register all 8 regions independently. Each region
        // has its own login session + cookie file; the daemon's
        // session map keys by `Site::id()` so isolation is automatic.
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Tw,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Sg,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::My,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Id,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Vn,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Ph,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Th,
        )),
        SiteEntry::stateless(tail_fin_shopee::ShopeeSite(
            tail_fin_shopee::ShopeeRegion::Br,
        )),
        SiteEntry::stateless(tail_fin_spotify::SpotifySite),
        SiteEntry::stateless(tail_fin_youtube::YoutubeSite),
        SiteEntry::new(tail_fin_bloomberg::BloombergSite, |s| {
            Box::new(BloombergHandler::new(s))
        }),
        SiteEntry::stateless(tail_fin_591::FiveNineOneSite),
        SiteEntry::stateless(tail_fin_pcc::PccSite),
        SiteEntry::stateless(tail_fin_tradingview::TradingViewSite),
    ]);
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_env("TFD_LOG")
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .with_writer(std::io::stderr)
        .init();

    let cli = Cli::parse();
    let socket = expand_home(&cli.socket);

    // Ensure the parent directory for the socket exists.
    if let Some(parent) = std::path::Path::new(&socket).parent() {
        let _ = std::fs::create_dir_all(parent);
    }

    match cli.command {
        Commands::Daemon { cmd } => match cmd {
            DaemonCmd::Start {
                idle_timeout,
                daemon_idle,
                max_sessions,
                detach,
            } => {
                if detach {
                    tail_fin_daemon::spawn::start_detached_and_wait(
                        &socket,
                        idle_timeout,
                        daemon_idle,
                        max_sessions,
                    )
                    .await?;
                } else {
                    register_all_sites();
                    tail_fin_daemon::server::serve(
                        &socket,
                        idle_timeout,
                        daemon_idle,
                        max_sessions,
                    )
                    .await?;
                }
            }
            DaemonCmd::Stop => {
                let req = make_req("daemon.shutdown", None, json!({}));
                let resp = client::send(&socket, &req).await?;
                tail_fin_daemon::dispatch::print_and_check(&resp);
            }
            DaemonCmd::Status => {
                ensure_daemon_safe(&socket).await?;
                let req = make_req("daemon.status", None, json!({}));
                let resp = client::send(&socket, &req).await?;
                tail_fin_daemon::dispatch::print_and_check(&resp);
            }
        },

        Commands::Session { cmd } => match cmd {
            SessionCmd::Acquire { site, host } => {
                ensure_daemon_safe(&socket).await?;
                let req = make_req(
                    "session.acquire",
                    None,
                    json!({"site": site, "host": host, "mode": "acquired"}),
                );
                let resp = client::send(&socket, &req).await?;
                tail_fin_daemon::dispatch::print_and_check(&resp);
            }
            SessionCmd::Release { session_id } => {
                ensure_daemon_safe(&socket).await?;
                let req = make_req("session.release", Some(&session_id), json!({}));
                let resp = client::send(&socket, &req).await?;
                tail_fin_daemon::dispatch::print_and_check(&resp);
            }
            SessionCmd::List => {
                ensure_daemon_safe(&socket).await?;
                let req = make_req("session.list", None, json!({}));
                let resp = client::send(&socket, &req).await?;
                tail_fin_daemon::dispatch::print_and_check(&resp);
            }
            SessionCmd::Destroy { session_id } => {
                ensure_daemon_safe(&socket).await?;
                let req = make_req("session.destroy", Some(&session_id), json!({}));
                let resp = client::send(&socket, &req).await?;
                tail_fin_daemon::dispatch::print_and_check(&resp);
            }
        },

        Commands::Start {
            idle_timeout,
            daemon_idle,
            max_sessions,
            detach: _,
        } => {
            register_all_sites();
            tail_fin_daemon::server::serve(&socket, idle_timeout, daemon_idle, max_sessions)
                .await?;
        }

        Commands::Sa { cmd } => {
            tail_fin_daemon::dispatch::sa::run(cmd, &socket, &cli.host, cli.session.clone())
                .await?;
        }
        Commands::Twitter { cmd } => {
            tail_fin_daemon::dispatch::twitter::run(cmd, &socket, &cli.host, cli.session.clone())
                .await?;
        }
        Commands::Grok { cmd } => {
            tail_fin_daemon::dispatch::grok::run(cmd, &socket, &cli.host, cli.session.clone())
                .await?;
        }
        Commands::Bloomberg { cmd } => {
            tail_fin_daemon::dispatch::bloomberg::run(cmd, &socket, &cli.host, cli.session.clone())
                .await?;
        }
    }

    Ok(())
}