mirror of
https://github.com/bybrooklyn/alchemist.git
synced 2026-04-18 01:43:34 -04:00
Fix shutdown exit and restore watch folder API sync
This commit is contained in:
20
src/main.rs
20
src/main.rs
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user