feat: move background processes to its own daemon

This commit is contained in:
zhom
2026-01-11 21:01:09 +04:00
parent 6756f88955
commit eeea15c65d
39 changed files with 3466 additions and 948 deletions
+401
View File
@@ -0,0 +1,401 @@
// Donut Browser Daemon - Background process for tray icon and services
// This runs independently of the main Tauri GUI
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process;
use std::sync::atomic::{AtomicBool, Ordering};
use muda::MenuEvent;
use serde::{Deserialize, Serialize};
use single_instance::SingleInstance;
use tao::event::{Event, StartCause};
use tao::event_loop::{ControlFlow, EventLoopBuilder};
use tokio::runtime::Runtime;
use tray_icon::TrayIcon;
use donutbrowser_lib::daemon::{autostart, services, tray};
static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct DaemonState {
daemon_pid: Option<u32>,
api_port: Option<u16>,
mcp_running: bool,
version: String,
}
fn get_state_path() -> PathBuf {
autostart::get_data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("daemon-state.json")
}
fn ensure_data_dir() -> std::io::Result<()> {
if let Some(data_dir) = autostart::get_data_dir() {
fs::create_dir_all(&data_dir)?;
}
Ok(())
}
fn read_state() -> DaemonState {
let path = get_state_path();
if path.exists() {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(state) = serde_json::from_str(&content) {
return state;
}
}
}
DaemonState::default()
}
fn write_state(state: &DaemonState) -> std::io::Result<()> {
let path = get_state_path();
let content = serde_json::to_string_pretty(state)?;
fs::write(path, content)
}
fn detach_from_parent() {
#[cfg(unix)]
{
unsafe {
libc::setsid();
}
}
}
fn spawn_detached() {
#[cfg(unix)]
{
match unsafe { libc::fork() } {
-1 => {
eprintln!("Fork failed");
process::exit(1);
}
0 => {
detach_from_parent();
}
_ => {
process::exit(0);
}
}
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
use std::process::{Command, Stdio};
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
let current_exe = env::current_exe().expect("Failed to get current exe path");
let _ = Command::new(current_exe)
.arg("--daemon-internal")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
.spawn();
process::exit(0);
}
}
fn set_high_priority() {
#[cfg(unix)]
{
// Set high priority so the daemon is killed last under resource pressure
// Negative nice value = higher priority. Try -10, fall back to -5 if it fails.
unsafe {
if libc::setpriority(libc::PRIO_PROCESS, 0, -10) != 0 {
let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -5);
}
}
}
#[cfg(windows)]
{
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{
GetCurrentProcess, SetPriorityClass, ABOVE_NORMAL_PRIORITY_CLASS,
};
// Set high priority so the daemon is killed last under resource pressure
unsafe {
let handle = GetCurrentProcess();
let _ = SetPriorityClass(handle, ABOVE_NORMAL_PRIORITY_CLASS);
// GetCurrentProcess returns a pseudo-handle that doesn't need to be closed,
// but we do it anyway for consistency
let _ = CloseHandle(handle);
}
}
}
fn run_daemon() {
// Set high priority so the daemon is less likely to be killed under resource pressure
set_high_priority();
// Initialize logging
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.format_timestamp_millis()
.init();
if let Err(e) = ensure_data_dir() {
eprintln!("Failed to create data directory: {}", e);
process::exit(1);
}
let instance =
SingleInstance::new("donut-browser-daemon").expect("Failed to create single instance lock");
if !instance.is_single() {
eprintln!("Daemon is already running");
process::exit(1);
}
log::info!("[daemon] Starting with PID {}", process::id());
// Create tokio runtime for async operations
let rt = Runtime::new().expect("Failed to create tokio runtime");
// Start services in the background
let services_result = rt.block_on(async { services::DaemonServices::start().await });
let daemon_services = match services_result {
Ok(s) => s,
Err(e) => {
log::error!("Failed to start services: {}", e);
process::exit(1);
}
};
// Write initial state
let state = DaemonState {
daemon_pid: Some(process::id()),
api_port: daemon_services.api_port,
mcp_running: daemon_services.mcp_running,
version: env!("CARGO_PKG_VERSION").to_string(),
};
if let Err(e) = write_state(&state) {
log::error!("Failed to write state: {}", e);
}
// Prepare tray menu and icon (but don't create the tray icon yet)
let tray_menu = tray::TrayMenu::new();
tray_menu.update_api_status(daemon_services.api_port);
tray_menu.update_mcp_status(daemon_services.mcp_running);
let icon = tray::load_icon();
let menu_channel = MenuEvent::receiver();
// Create the event loop
let event_loop = EventLoopBuilder::new().build();
// Store tray icon in Option - created after event loop starts
let mut tray_icon: Option<TrayIcon> = None;
// Run the event loop
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Poll;
match event {
Event::NewEvents(StartCause::Init) => {
// Hide from dock on macOS (must be done after event loop starts)
#[cfg(target_os = "macos")]
{
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};
if let Some(mtm) = MainThreadMarker::new() {
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
}
}
// Create tray icon after event loop has started (required for macOS)
tray_icon = Some(tray::create_tray_icon(icon.clone(), &tray_menu.menu));
log::info!("[daemon] Tray icon created");
}
Event::MainEventsCleared => {
// Process menu events
while let Ok(event) = menu_channel.try_recv() {
if event.id == tray_menu.open_item.id() || event.id == tray_menu.preferences_item.id() {
tray::open_gui();
} else if event.id == tray_menu.quit_item.id() {
log::info!("[daemon] Quit requested");
SHOULD_QUIT.store(true, Ordering::SeqCst);
}
}
if SHOULD_QUIT.load(Ordering::SeqCst) {
// Cleanup
let mut state = read_state();
state.daemon_pid = None;
let _ = write_state(&state);
log::info!("[daemon] Exiting");
*control_flow = ControlFlow::Exit;
}
}
_ => {}
}
// Keep tray_icon alive
let _ = &tray_icon;
});
}
fn stop_daemon() {
let state = read_state();
if let Some(pid) = state.daemon_pid {
#[cfg(unix)]
{
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
}
eprintln!("Sent stop signal to daemon (PID {})", pid);
}
#[cfg(windows)]
{
use std::process::Command;
let _ = Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.output();
eprintln!("Sent stop signal to daemon (PID {})", pid);
}
} else {
eprintln!("Daemon is not running");
}
}
fn show_status() {
let state = read_state();
if let Some(pid) = state.daemon_pid {
#[cfg(unix)]
let is_running = unsafe { libc::kill(pid as i32, 0) == 0 };
#[cfg(windows)]
let is_running = {
use std::process::Command;
let output = Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid)])
.output();
output
.map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string()))
.unwrap_or(false)
};
#[cfg(not(any(unix, windows)))]
let is_running = false;
if is_running {
eprintln!("Daemon is running (PID {})", pid);
if let Some(port) = state.api_port {
eprintln!(" API: Running on port {}", port);
} else {
eprintln!(" API: Stopped");
}
eprintln!(
" MCP: {}",
if state.mcp_running {
"Running"
} else {
"Stopped"
}
);
} else {
eprintln!("Daemon is not running (stale PID in state file)");
}
} else {
eprintln!("Daemon is not running");
}
}
fn print_usage() {
eprintln!("Donut Browser Daemon");
eprintln!();
eprintln!("Usage: donut-daemon <command>");
eprintln!();
eprintln!("Commands:");
eprintln!(" start Start the daemon (detaches from terminal)");
eprintln!(" stop Stop the running daemon");
eprintln!(" status Show daemon status");
eprintln!(" run Run in foreground (for debugging)");
eprintln!(" autostart Manage autostart settings");
eprintln!(" enable Enable autostart on login");
eprintln!(" disable Disable autostart on login");
eprintln!(" status Show autostart status");
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_usage();
process::exit(1);
}
match args[1].as_str() {
"start" => {
eprintln!("Starting daemon...");
spawn_detached();
run_daemon();
}
"stop" => {
stop_daemon();
}
"status" => {
show_status();
}
"run" => {
run_daemon();
}
"--daemon-internal" => {
run_daemon();
}
"autostart" => {
if args.len() < 3 {
eprintln!("Usage: donut-daemon autostart <enable|disable|status>");
process::exit(1);
}
match args[2].as_str() {
"enable" => {
if let Err(e) = autostart::enable_autostart() {
eprintln!("Failed to enable autostart: {}", e);
process::exit(1);
}
eprintln!("Autostart enabled");
}
"disable" => {
if let Err(e) = autostart::disable_autostart() {
eprintln!("Failed to disable autostart: {}", e);
process::exit(1);
}
eprintln!("Autostart disabled");
}
"status" => {
if autostart::is_autostart_enabled() {
eprintln!("Autostart is enabled");
} else {
eprintln!("Autostart is disabled");
}
}
_ => {
eprintln!("Unknown autostart command: {}", args[2]);
process::exit(1);
}
}
}
_ => {
print_usage();
process::exit(1);
}
}
}