0c66fc6010
Move three directories into their target topology: - doctate-common/ -> common/ - client-desktop/ -> clients/desktop/ - watch/wearos/ -> clients/wearos/ Update doctate-common path-dependency in 3 Cargo.toml files (server, clients/desktop, experiments) and the workspace members list in the root Cargo.toml. Crate names unchanged (doctate-server, doctate-common, doctate-desktop). Workspace still in single-Cargo.lock form; isolation into standalone workspaces follows in stage 3. All 508 tests pass, experiments standalone-workspace also resolves the new path.
113 lines
3.5 KiB
Rust
113 lines
3.5 KiB
Rust
use std::sync::Arc;
|
|
|
|
use eframe::egui;
|
|
use tracing::info;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
use std::fs::{File, OpenOptions, TryLockError};
|
|
|
|
use doctate_desktop::app::DoctateApp;
|
|
use doctate_desktop::paths::{cases_dir, lock_file_path, pending_dir, snapshot_cache_path};
|
|
|
|
/// Unwrap a startup `Result` or print `context: {err}` to stderr and
|
|
/// `exit(1)`. Used for unrecoverable bootstrap failures before the
|
|
/// egui event loop runs — there is no UI to show an error in.
|
|
fn or_die<T, E: std::fmt::Display>(result: Result<T, E>, context: &str) -> T {
|
|
match result {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("{context}: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> eframe::Result {
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| EnvFilter::new("doctate_desktop=info")),
|
|
)
|
|
.init();
|
|
|
|
info!("starting doctate-desktop");
|
|
|
|
let runtime = Arc::new(
|
|
tokio::runtime::Builder::new_multi_thread()
|
|
.worker_threads(2)
|
|
.enable_all()
|
|
.build()
|
|
.expect("build tokio runtime"),
|
|
);
|
|
|
|
let pending = or_die(pending_dir(), "cannot determine pending directory");
|
|
|
|
// Single-instance guard. The lock is held by the `_lock_file` binding
|
|
// for the entire `main` lifetime — OS releases it automatically on
|
|
// process exit or crash, so a stale lock file after a crash is
|
|
// harmless (the next launch re-acquires).
|
|
let _lock_file = acquire_single_instance_lock();
|
|
let cases = or_die(cases_dir(), "cannot determine cases directory");
|
|
let cache_path = or_die(
|
|
snapshot_cache_path(),
|
|
"cannot determine snapshot cache path",
|
|
);
|
|
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default()
|
|
.with_inner_size([400.0, 560.0])
|
|
.with_min_inner_size([320.0, 400.0])
|
|
.with_resizable(true),
|
|
..Default::default()
|
|
};
|
|
|
|
eframe::run_native(
|
|
"doctate",
|
|
options,
|
|
Box::new(move |_cc| {
|
|
let app = DoctateApp::new(runtime, pending, cases, cache_path);
|
|
Ok(Box::new(app) as Box<dyn eframe::App>)
|
|
}),
|
|
)
|
|
}
|
|
|
|
/// Open and exclusively lock a per-user `client.lock`. Returns the
|
|
/// handle; dropping it (process exit) releases the lock. If another
|
|
/// instance already holds it, prints a friendly message and exits.
|
|
fn acquire_single_instance_lock() -> File {
|
|
let path = or_die(lock_file_path(), "cannot determine lock file path");
|
|
if let Some(parent) = path.parent()
|
|
&& let Err(e) = std::fs::create_dir_all(parent)
|
|
{
|
|
eprintln!("cannot create data directory {}: {e}", parent.display());
|
|
std::process::exit(1);
|
|
}
|
|
let file = match OpenOptions::new()
|
|
.create(true)
|
|
.read(true)
|
|
.write(true)
|
|
.truncate(false)
|
|
.open(&path)
|
|
{
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
eprintln!("cannot open lock file {}: {e}", path.display());
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
match file.try_lock() {
|
|
Ok(()) => file,
|
|
Err(TryLockError::WouldBlock) => {
|
|
eprintln!(
|
|
"doctate läuft bereits (Lock hält auf {}). Nur eine Instanz pro User zulässig.",
|
|
path.display()
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
Err(TryLockError::Error(e)) => {
|
|
eprintln!("lock acquire failed on {}: {e}", path.display());
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|