Fix shutdown exit and restore watch folder API sync

This commit is contained in:
2026-04-02 23:27:47 -04:00
parent 003dac15df
commit e57c2b8a1f
3 changed files with 120 additions and 52 deletions

View File

@@ -704,7 +704,7 @@ async fn run() -> Result<()> {
"Boot sequence completed in {} ms",
boot_start.elapsed().as_millis()
);
alchemist::server::run_server(alchemist::server::RunServerArgs {
let server_result = alchemist::server::run_server(alchemist::server::RunServerArgs {
db,
config,
agent,
@@ -721,7 +721,23 @@ async fn run() -> Result<()> {
file_watcher,
library_scanner,
})
.await?;
.await;
// Background tasks (run_loop, scheduler, watcher,
// maintenance) have no shutdown signal and run
// forever. After run_server returns, graceful
// shutdown is complete — all jobs are drained
// and FFmpeg processes are cancelled. Exit cleanly.
match server_result {
Ok(()) => {
info!("Server shutdown complete. Exiting.");
std::process::exit(0);
}
Err(e) => {
error!("Server exited with error: {e}");
std::process::exit(1);
}
}
} else {
// CLI Mode
if setup_mode {

View File

@@ -10,6 +10,7 @@ use crate::system::hardware::{HardwareProbeLog, HardwareState};
use axum::{
Router,
body::{Body, to_bytes},
extract::ConnectInfo,
http::{Method, Request, header},
};
use chrono::Utc;
@@ -17,6 +18,7 @@ use futures::StreamExt;
use http_body_util::BodyExt;
use serde_json::json;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -155,6 +157,19 @@ fn auth_json_request(
.unwrap()
}
fn localhost_request(method: Method, uri: &str, body: Body) -> Request<Body> {
let mut request = Request::builder()
.method(method)
.uri(uri)
.body(body)
.unwrap();
request.extensions_mut().insert(ConnectInfo(SocketAddr::from((
[127, 0, 0, 1],
3000,
))));
request
}
async fn body_text(response: axum::response::Response) -> String {
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
@@ -704,16 +719,11 @@ async fn fs_endpoints_are_available_during_setup()
let browse_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::GET)
.uri(format!(
"/api/fs/browse?path={}",
browse_root.to_string_lossy()
))
.body(Body::empty())
.unwrap(),
)
.oneshot(localhost_request(
Method::GET,
&format!("/api/fs/browse?path={}", browse_root.to_string_lossy()),
Body::empty(),
))
.await?;
assert_eq!(browse_response.status(), StatusCode::OK);
let browse_body = body_text(browse_response).await;
@@ -721,19 +731,23 @@ async fn fs_endpoints_are_available_during_setup()
let preview_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/fs/preview")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
.oneshot({
let mut request = localhost_request(
Method::POST,
"/api/fs/preview",
Body::from(
json!({
"directories": [browse_root.to_string_lossy().to_string()]
})
.to_string(),
))
.unwrap(),
)
),
);
request.headers_mut().insert(
header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
request
})
.await?;
assert_eq!(preview_response.status(), StatusCode::OK);
let preview_body = body_text(preview_response).await;

View File

@@ -40,6 +40,7 @@ interface SettingsBundleResponse {
settings: {
scanner: {
directories: string[];
extra_watch_dirs?: Array<{ path: string; is_recursive: boolean }>;
};
[key: string]: unknown;
};
@@ -82,10 +83,12 @@ export default function WatchFolders() {
[profiles]
);
const fetchBundle = async () => apiJson<SettingsBundleResponse>("/api/settings/bundle");
const fetchDirs = async () => {
// Fetch both canonical library dirs and extra watch dirs, merge them for the UI
const [bundle, watchDirs] = await Promise.all([
apiJson<SettingsBundleResponse>("/api/settings/bundle"),
fetchBundle(),
apiJson<WatchDir[]>("/api/settings/watch-dirs")
]);
@@ -110,6 +113,7 @@ export default function WatchFolders() {
const existing = merged.find(m => m.path === wd.path);
if (existing) {
existing.id = wd.id;
existing.is_recursive = wd.is_recursive;
existing.profile_id = wd.profile_id;
}
}
@@ -118,6 +122,23 @@ export default function WatchFolders() {
setDirs(merged);
};
const saveLibraryDirs = async (
bundle: SettingsBundleResponse,
nextDirectories: string[]
) => {
await apiAction("/api/settings/bundle", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...bundle.settings,
scanner: {
...bundle.settings.scanner,
directories: nextDirectories,
},
}),
});
};
const fetchProfiles = async () => {
const data = await apiJson<LibraryProfile[]>("/api/profiles");
setProfiles(data);
@@ -169,26 +190,26 @@ export default function WatchFolders() {
}
try {
const bundle = await apiJson<SettingsBundleResponse>("/api/settings/bundle");
const currentDirs = bundle.settings.scanner.directories;
if (currentDirs.includes(normalized)) {
// Even if it's in config, sync it to ensure it's in DB for profiles
await apiAction("/api/settings/folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dirs: currentDirs.map(d => ({ path: d, is_recursive: true }))
}),
});
} else {
await apiAction("/api/settings/folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dirs: [...currentDirs, normalized].map(d => ({ path: d, is_recursive: true }))
}),
});
const createdDir = await apiJson<WatchDir>("/api/settings/watch-dirs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: normalized,
is_recursive: true,
}),
});
try {
const bundle = await fetchBundle();
const currentDirs = bundle.settings.scanner.directories;
if (!currentDirs.includes(createdDir.path)) {
await saveLibraryDirs(bundle, [...currentDirs, createdDir.path]);
}
} catch (e) {
await apiAction(`/api/settings/watch-dirs/${createdDir.id}`, {
method: "DELETE",
}).catch(() => undefined);
throw e;
}
setDirInput("");
@@ -207,16 +228,33 @@ export default function WatchFolders() {
if (!dir) return;
try {
const bundle = await apiJson<SettingsBundleResponse>("/api/settings/bundle");
const filteredDirs = bundle.settings.scanner.directories.filter(candidate => candidate !== dirPath);
await apiAction("/api/settings/folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dirs: filteredDirs.map(d => ({ path: d, is_recursive: true }))
}),
});
if (dir.id > 0) {
await apiAction(`/api/settings/watch-dirs/${dir.id}`, {
method: "DELETE",
});
}
try {
const bundle = await fetchBundle();
const filteredDirs = bundle.settings.scanner.directories.filter(
(candidate) => candidate !== dirPath
);
if (filteredDirs.length !== bundle.settings.scanner.directories.length) {
await saveLibraryDirs(bundle, filteredDirs);
}
} catch (e) {
if (dir.id > 0) {
await apiAction("/api/settings/watch-dirs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: dir.path,
is_recursive: dir.is_recursive,
}),
}).catch(() => undefined);
}
throw e;
}
setError(null);
await fetchDirs();