1use std::sync::Arc;
22
23use anyhow::{Context as _, Result};
24use vs_daemon::{config::Paths as DaemonPaths, server, Daemon};
25
26pub struct ServeArgs {
28 pub paths: DaemonPaths,
29 pub stop: bool,
32}
33
34pub fn run_stop(paths: &DaemonPaths) -> Result<()> {
39 let pid_file = paths.pid_file();
40 let pid_str = match std::fs::read_to_string(&pid_file) {
41 Ok(s) => s,
42 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
43 eprintln!("no daemon running (no PID file at {})", pid_file.display());
44 return Ok(());
45 }
46 Err(e) => return Err(e).context("read pid file"),
47 };
48 let pid: i32 = pid_str
49 .trim()
50 .parse()
51 .with_context(|| format!("malformed PID file: {pid_str:?}"))?;
52 #[cfg(unix)]
53 {
54 let r = unsafe { libc::kill(pid, libc::SIGTERM) };
55 if r != 0 {
56 let e = std::io::Error::last_os_error();
57 if e.raw_os_error() == Some(libc::ESRCH) {
58 let _ = std::fs::remove_file(&pid_file);
59 eprintln!("no daemon running (cleaned stale PID file for pid {pid})");
60 return Ok(());
61 }
62 return Err(anyhow::anyhow!("kill({pid}, SIGTERM) failed: {e}"));
63 }
64 }
65 #[cfg(windows)]
66 {
67 use windows::Win32::Foundation::CloseHandle;
68 use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
69 unsafe {
70 let h = OpenProcess(PROCESS_TERMINATE, false, u32::try_from(pid).unwrap_or(0))
71 .map_err(|e| anyhow::anyhow!("OpenProcess({pid}): {e}"))?;
72 let r = TerminateProcess(h, 0);
73 let _ = CloseHandle(h);
74 r.map_err(|e| anyhow::anyhow!("TerminateProcess({pid}): {e}"))?;
75 }
76 }
77 let socket = paths.socket();
78 for _ in 0..50 {
79 if !socket.exists() {
80 eprintln!("daemon stopped (pid {pid})");
81 return Ok(());
82 }
83 std::thread::sleep(std::time::Duration::from_millis(100));
84 }
85 Err(anyhow::anyhow!(
86 "daemon (pid {pid}) did not exit within 5s; socket still present at {}",
87 socket.display()
88 ))
89}
90
91#[cfg(unix)]
94pub async fn wait_terminate() {
95 if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
96 s.recv().await;
97 } else {
98 std::future::pending::<()>().await;
99 }
100}
101
102#[cfg(not(unix))]
103pub async fn wait_terminate() {
104 std::future::pending::<()>().await;
105}
106
107#[cfg(target_os = "macos")]
112#[allow(clippy::too_many_lines)]
113pub fn run(args: &ServeArgs) -> Result<()> {
114 use objc2::MainThreadMarker;
115 use objc2_app_kit::NSApplication;
116 use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSRunLoop};
117 use vs_engine_webkit::{backend::webkit::WkBackend, Engine, EngineRuntime};
118
119 if args.stop {
120 return run_stop(&args.paths);
121 }
122
123 init_tracing();
124 install_panic_hook();
125 install_seh_handler();
126 args.paths.ensure_root().context("ensure ~/.vibesurfer")?;
127
128 let mtm = MainThreadMarker::new()
129 .ok_or_else(|| anyhow::anyhow!("vs serve must be invoked from the OS main thread"))?;
130 let _app = NSApplication::sharedApplication(mtm);
133
134 let store = vs_store::Store::open(args.paths.db()).context("open state.db")?;
135 let captures_dir = args.paths.captures();
136 let skills_dir = args.paths.root.join("skills");
137
138 let backend = WkBackend::new(mtm).with_capture_dir(captures_dir.clone());
143 let engine_box: Box<dyn Engine> = Box::new(backend);
144 let (engine_runtime, mut dispatcher) = EngineRuntime::dispatcher(engine_box);
145 let engine_runtime = Arc::new(engine_runtime);
146
147 let mut daemon = Daemon::new(store, engine_runtime.clone())
148 .with_captures_dir(captures_dir)
149 .with_skills_dir(skills_dir);
150
151 if let Ok(k) = vs_store::MasterKey::resolve(args.paths.key_file()) {
152 daemon = daemon.with_master_key(k);
153 } else {
154 tracing::warn!(
155 "no master key (keyring entry missing and {} not present); vs_auth save|load will fail",
156 args.paths.key_file().display()
157 );
158 }
159
160 let socket = args.paths.socket();
161 let pid_path = args.paths.pid_file();
162
163 let server_thread = std::thread::Builder::new()
168 .name("vs-daemon-tokio".into())
169 .spawn(move || -> Result<()> {
170 let rt = tokio::runtime::Builder::new_multi_thread()
171 .worker_threads(4)
172 .enable_all()
173 .build()
174 .context("build tokio runtime")?;
175 if let Err(e) = std::fs::write(&pid_path, std::process::id().to_string()) {
176 tracing::warn!(?pid_path, error = %e, "write pid file");
177 }
178 rt.block_on(async move {
179 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
180 let mut server =
181 tokio::spawn(async move { server::serve(daemon, socket, shutdown_rx).await });
182 tokio::select! {
183 _ = tokio::signal::ctrl_c() => {
184 tracing::info!("ctrl-c received, shutting down");
185 let _ = shutdown_tx.send(());
186 if let Ok(Err(e)) = server.await {
187 tracing::error!(error = %e, "server task ended with error");
188 }
189 }
190 () = wait_terminate() => {
191 tracing::info!("SIGTERM received, shutting down");
192 let _ = shutdown_tx.send(());
193 if let Ok(Err(e)) = server.await {
194 tracing::error!(error = %e, "server task ended with error");
195 }
196 }
197 res = &mut server => {
198 match res {
202 Ok(Err(e)) => tracing::error!(
203 error = %e,
204 "server task failed before shutdown signal"
205 ),
206 Err(e) => tracing::error!(
207 error = %e,
208 "server task panicked"
209 ),
210 Ok(Ok(())) => {}
211 }
212 }
213 }
214 });
215 let _ = std::fs::remove_file(&pid_path);
216 drop(rt);
220 Ok(())
221 })
222 .context("spawn vs-daemon-tokio thread")?;
223
224 let runloop = NSRunLoop::currentRunLoop();
227 'main: loop {
228 loop {
230 match dispatcher.tick() {
231 Ok(true) => {}
232 Ok(false) => break,
233 Err(()) => break 'main,
234 }
235 }
236 let slice = NSDate::dateWithTimeIntervalSinceNow(0.05);
239 unsafe { runloop.runMode_beforeDate(NSDefaultRunLoopMode, &slice) };
240 }
241
242 let _ = server_thread.join();
243 drop(engine_runtime); Ok(())
245}
246
247#[cfg(target_os = "linux")]
252#[allow(clippy::too_many_lines)]
253pub fn run(args: &ServeArgs) -> Result<()> {
254 use vs_engine_webkit::{backend::wpe::WpeBackend, Engine, EngineRuntime};
255
256 init_tracing();
257 install_panic_hook();
258 install_seh_handler();
259 args.paths.ensure_root().context("ensure ~/.vibesurfer")?;
260 if args.stop {
261 return run_stop(&args.paths);
262 }
263
264 gtk4::init().context("gtk4 init")?;
266
267 let store = vs_store::Store::open(args.paths.db()).context("open state.db")?;
268 let captures_dir = args.paths.captures();
269 let skills_dir = args.paths.root.join("skills");
270
271 let backend = WpeBackend::new().with_capture_dir(captures_dir.clone());
272 let engine_box: Box<dyn Engine> = Box::new(backend);
273 let (engine_runtime, mut dispatcher) = EngineRuntime::dispatcher(engine_box);
274 let engine_runtime = Arc::new(engine_runtime);
275
276 let mut daemon = Daemon::new(store, engine_runtime.clone())
277 .with_captures_dir(captures_dir)
278 .with_skills_dir(skills_dir);
279
280 if let Ok(k) = vs_store::MasterKey::resolve(args.paths.key_file()) {
281 daemon = daemon.with_master_key(k);
282 } else {
283 tracing::warn!(
284 "no master key (keyring entry missing and {} not present); vs_auth save|load will fail",
285 args.paths.key_file().display()
286 );
287 }
288
289 let socket = args.paths.socket();
290 let pid_path = args.paths.pid_file();
291
292 let server_thread = std::thread::Builder::new()
293 .name("vs-daemon-tokio".into())
294 .spawn(move || -> Result<()> {
295 let rt = tokio::runtime::Builder::new_multi_thread()
296 .worker_threads(4)
297 .enable_all()
298 .build()
299 .context("build tokio runtime")?;
300 if let Err(e) = std::fs::write(&pid_path, std::process::id().to_string()) {
301 tracing::warn!(?pid_path, error = %e, "write pid file");
302 }
303 rt.block_on(async move {
304 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
305 let mut server =
306 tokio::spawn(async move { server::serve(daemon, socket, shutdown_rx).await });
307 tokio::select! {
308 _ = tokio::signal::ctrl_c() => {
309 tracing::info!("ctrl-c received, shutting down");
310 let _ = shutdown_tx.send(());
311 if let Ok(Err(e)) = server.await {
312 tracing::error!(error = %e, "server task ended with error");
313 }
314 }
315 () = wait_terminate() => {
316 tracing::info!("SIGTERM received, shutting down");
317 let _ = shutdown_tx.send(());
318 if let Ok(Err(e)) = server.await {
319 tracing::error!(error = %e, "server task ended with error");
320 }
321 }
322 res = &mut server => {
323 match res {
324 Ok(Err(e)) => tracing::error!(
325 error = %e,
326 "server task failed before shutdown signal"
327 ),
328 Err(e) => tracing::error!(
329 error = %e,
330 "server task panicked"
331 ),
332 Ok(Ok(())) => {}
333 }
334 }
335 }
336 });
337 let _ = std::fs::remove_file(&pid_path);
338 drop(rt);
339 Ok(())
340 })
341 .context("spawn vs-daemon-tokio thread")?;
342
343 let main_ctx = glib::MainContext::default();
346 'main: loop {
347 loop {
348 match dispatcher.tick() {
349 Ok(true) => {}
350 Ok(false) => break,
351 Err(()) => break 'main,
352 }
353 }
354 if !main_ctx.iteration(false) {
357 std::thread::sleep(std::time::Duration::from_millis(10));
358 }
359 }
360
361 let _ = server_thread.join();
362 drop(engine_runtime);
363 Ok(())
364}
365
366#[cfg(target_os = "windows")]
371#[allow(clippy::too_many_lines)]
372pub fn run(args: &ServeArgs) -> Result<()> {
373 use vs_engine_webkit::{backend::webview2::Webview2Backend, Engine, EngineRuntime};
374 use windows::Win32::System::Com::{CoInitializeEx, COINIT_APARTMENTTHREADED};
375 use windows::Win32::UI::WindowsAndMessaging::{
376 DispatchMessageW, PeekMessageW, TranslateMessage, MSG, PM_REMOVE,
377 };
378
379 init_tracing();
380 install_panic_hook();
381 install_seh_handler();
382 args.paths.ensure_root().context("ensure ~/.vibesurfer")?;
383 if args.stop {
384 return run_stop(&args.paths);
385 }
386
387 unsafe {
390 let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
391 }
392
393 let store = vs_store::Store::open(args.paths.db()).context("open state.db")?;
394 let captures_dir = args.paths.captures();
395 let skills_dir = args.paths.root.join("skills");
396
397 let backend = Webview2Backend::new().with_capture_dir(captures_dir.clone());
398 let engine_box: Box<dyn Engine> = Box::new(backend);
399 let (engine_runtime, mut dispatcher) = EngineRuntime::dispatcher(engine_box);
400 let engine_runtime = Arc::new(engine_runtime);
401
402 let mut daemon = Daemon::new(store, engine_runtime.clone())
403 .with_captures_dir(captures_dir)
404 .with_skills_dir(skills_dir);
405
406 if let Ok(k) = vs_store::MasterKey::resolve(args.paths.key_file()) {
407 daemon = daemon.with_master_key(k);
408 } else {
409 tracing::warn!(
410 "no master key (keyring entry missing and {} not present); vs_auth save|load will fail",
411 args.paths.key_file().display()
412 );
413 }
414
415 let socket = args.paths.socket();
416 let pid_path = args.paths.pid_file();
417
418 let server_thread = std::thread::Builder::new()
419 .name("vs-daemon-tokio".into())
420 .spawn(move || -> Result<()> {
421 let rt = tokio::runtime::Builder::new_multi_thread()
422 .worker_threads(4)
423 .enable_all()
424 .build()
425 .context("build tokio runtime")?;
426 if let Err(e) = std::fs::write(&pid_path, std::process::id().to_string()) {
427 tracing::warn!(?pid_path, error = %e, "write pid file");
428 }
429 rt.block_on(async move {
430 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
431 let mut server =
432 tokio::spawn(async move { server::serve(daemon, socket, shutdown_rx).await });
433 tokio::select! {
434 _ = tokio::signal::ctrl_c() => {
435 tracing::info!("ctrl-c received, shutting down");
436 let _ = shutdown_tx.send(());
437 if let Ok(Err(e)) = server.await {
438 tracing::error!(error = %e, "server task ended with error");
439 }
440 }
441 () = wait_terminate() => {
442 tracing::info!("SIGTERM received, shutting down");
443 let _ = shutdown_tx.send(());
444 if let Ok(Err(e)) = server.await {
445 tracing::error!(error = %e, "server task ended with error");
446 }
447 }
448 res = &mut server => {
449 match res {
450 Ok(Err(e)) => tracing::error!(
451 error = %e,
452 "server task failed before shutdown signal"
453 ),
454 Err(e) => tracing::error!(
455 error = %e,
456 "server task panicked"
457 ),
458 Ok(Ok(())) => {}
459 }
460 }
461 }
462 });
463 let _ = std::fs::remove_file(&pid_path);
464 drop(rt);
465 Ok(())
466 })
467 .context("spawn vs-daemon-tokio thread")?;
468
469 let mut shutdown = false;
472 while !shutdown {
473 loop {
474 match dispatcher.tick() {
475 Ok(true) => {}
476 Ok(false) => break,
477 Err(()) => {
478 shutdown = true;
479 break;
480 }
481 }
482 }
483 let mut msg = MSG::default();
486 unsafe {
487 while PeekMessageW(&raw mut msg, None, 0, 0, PM_REMOVE).as_bool() {
488 let _ = TranslateMessage(&raw const msg);
489 DispatchMessageW(&raw const msg);
490 }
491 }
492 std::thread::sleep(std::time::Duration::from_millis(10));
493 }
494
495 let _ = server_thread.join();
496 drop(engine_runtime);
497 Ok(())
498}
499
500fn init_tracing() {
501 if tracing::dispatcher::has_been_set() {
502 return;
503 }
504 let _ = tracing_subscriber::fmt()
505 .with_env_filter(
506 tracing_subscriber::EnvFilter::try_from_default_env()
507 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("vs_daemon=info,info")),
508 )
509 .with_writer(std::io::stderr)
510 .try_init();
511}
512
513fn install_panic_hook() {
520 let prev = std::panic::take_hook();
521 std::panic::set_hook(Box::new(move |info| {
522 let location = info
523 .location()
524 .map_or_else(|| "<unknown>".to_string(), ToString::to_string);
525 let msg = info
526 .payload()
527 .downcast_ref::<&str>()
528 .copied()
529 .or_else(|| info.payload().downcast_ref::<String>().map(String::as_str))
530 .unwrap_or("<no message>");
531 tracing::error!(at = %location, "PANIC: {msg}");
532 prev(info);
533 }));
534}
535
536#[cfg(target_os = "windows")]
554fn install_seh_handler() {
555 use std::io::Write;
556 use windows::Win32::System::Diagnostics::Debug::{
557 AddVectoredExceptionHandler, EXCEPTION_POINTERS,
558 };
559
560 unsafe extern "system" fn handler(info: *mut EXCEPTION_POINTERS) -> i32 {
561 if info.is_null() {
562 return 0; }
564 let info = unsafe { &*info };
565 if info.ExceptionRecord.is_null() {
566 return 0;
567 }
568 let rec = unsafe { &*info.ExceptionRecord };
569 let code = u32::from_ne_bytes(rec.ExceptionCode.0.to_ne_bytes());
577 if code & 0xF000_0000 != 0xC000_0000 {
578 return 0;
579 }
580 let mut err = std::io::stderr().lock();
581 let _ = writeln!(
582 err,
583 "VIBESURFER_SEH code=0x{:08x} ip={:p} flags=0x{:x} params={}",
584 code, rec.ExceptionAddress, rec.ExceptionFlags, rec.NumberParameters,
585 );
586 if code == 0xC000_0005 && rec.NumberParameters >= 2 {
591 let kind = rec.ExceptionInformation[0];
592 let va = rec.ExceptionInformation[1];
593 let kind_str = match kind {
594 0 => "read",
595 1 => "write",
596 8 => "execute",
597 _ => "?",
598 };
599 let _ = writeln!(
600 err,
601 "VIBESURFER_SEH access={kind_str} (kind={kind}) faulting_va=0x{va:x}",
602 );
603 }
604 if !info.ContextRecord.is_null() {
609 let ctx = unsafe { &*info.ContextRecord };
610 #[cfg(target_arch = "x86_64")]
611 {
612 let _ = writeln!(
613 err,
614 "VIBESURFER_SEH rip=0x{:x} rsp=0x{:x} rbp=0x{:x} rcx=0x{:x} rdx=0x{:x}",
615 ctx.Rip, ctx.Rsp, ctx.Rbp, ctx.Rcx, ctx.Rdx,
616 );
617 }
618 #[cfg(target_arch = "aarch64")]
619 {
620 let _ = writeln!(
621 err,
622 "VIBESURFER_SEH pc=0x{:x} sp=0x{:x} fp=0x{:x}",
623 ctx.Pc, ctx.Sp, ctx.Fp,
624 );
625 }
626 }
627 let _ = err.flush();
628 0
629 }
630
631 unsafe {
632 AddVectoredExceptionHandler(1 , Some(handler));
633 }
634}
635
636#[cfg(not(target_os = "windows"))]
640fn install_seh_handler() {}