feat: production hardening (panic isolation, file logging, apps cache)
- Kernel::search wraps each plugin in catch_unwind; panics are logged and return [] - init_logging() adds daily rolling file at ~/.local/share/k-launcher/logs/ - AppsPlugin caches entries to ~/.cache/k-launcher/apps.bin via bincode; stale-while-revalidate on subsequent launches - 57 tests pass
This commit is contained in:
@@ -8,3 +8,4 @@ async-trait = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -97,6 +97,17 @@ pub trait SearchEngine: Send + Sync {
|
||||
async fn search(&self, query: &str) -> Vec<SearchResult>;
|
||||
}
|
||||
|
||||
// --- NullSearchEngine ---
|
||||
|
||||
pub struct NullSearchEngine;
|
||||
|
||||
#[async_trait]
|
||||
impl SearchEngine for NullSearchEngine {
|
||||
async fn search(&self, _query: &str) -> Vec<SearchResult> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
// --- Kernel (Application use case) ---
|
||||
|
||||
pub struct Kernel {
|
||||
@@ -113,9 +124,25 @@ impl Kernel {
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||
let futures = self.plugins.iter().map(|p| p.search(query));
|
||||
let nested: Vec<Vec<SearchResult>> = join_all(futures).await;
|
||||
let mut flat: Vec<SearchResult> = nested.into_iter().flatten().collect();
|
||||
use futures::FutureExt;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
|
||||
let futures = self
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|p| AssertUnwindSafe(p.search(query)).catch_unwind());
|
||||
let outcomes = join_all(futures).await;
|
||||
let mut flat: Vec<SearchResult> = outcomes
|
||||
.into_iter()
|
||||
.zip(self.plugins.iter())
|
||||
.flat_map(|(outcome, plugin)| match outcome {
|
||||
Ok(results) => results,
|
||||
Err(_) => {
|
||||
tracing::error!(plugin = plugin.name(), "plugin panicked during search");
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
flat.sort_by(|a, b| b.score.cmp(&a.score));
|
||||
flat.truncate(self.max_results);
|
||||
flat
|
||||
@@ -203,6 +230,29 @@ mod tests {
|
||||
assert_eq!(results[2].score.value(), 5);
|
||||
}
|
||||
|
||||
struct PanicPlugin;
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for PanicPlugin {
|
||||
fn name(&self) -> &str {
|
||||
"panic-plugin"
|
||||
}
|
||||
|
||||
async fn search(&self, _query: &str) -> Vec<SearchResult> {
|
||||
panic!("test panic");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn kernel_continues_after_plugin_panic() {
|
||||
let panic_plugin = Arc::new(PanicPlugin);
|
||||
let normal_plugin = Arc::new(MockPlugin::returns(vec![("survivor", 5)]));
|
||||
let k = Kernel::new(vec![panic_plugin, normal_plugin], 8);
|
||||
let results = k.search("q").await;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].title.as_str(), "survivor");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn kernel_truncates_at_max_results() {
|
||||
let plugin = Arc::new(MockPlugin::returns(vec![
|
||||
|
||||
Reference in New Issue
Block a user