use std::sync::Arc;
use anyhow::{Context as _, Result};
use vs_daemon::{config::Paths as DaemonPaths, server, Daemon};
pub struct ServeArgs {
pub paths: DaemonPaths,
}
#[cfg(target_os = "macos")]
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};
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 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")?;
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");
}
}
res = &mut server => {
match res {
Ok(Err(e)) => tracing::error!(
error = %e,
"server task failed before ctrl-c"
),
Err(e) => tracing::error!(
error = %e,
"server task panicked"
),
Ok(Ok(())) => {}
}
}
}
});
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")]
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")?;
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 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")?;
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");
}
}
res = &mut server => {
match res {
Ok(Err(e)) => tracing::error!(
error = %e,
"server task failed before ctrl-c"
),
Err(e) => tracing::error!(
error = %e,
"server task panicked"
),
Ok(Ok(())) => {}
}
}
}
});
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")]
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")?;
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 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")?;
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");
}
}
res = &mut server => {
match res {
Ok(Err(e)) => tracing::error!(
error = %e,
"server task failed before ctrl-c"
),
Err(e) => tracing::error!(
error = %e,
"server task panicked"
),
Ok(Ok(())) => {}
}
}
}
});
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() {}