use std::sync::Arc;
use anyhow::{Context as _, Result};
use vs_daemon::{config::Paths as DaemonPaths, server, Daemon};
pub struct ServeArgs {
pub paths: DaemonPaths,
pub stop: bool,
}
pub fn run_stop(paths: &DaemonPaths) -> Result<()> {
let pid_file = paths.pid_file();
let pid_str = match std::fs::read_to_string(&pid_file) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("no daemon running (no PID file at {})", pid_file.display());
return Ok(());
}
Err(e) => return Err(e).context("read pid file"),
};
let pid: i32 = pid_str
.trim()
.parse()
.with_context(|| format!("malformed PID file: {pid_str:?}"))?;
#[cfg(unix)]
{
let r = unsafe { libc::kill(pid, libc::SIGTERM) };
if r != 0 {
let e = std::io::Error::last_os_error();
if e.raw_os_error() == Some(libc::ESRCH) {
let _ = std::fs::remove_file(&pid_file);
eprintln!("no daemon running (cleaned stale PID file for pid {pid})");
return Ok(());
}
return Err(anyhow::anyhow!("kill({pid}, SIGTERM) failed: {e}"));
}
}
#[cfg(windows)]
{
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
unsafe {
let h = OpenProcess(PROCESS_TERMINATE, false, u32::try_from(pid).unwrap_or(0))
.map_err(|e| anyhow::anyhow!("OpenProcess({pid}): {e}"))?;
let r = TerminateProcess(h, 0);
let _ = CloseHandle(h);
r.map_err(|e| anyhow::anyhow!("TerminateProcess({pid}): {e}"))?;
}
}
let socket = paths.socket();
for _ in 0..50 {
if !socket.exists() {
eprintln!("daemon stopped (pid {pid})");
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(anyhow::anyhow!(
"daemon (pid {pid}) did not exit within 5s; socket still present at {}",
socket.display()
))
}
#[cfg(unix)]
pub async fn wait_terminate() {
if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
s.recv().await;
} else {
std::future::pending::<()>().await;
}
}
#[cfg(not(unix))]
pub async fn wait_terminate() {
std::future::pending::<()>().await;
}
#[cfg(target_os = "macos")]
#[allow(clippy::too_many_lines)]
pub fn run(args: &ServeArgs) -> Result<()> {
use objc2::MainThreadMarker;
use objc2_app_kit::NSApplication;
use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSRunLoop};
use vs_engine_webkit::{backend::webkit::WkBackend, Engine, EngineRuntime};
if args.stop {
return run_stop(&args.paths);
}
init_tracing();
install_panic_hook();
install_seh_handler();
args.paths.ensure_root().context("ensure ~/.vibesurfer")?;
let mtm = MainThreadMarker::new()
.ok_or_else(|| anyhow::anyhow!("vs serve must be invoked from the OS main thread"))?;
let _app = NSApplication::sharedApplication(mtm);
let store = vs_store::Store::open(args.paths.db()).context("open state.db")?;
let captures_dir = args.paths.captures();
let skills_dir = args.paths.root.join("skills");
let backend = WkBackend::new(mtm).with_capture_dir(captures_dir.clone());
let engine_box: Box<dyn Engine> = Box::new(backend);
let (engine_runtime, mut dispatcher) = EngineRuntime::dispatcher(engine_box);
let engine_runtime = Arc::new(engine_runtime);
let mut daemon = Daemon::new(store, engine_runtime.clone())
.with_captures_dir(captures_dir)
.with_skills_dir(skills_dir);
if let Ok(k) = vs_store::MasterKey::resolve(args.paths.key_file()) {
daemon = daemon.with_master_key(k);
} else {
tracing::warn!(
"no master key (keyring entry missing and {} not present); vs_auth save|load will fail",
args.paths.key_file().display()
);
}
let socket = args.paths.socket();
let pid_path = args.paths.pid_file();
let server_thread = std::thread::Builder::new()
.name("vs-daemon-tokio".into())
.spawn(move || -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.context("build tokio runtime")?;
if let Err(e) = std::fs::write(&pid_path, std::process::id().to_string()) {
tracing::warn!(?pid_path, error = %e, "write pid file");
}
rt.block_on(async move {
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let mut server =
tokio::spawn(async move { server::serve(daemon, socket, shutdown_rx).await });
tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("ctrl-c received, shutting down");
let _ = shutdown_tx.send(());
if let Ok(Err(e)) = server.await {
tracing::error!(error = %e, "server task ended with error");
}
}
() = wait_terminate() => {
tracing::info!("SIGTERM received, shutting down");
let _ = shutdown_tx.send(());
if let Ok(Err(e)) = server.await {
tracing::error!(error = %e, "server task ended with error");
}
}
res = &mut server => {
match res {
Ok(Err(e)) => tracing::error!(
error = %e,
"server task failed before shutdown signal"
),
Err(e) => tracing::error!(
error = %e,
"server task panicked"
),
Ok(Ok(())) => {}
}
}
}
});
let _ = std::fs::remove_file(&pid_path);
drop(rt);
Ok(())
})
.context("spawn vs-daemon-tokio thread")?;
let runloop = NSRunLoop::currentRunLoop();
'main: loop {
loop {
match dispatcher.tick() {
Ok(true) => {}
Ok(false) => break,
Err(()) => break 'main,
}
}
let slice = NSDate::dateWithTimeIntervalSinceNow(0.05);
unsafe { runloop.runMode_beforeDate(NSDefaultRunLoopMode, &slice) };
}
let _ = server_thread.join();
drop(engine_runtime); Ok(())
}
#[cfg(target_os = "linux")]
#[allow(clippy::too_many_lines)]
pub fn run(args: &ServeArgs) -> Result<()> {
use vs_engine_webkit::{backend::wpe::WpeBackend, Engine, EngineRuntime};
init_tracing();
install_panic_hook();
install_seh_handler();
args.paths.ensure_root().context("ensure ~/.vibesurfer")?;
if args.stop {
return run_stop(&args.paths);
}
gtk4::init().context("gtk4 init")?;
let store = vs_store::Store::open(args.paths.db()).context("open state.db")?;
let captures_dir = args.paths.captures();
let skills_dir = args.paths.root.join("skills");
let backend = WpeBackend::new().with_capture_dir(captures_dir.clone());
let engine_box: Box<dyn Engine> = Box::new(backend);
let (engine_runtime, mut dispatcher) = EngineRuntime::dispatcher(engine_box);
let engine_runtime = Arc::new(engine_runtime);
let mut daemon = Daemon::new(store, engine_runtime.clone())
.with_captures_dir(captures_dir)
.with_skills_dir(skills_dir);
if let Ok(k) = vs_store::MasterKey::resolve(args.paths.key_file()) {
daemon = daemon.with_master_key(k);
} else {
tracing::warn!(
"no master key (keyring entry missing and {} not present); vs_auth save|load will fail",
args.paths.key_file().display()
);
}
let socket = args.paths.socket();
let pid_path = args.paths.pid_file();
let server_thread = std::thread::Builder::new()
.name("vs-daemon-tokio".into())
.spawn(move || -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.context("build tokio runtime")?;
if let Err(e) = std::fs::write(&pid_path, std::process::id().to_string()) {
tracing::warn!(?pid_path, error = %e, "write pid file");
}
rt.block_on(async move {
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let mut server =
tokio::spawn(async move { server::serve(daemon, socket, shutdown_rx).await });
tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("ctrl-c received, shutting down");
let _ = shutdown_tx.send(());
if let Ok(Err(e)) = server.await {
tracing::error!(error = %e, "server task ended with error");
}
}
() = wait_terminate() => {
tracing::info!("SIGTERM received, shutting down");
let _ = shutdown_tx.send(());
if let Ok(Err(e)) = server.await {
tracing::error!(error = %e, "server task ended with error");
}
}
res = &mut server => {
match res {
Ok(Err(e)) => tracing::error!(
error = %e,
"server task failed before shutdown signal"
),
Err(e) => tracing::error!(
error = %e,
"server task panicked"
),
Ok(Ok(())) => {}
}
}
}
});
let _ = std::fs::remove_file(&pid_path);
drop(rt);
Ok(())
})
.context("spawn vs-daemon-tokio thread")?;
let main_ctx = glib::MainContext::default();
'main: loop {
loop {
match dispatcher.tick() {
Ok(true) => {}
Ok(false) => break,
Err(()) => break 'main,
}
}
if !main_ctx.iteration(false) {
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
let _ = server_thread.join();
drop(engine_runtime);
Ok(())
}
#[cfg(target_os = "windows")]
#[allow(clippy::too_many_lines)]
pub fn run(args: &ServeArgs) -> Result<()> {
use vs_engine_webkit::{backend::webview2::Webview2Backend, Engine, EngineRuntime};
use windows::Win32::System::Com::{CoInitializeEx, COINIT_APARTMENTTHREADED};
use windows::Win32::UI::WindowsAndMessaging::{
DispatchMessageW, PeekMessageW, TranslateMessage, MSG, PM_REMOVE,
};
init_tracing();
install_panic_hook();
install_seh_handler();
args.paths.ensure_root().context("ensure ~/.vibesurfer")?;
if args.stop {
return run_stop(&args.paths);
}
unsafe {
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
}
let store = vs_store::Store::open(args.paths.db()).context("open state.db")?;
let captures_dir = args.paths.captures();
let skills_dir = args.paths.root.join("skills");
let backend = Webview2Backend::new().with_capture_dir(captures_dir.clone());
let engine_box: Box<dyn Engine> = Box::new(backend);
let (engine_runtime, mut dispatcher) = EngineRuntime::dispatcher(engine_box);
let engine_runtime = Arc::new(engine_runtime);
let mut daemon = Daemon::new(store, engine_runtime.clone())
.with_captures_dir(captures_dir)
.with_skills_dir(skills_dir);
if let Ok(k) = vs_store::MasterKey::resolve(args.paths.key_file()) {
daemon = daemon.with_master_key(k);
} else {
tracing::warn!(
"no master key (keyring entry missing and {} not present); vs_auth save|load will fail",
args.paths.key_file().display()
);
}
let socket = args.paths.socket();
let pid_path = args.paths.pid_file();
let server_thread = std::thread::Builder::new()
.name("vs-daemon-tokio".into())
.spawn(move || -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.context("build tokio runtime")?;
if let Err(e) = std::fs::write(&pid_path, std::process::id().to_string()) {
tracing::warn!(?pid_path, error = %e, "write pid file");
}
rt.block_on(async move {
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let mut server =
tokio::spawn(async move { server::serve(daemon, socket, shutdown_rx).await });
tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("ctrl-c received, shutting down");
let _ = shutdown_tx.send(());
if let Ok(Err(e)) = server.await {
tracing::error!(error = %e, "server task ended with error");
}
}
() = wait_terminate() => {
tracing::info!("SIGTERM received, shutting down");
let _ = shutdown_tx.send(());
if let Ok(Err(e)) = server.await {
tracing::error!(error = %e, "server task ended with error");
}
}
res = &mut server => {
match res {
Ok(Err(e)) => tracing::error!(
error = %e,
"server task failed before shutdown signal"
),
Err(e) => tracing::error!(
error = %e,
"server task panicked"
),
Ok(Ok(())) => {}
}
}
}
});
let _ = std::fs::remove_file(&pid_path);
drop(rt);
Ok(())
})
.context("spawn vs-daemon-tokio thread")?;
let mut shutdown = false;
while !shutdown {
loop {
match dispatcher.tick() {
Ok(true) => {}
Ok(false) => break,
Err(()) => {
shutdown = true;
break;
}
}
}
let mut msg = MSG::default();
unsafe {
while PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = TranslateMessage(&raw const msg);
DispatchMessageW(&raw const msg);
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let _ = server_thread.join();
drop(engine_runtime);
Ok(())
}
fn init_tracing() {
if tracing::dispatcher::has_been_set() {
return;
}
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("vs_daemon=info,info")),
)
.with_writer(std::io::stderr)
.try_init();
}
fn install_panic_hook() {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let location = info
.location()
.map_or_else(|| "<unknown>".to_string(), ToString::to_string);
let msg = info
.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| info.payload().downcast_ref::<String>().map(String::as_str))
.unwrap_or("<no message>");
tracing::error!(at = %location, "PANIC: {msg}");
prev(info);
}));
}
#[cfg(target_os = "windows")]
fn install_seh_handler() {
use std::io::Write;
use windows::Win32::System::Diagnostics::Debug::{
AddVectoredExceptionHandler, EXCEPTION_POINTERS,
};
unsafe extern "system" fn handler(info: *mut EXCEPTION_POINTERS) -> i32 {
if info.is_null() {
return 0; }
let info = unsafe { &*info };
if info.ExceptionRecord.is_null() {
return 0;
}
let rec = unsafe { &*info.ExceptionRecord };
let code = u32::from_ne_bytes(rec.ExceptionCode.0.to_ne_bytes());
if code & 0xF000_0000 != 0xC000_0000 {
return 0;
}
let mut err = std::io::stderr().lock();
let _ = writeln!(
err,
"VIBESURFER_SEH code=0x{:08x} ip={:p} flags=0x{:x} params={}",
code, rec.ExceptionAddress, rec.ExceptionFlags, rec.NumberParameters,
);
if code == 0xC000_0005 && rec.NumberParameters >= 2 {
let kind = rec.ExceptionInformation[0];
let va = rec.ExceptionInformation[1];
let kind_str = match kind {
0 => "read",
1 => "write",
8 => "execute",
_ => "?",
};
let _ = writeln!(
err,
"VIBESURFER_SEH access={kind_str} (kind={kind}) faulting_va=0x{va:x}",
);
}
if !info.ContextRecord.is_null() {
let ctx = unsafe { &*info.ContextRecord };
#[cfg(target_arch = "x86_64")]
{
let _ = writeln!(
err,
"VIBESURFER_SEH rip=0x{:x} rsp=0x{:x} rbp=0x{:x} rcx=0x{:x} rdx=0x{:x}",
ctx.Rip, ctx.Rsp, ctx.Rbp, ctx.Rcx, ctx.Rdx,
);
}
#[cfg(target_arch = "aarch64")]
{
let _ = writeln!(
err,
"VIBESURFER_SEH pc=0x{:x} sp=0x{:x} fp=0x{:x}",
ctx.Pc, ctx.Sp, ctx.Fp,
);
}
}
let _ = err.flush();
0
}
unsafe {
AddVectoredExceptionHandler(1 , Some(handler));
}
}
#[cfg(not(target_os = "windows"))]
fn install_seh_handler() {}