Refactor: remove desktop/Tauri bundle, add i18n, CI/CD pipeline
Nightly Build / build (push) Failing after 6s
Nightly Build / build (push) Failing after 6s
- Remove desktop (Tauri) bundle: docs/desktop.md, icons/, tauri.conf.json, src/desktop/mod.rs, gen/schemas/ - Remove build.rs (no longer needed) - Add i18n system (crates/core-api, plugin-mobile-connector, web) - Refactor config system (src/config.rs, boot_format.rs) - Add mobile connector features (app, router, device pairing) - Plugin system improvements (skald-core) - Update dependencies (Cargo.lock, Cargo.toml) - CI/CD: Gitea Actions workflows (nightly + release), package.sh, verify-version.sh, builds.skaldagent.net config
This commit is contained in:
+2
-2
@@ -1,7 +1,7 @@
|
||||
//! How this binary renders the bootstrap lines that `skald_core::boot` emits.
|
||||
//!
|
||||
//! Rendering is the shell's business, not the core's: a desktop bundle or a
|
||||
//! setup wizard would format the same `boot` target differently, or not at all.
|
||||
//! Rendering is the shell's business, not the core's: another shell (e.g. the
|
||||
//! setup wizard) formats the same `boot` target differently, or not at all.
|
||||
//! Wired in `main.rs` as a stdout layer filtered on `boot::TARGET`, independent
|
||||
//! of `RUST_LOG`, so this output always appears whatever the log filter is.
|
||||
|
||||
|
||||
+1
-147
@@ -12,15 +12,6 @@ pub use skald_core::config::{
|
||||
const DEFAULT_CONFIG: &str = "default.config.yaml";
|
||||
const CONFIG: &str = "config.yml";
|
||||
|
||||
/// Default config baked into the binary at compile time.
|
||||
///
|
||||
/// Used by [`bootstrap_data_dir`] (desktop mode) to seed `config.yml` on first
|
||||
/// launch, where the bundled binary cannot rely on `default.config.yaml` being
|
||||
/// next to it on disk (the cwd has already been relocated to the per-user data
|
||||
/// dir). Headless mode still copies `default.config.yaml` from the source tree
|
||||
/// as before.
|
||||
const DEFAULT_CONFIG_EMBEDDED: &str = include_str!("../default.config.yaml");
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
@@ -104,144 +95,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute directory for log files.
|
||||
///
|
||||
/// `init_logging()` runs **before** [`bootstrap_data_dir`] relocates the cwd, so
|
||||
/// a relative `"logs"` path would resolve against the bundle's launch cwd (`/`
|
||||
/// for a Finder-launched `.app`) — un-writable, so the log folder stays empty.
|
||||
/// In a packaged bundle, return an absolute path under the per-user data dir
|
||||
/// instead. In every other mode (headless, desktop dev) keep the historical
|
||||
/// relative `"logs"`, unchanged.
|
||||
/// Directory for log files: a relative `"logs"` under the launch cwd.
|
||||
pub fn resolved_log_dir() -> std::path::PathBuf {
|
||||
#[cfg(feature = "desktop")]
|
||||
{
|
||||
if running_from_bundle() {
|
||||
if let Some(dir) = dirs::data_dir() {
|
||||
return dir.join("Skald").join("logs");
|
||||
}
|
||||
}
|
||||
}
|
||||
std::path::PathBuf::from("logs")
|
||||
}
|
||||
|
||||
/// In desktop mode (Tauri bundle), relocate the process working directory to
|
||||
/// the OS-appropriate per-user data dir so that every relative path in
|
||||
/// `config.yml` (db, logs, data, secrets, models, agents, …) resolves there
|
||||
/// instead of `/` (the default cwd of a `.app` bundle on macOS, or the Windows
|
||||
/// equivalent). Also seeds `config.yml` from the bundled default if missing.
|
||||
///
|
||||
/// | OS | Location |
|
||||
/// |---------|-----------------------------------------------------|
|
||||
/// | macOS | `~/Library/Application Support/Skald` |
|
||||
/// | Windows | `%APPDATA%\Skald` (= `C:\Users\<u>\AppData\Roaming`)|
|
||||
/// | Linux | `~/.local/share/Skald` |
|
||||
///
|
||||
/// ## When relocation happens
|
||||
/// Only when the process is running from a packaged bundle (e.g. inside
|
||||
/// `Skald.app/Contents/MacOS/`). In dev mode (`cargo run --features desktop`),
|
||||
/// the cwd is left untouched so all source-tree assets (`agents/`, `skills/`,
|
||||
/// `web/`, `config.yml`, …) keep resolving from the crate root as in headless
|
||||
/// mode.
|
||||
///
|
||||
/// In headless mode this is always a no-op: the cwd stays as the user launched
|
||||
/// it, preserving today's behaviour (`./database.db`, `./logs/`, …).
|
||||
#[cfg(feature = "desktop")]
|
||||
pub fn bootstrap_data_dir() -> Result<()> {
|
||||
use tracing::info;
|
||||
if !running_from_bundle() {
|
||||
info!("desktop mode (dev): cwd unchanged — using source-tree assets");
|
||||
return Ok(());
|
||||
}
|
||||
let data_dir = dirs::data_dir()
|
||||
.context("could not determine OS data directory")?
|
||||
.join("Skald");
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.with_context(|| format!("failed to create data dir at {}", data_dir.display()))?;
|
||||
info!(path = %data_dir.display(), "desktop mode: relocating cwd to per-user data dir");
|
||||
std::env::set_current_dir(&data_dir)
|
||||
.with_context(|| format!("failed to cd to {}", data_dir.display()))?;
|
||||
|
||||
// Seed config.yml from the bundled default if absent (first launch).
|
||||
let config_path = Path::new(CONFIG);
|
||||
if !config_path.exists() {
|
||||
let _ = std::fs::write(CONFIG, DEFAULT_CONFIG_EMBEDDED);
|
||||
info!(path = %data_dir.display(), "seeded config.yml from embedded default");
|
||||
}
|
||||
|
||||
// Make the read-only bundled assets reachable from the relocated cwd.
|
||||
link_bundled_assets(&data_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read-only asset directories shipped inside the `.app` bundle's `Resources/`
|
||||
/// dir (see `tauri.conf.json > bundle > resources`). The backend looks these up
|
||||
/// by relative path from the cwd (agent discovery reads `agents/`, Axum serves
|
||||
/// `web/`, etc.), but the cwd has just been relocated to the data dir — where
|
||||
/// they don't exist. Without this, `Skald::new` fails with
|
||||
/// "Failed to read agents directory 'agents'" and the app exits on launch.
|
||||
#[cfg(feature = "desktop")]
|
||||
const BUNDLED_ASSETS: &[&str] = &["agents", "web", "skills", "commands"];
|
||||
|
||||
/// (Re)link each bundled asset dir into the per-user data dir as a symlink to
|
||||
/// the copy inside the app bundle's `Resources/`, so the existing relative-path
|
||||
/// lookups resolve while mutable state (db, config, logs, secrets) stays in the
|
||||
/// data dir itself.
|
||||
///
|
||||
/// Symlinking (rather than copying) keeps the assets in sync with the installed
|
||||
/// app version automatically. A pre-existing **real** directory is treated as a
|
||||
/// user override and left untouched; only symlinks are refreshed.
|
||||
#[cfg(feature = "desktop")]
|
||||
fn link_bundled_assets(data_dir: &Path) -> Result<()> {
|
||||
use tracing::{info, warn};
|
||||
let exe = std::env::current_exe().context("could not resolve current_exe")?;
|
||||
// `.../Skald.app/Contents/MacOS/skald` → `.../Skald.app/Contents/Resources`
|
||||
let resource_dir = match exe.parent().and_then(|p| p.parent()) {
|
||||
Some(contents) => contents.join("Resources"),
|
||||
None => {
|
||||
warn!("could not derive bundle Resources dir from exe path — skipping asset link");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
for name in BUNDLED_ASSETS {
|
||||
let src = resource_dir.join(name);
|
||||
if !src.exists() {
|
||||
warn!(asset = name, "bundled asset missing from Resources — skipping");
|
||||
continue;
|
||||
}
|
||||
let dst = data_dir.join(name);
|
||||
match std::fs::symlink_metadata(&dst) {
|
||||
// Stale symlink from a previous launch — replace it.
|
||||
Ok(meta) if meta.file_type().is_symlink() => { let _ = std::fs::remove_file(&dst); }
|
||||
// A real dir/file the user created — respect it, don't clobber.
|
||||
Ok(_) => continue,
|
||||
// Absent — fall through and create.
|
||||
Err(_) => {}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(&src, &dst)
|
||||
.with_context(|| format!("failed to symlink {} -> {}", dst.display(), src.display()))?;
|
||||
info!(asset = name, target = %src.display(), "linked bundled asset into data dir");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Heuristic: are we running inside a packaged bundle (e.g. `Foo.app`)?
|
||||
/// Used to decide whether to relocate the cwd to the per-user data dir.
|
||||
#[cfg(feature = "desktop")]
|
||||
fn running_from_bundle() -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
return exe.to_string_lossy().contains(".app/");
|
||||
}
|
||||
}
|
||||
// Windows / Linux: TBD when packaging targets land. For now treat all
|
||||
// launches as dev mode (no cwd relocation).
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
pub fn bootstrap_data_dir() -> Result<()> {
|
||||
// Headless mode: keep cwd as launched, no relocation.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
//! Desktop (Tauri) entry point — compiled only under `--features desktop`.
|
||||
//!
|
||||
//! Wraps the headless Skald backend in a Tauri event loop. The backend runs on
|
||||
//! Tauri's shared tokio runtime (no dual runtime). A system-tray icon provides
|
||||
//! `Open` (show+focus the main window) and `Quit` (graceful shutdown).
|
||||
//!
|
||||
//! ## Window policy
|
||||
//! The main window starts hidden. The traffic-light red / window X button
|
||||
//! *hides* it instead of closing — the app keeps running in the tray. Only the
|
||||
//! tray's `Quit` menu item (or Cmd+Q / system termination) actually shuts the
|
||||
//! backend down and exits.
|
||||
//!
|
||||
//! ## Restart safety
|
||||
//! The `tauri::RunEvent::ExitRequested` handler is re-entrant-guarded by an
|
||||
//! `AtomicBool`: the first trigger prevents the exit, runs the async backend
|
||||
//! shutdown, then calls `app.exit(0)` (which would otherwise loop).
|
||||
//!
|
||||
//! See `docs/desktop.md` for the architecture overview and build instructions.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use tauri::{
|
||||
menu::{MenuBuilder, MenuItemBuilder},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent,
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{config::Config, run_backend, shutdown_backend, Backend};
|
||||
|
||||
/// Slot for the backend handle, kept in Tauri's managed state.
|
||||
///
|
||||
/// `None` until the async `run_backend()` completes; `Some(Backend)` afterwards.
|
||||
/// The exit handler takes ownership when the user quits, so we need an
|
||||
/// `Option` rather than a plain `Backend`.
|
||||
type BackendSlot = Mutex<Option<Backend>>;
|
||||
|
||||
/// Process-wide handle to the Tauri app, populated once in the setup hook.
|
||||
///
|
||||
/// Used by code paths that don't naturally receive an `AppHandle` (notably the
|
||||
/// `restart` tool, which is constructed deep inside the tool registry but needs
|
||||
/// to trigger `AppHandle::restart()` in desktop mode).
|
||||
static APP_HANDLE: OnceLock<tauri::AppHandle> = OnceLock::new();
|
||||
|
||||
/// Take a clone of the Tauri `AppHandle`, if the desktop runtime is up.
|
||||
/// Always `None` in headless mode (or before the setup hook has run).
|
||||
pub fn app_handle() -> Option<tauri::AppHandle> {
|
||||
APP_HANDLE.get().cloned()
|
||||
}
|
||||
|
||||
/// Desktop entry point. Builds the Tauri app, spawns the backend on its
|
||||
/// shared tokio runtime, wires the system-tray menu, and runs the event loop.
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
info!(version = env!("CARGO_PKG_VERSION"), "starting skald (desktop mode)");
|
||||
|
||||
// Re-entrancy guard: the first `ExitRequested` triggers async shutdown and
|
||||
// then calls `app.exit(0)`, which would itself re-emit `ExitRequested`. The
|
||||
// flag short-circuits the second trigger so we actually leave the process.
|
||||
let exiting = Arc::new(AtomicBool::new(false));
|
||||
|
||||
tauri::Builder::default()
|
||||
// Pre-register the backend slot so it exists before the setup hook
|
||||
// (the setup hook spawns the backend async; state must already be there).
|
||||
.manage::<BackendSlot>(Mutex::new(None))
|
||||
.setup(|app| {
|
||||
// Stash the app handle for code paths without a natural handle
|
||||
// reference (notably the `restart` tool, reached through the handler
|
||||
// installed just below).
|
||||
let _ = APP_HANDLE.set(app.handle().clone());
|
||||
|
||||
// Teach the core how to restart *this* shell. A bundled app has no
|
||||
// supervisor reading its exit code, and the binary is read-only, so
|
||||
// "restart" means: tear down Tauri, spawn a fresh copy, exit. This
|
||||
// mirrors what `tauri-plugin-process`'s JS `restart` does internally.
|
||||
skald_core::tools::restart::set_restart_handler(Box::new(|| {
|
||||
let handle = app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Tauri app handle is not set yet"))?;
|
||||
let exe = std::env::current_exe()
|
||||
.map_err(|e| anyhow::anyhow!("failed to resolve current_exe: {e}"))?;
|
||||
// Tauri-side teardown (webview, tray, event loop, windows).
|
||||
handle.cleanup_before_exit();
|
||||
// Spawn a fresh copy of the current binary (detached).
|
||||
let _ = std::process::Command::new(exe).spawn();
|
||||
std::process::exit(0);
|
||||
}));
|
||||
|
||||
build_tray(app)?;
|
||||
|
||||
// Resolve the backend port from config so the webview URL is always
|
||||
// in sync with where Axum will actually bind. We load the config
|
||||
// sync here just to read the port; the backend task re-loads it
|
||||
// (cheap — single YAML parse).
|
||||
// In desktop mode this also performs the cwd relocation (no-op in
|
||||
// dev, real relocation inside an `.app` bundle).
|
||||
let port = match std::panic::catch_unwind(|| {
|
||||
crate::config::bootstrap_data_dir()
|
||||
.and_then(|_| Config::load())
|
||||
.map(|c| c.server.port)
|
||||
}) {
|
||||
Ok(Ok(port)) => port,
|
||||
Ok(Err(e)) => {
|
||||
error!(error = %e, "failed to load config for window URL");
|
||||
app.handle().exit(1);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
error!("config load panicked");
|
||||
app.handle().exit(1);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let url = format!("http://127.0.0.1:{port}");
|
||||
info!(%url, "creating main window");
|
||||
let parsed_url = tauri::Url::parse(&url)
|
||||
.map_err(tauri::Error::InvalidUrl)?;
|
||||
WebviewWindowBuilder::new(app, "main", WebviewUrl::External(parsed_url))
|
||||
.title("Skald")
|
||||
.inner_size(1200.0, 800.0)
|
||||
.min_inner_size(800.0, 600.0)
|
||||
.visible(false)
|
||||
.build()?;
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
// Spawn the backend on Tauri's shared tokio runtime.
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match run_backend().await {
|
||||
Ok(backend) => {
|
||||
info!("backend ready — desktop mode");
|
||||
let slot = app_handle.state::<BackendSlot>();
|
||||
*slot.lock().unwrap() = Some(backend);
|
||||
// Reveal the main window now that the backend is serving.
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "backend startup failed");
|
||||
app_handle.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
// Close button (traffic-light red / X) → hide instead of close.
|
||||
// The window stays alive in the tray; only "Quit" terminates.
|
||||
.on_window_event(|window, event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
})
|
||||
.build(tauri::generate_context!())?
|
||||
.run({
|
||||
let exiting = exiting.clone();
|
||||
move |app_handle, event| {
|
||||
if let RunEvent::ExitRequested { api, .. } = event {
|
||||
// Second trigger (from our own app.exit(0)) — let it proceed.
|
||||
if exiting.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
api.prevent_exit();
|
||||
let app_handle = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// Scope the MutexGuard so it is dropped before any `.await`:
|
||||
// std::sync::MutexGuard is !Send, so holding it across an
|
||||
// await point would make the whole future !Send (Tauri's
|
||||
// runtime requires Send futures).
|
||||
let backend = {
|
||||
let slot = app_handle.state::<BackendSlot>();
|
||||
slot.lock().unwrap().take()
|
||||
};
|
||||
if let Some(backend) = backend {
|
||||
info!("graceful shutdown — desktop mode");
|
||||
shutdown_backend(backend).await;
|
||||
info!("shutdown complete — desktop mode");
|
||||
} else {
|
||||
warn!("exit requested before backend was ready");
|
||||
}
|
||||
// Actually leave the process now.
|
||||
app_handle.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build the system-tray icon, its menu (Open / Quit), and the event handlers.
|
||||
fn build_tray(app: &tauri::App) -> tauri::Result<()> {
|
||||
let open = MenuItemBuilder::with_id("open", "Open").build(app)?;
|
||||
let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?;
|
||||
let menu = MenuBuilder::new(app).items(&[&open, &quit]).build()?;
|
||||
|
||||
// Tray icon: reuse the app's bundled window icon for now. On macOS the
|
||||
// system auto-recolors template images for the menubar theme; we set
|
||||
// `icon_as_template(true)` accordingly. A dedicated monochrome tray PNG
|
||||
// (loaded via the right Tauri image API for this version) can replace this
|
||||
// later — see the icon sources under `icons/`.
|
||||
let icon = app.default_window_icon().cloned()
|
||||
.ok_or_else(|| tauri::Error::AssetNotFound("default window icon".into()))?;
|
||||
|
||||
TrayIconBuilder::with_id("main")
|
||||
.tooltip("Skald")
|
||||
.icon(icon)
|
||||
.icon_as_template(true)
|
||||
.menu(&menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"open" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
// Trigger the graceful path via ExitRequested. The handler in
|
||||
// `run()` will drain the backend and then call `exit(0)`.
|
||||
app.exit(0);
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
// Single left-click toggles the main window (show+focus or hide).
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -48,7 +48,7 @@ use super::ApiError;
|
||||
static FEED_URL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
|
||||
/// Installs the feed URL from config. Called once during frontend construction;
|
||||
/// later calls are ignored, so tests and the desktop shell cannot race it.
|
||||
/// later calls are ignored, so concurrent callers (e.g. tests) cannot race it.
|
||||
pub fn set_feed_url(url: String) {
|
||||
let _ = FEED_URL.set(url.trim_end_matches('/').to_string());
|
||||
}
|
||||
|
||||
+14
-33
@@ -1,6 +1,4 @@
|
||||
mod boot_format;
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
mod frontend;
|
||||
mod config;
|
||||
|
||||
@@ -11,7 +9,7 @@ use skald_core::boot;
|
||||
use std::io::IsTerminal;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use sqlx::SqlitePool;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing::{debug, error, info, warn};
|
||||
@@ -31,9 +29,8 @@ const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
|
||||
/// Backend handle — everything that must live until shutdown.
|
||||
///
|
||||
/// Constructed by [`run_backend`], consumed by [`shutdown_backend`]. In
|
||||
/// headless mode it lives in `async_main()`; in desktop mode it's stashed in
|
||||
/// Tauri's managed state (`app.manage(backend)`) and consumed on Quit.
|
||||
/// Constructed by [`run_backend`], consumed by [`shutdown_backend`]; it lives in
|
||||
/// `async_main()` for the lifetime of the process.
|
||||
pub struct Backend {
|
||||
pub skald: Arc<Skald>,
|
||||
pub web: WebServerHandle,
|
||||
@@ -44,31 +41,22 @@ fn main() -> Result<()> {
|
||||
// Install the rustls crypto provider (ring) before any TLS handshake.
|
||||
// Required because reqwest is built with `rustls-no-provider` (see
|
||||
// Cargo.toml): exactly one process-wide provider must be installed before
|
||||
// the first Client is built. In headless mode this happened to work
|
||||
// because the first HTTPS request was lazy; in desktop mode the backend
|
||||
// task fires requests earlier, so install it explicitly up front.
|
||||
// the first Client is built.
|
||||
rustls::crypto::ring::default_provider().install_default()
|
||||
.expect("failed to install rustls ring crypto provider");
|
||||
|
||||
init_logging();
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
{
|
||||
desktop::run()
|
||||
}
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
{
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
rt.block_on(async_main())
|
||||
}
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
rt.block_on(async_main())
|
||||
}
|
||||
|
||||
/// Initialise tracing (file + boot stdout layers) and the panic hook.
|
||||
///
|
||||
/// Called once at process start, before either the tokio runtime (headless) or
|
||||
/// the Tauri event loop (desktop). Not dependent on any async runtime.
|
||||
/// Called once at process start, before the tokio runtime is built. Not
|
||||
/// dependent on any async runtime.
|
||||
fn init_logging() {
|
||||
let log_dir = config::resolved_log_dir();
|
||||
std::fs::create_dir_all(&log_dir).ok();
|
||||
@@ -103,8 +91,8 @@ fn init_logging() {
|
||||
.init();
|
||||
|
||||
// Route panics through tracing so they land in logs/ (the default hook only
|
||||
// writes to stderr, invisible under supervisors / Tauri). Chain to the
|
||||
// default hook so the human-readable message + backtrace still print.
|
||||
// writes to stderr, invisible under a supervisor). Chain to the default hook
|
||||
// so the human-readable message + backtrace still print.
|
||||
let default_panic = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let location = info.location().map(|l| l.to_string()).unwrap_or_else(|| "unknown".into());
|
||||
@@ -116,8 +104,8 @@ fn init_logging() {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Headless entry point (no Tauri): run the backend, wait for a shutdown
|
||||
/// signal, then shut everything down. Used only in `cfg(not(feature = "desktop"))`.
|
||||
/// Entry point: run the backend, wait for a shutdown signal, then shut
|
||||
/// everything down.
|
||||
async fn async_main() -> Result<()> {
|
||||
info!(version = env!("CARGO_PKG_VERSION"), "starting {APP_NAME}");
|
||||
boot::title(format!("{APP_NAME} v{} — starting", env!("CARGO_PKG_VERSION")));
|
||||
@@ -135,14 +123,7 @@ async fn async_main() -> Result<()> {
|
||||
/// Boot the Skald backend: load config, build plugins, open the DB pool,
|
||||
/// construct `Skald`, and start the web frontend. Returns a [`Backend`] whose
|
||||
/// components must be shut down via [`shutdown_backend`] for graceful exit.
|
||||
///
|
||||
/// Shared by both the headless entry point and the desktop (Tauri) setup hook.
|
||||
pub async fn run_backend() -> Result<Backend> {
|
||||
// In desktop mode, relocate the process cwd to the OS-appropriate per-user
|
||||
// data dir before reading any relative path (db, logs, data, …). Headless
|
||||
// mode keeps the cwd unchanged.
|
||||
config::bootstrap_data_dir()?;
|
||||
|
||||
let cfg = match Config::load() {
|
||||
Ok(c) => { debug!("config loaded"); c }
|
||||
Err(e) => { error!(error = %e, "failed to load config"); return Err(e); }
|
||||
|
||||
Reference in New Issue
Block a user