Fix: Cache-Boot (KCEF Corruption)

This commit is contained in:
Youwes09
2026-05-17 03:29:39 -05:00
parent 897ecfd316
commit d98547d540
4 changed files with 117 additions and 58 deletions
+38 -4
View File
@@ -75,6 +75,24 @@ fn remove_dir_best_effort(path: &std::path::Path) {
} }
} }
fn wait_until_deletable(path: &std::path::Path, timeout_secs: u64) -> bool {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
while std::time::Instant::now() < deadline {
let locked = if path.is_file() {
std::fs::OpenOptions::new().write(true).open(path).is_err()
} else if path.is_dir() {
std::fs::read_dir(path).is_err()
} else {
return true;
};
if !locked {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
#[tauri::command] #[tauri::command]
pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> { pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> {
let window = app.get_webview_window("main").ok_or("no main window")?; let window = app.get_webview_window("main").ok_or("no main window")?;
@@ -92,10 +110,17 @@ pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> {
pub fn clear_suwayomi_cache() -> Result<(), String> { pub fn clear_suwayomi_cache() -> Result<(), String> {
use crate::server::resolve::suwayomi_data_dir; use crate::server::resolve::suwayomi_data_dir;
let data_dir = suwayomi_data_dir(); let data_dir = suwayomi_data_dir();
for dir in &["cache", "bin/kcef", "cache/kcef"] { for dir in &["cache/kcef", "logs"] {
let p = data_dir.join(dir); let p = data_dir.join(dir);
if p.exists() { if p.exists() {
std::fs::remove_dir_all(&p).map_err(|e| e.to_string())?; remove_dir_best_effort(&p);
}
}
for dir in &["downloads/thumbnails"] {
let p = data_dir.join(dir);
if p.exists() {
remove_dir_best_effort(&p);
let _ = std::fs::create_dir_all(&p);
} }
} }
Ok(()) Ok(())
@@ -106,10 +131,19 @@ pub fn reset_suwayomi_data(app: tauri::AppHandle) -> Result<(), String> {
use crate::server::resolve::suwayomi_data_dir; use crate::server::resolve::suwayomi_data_dir;
crate::server::kill_tachidesk(&app); crate::server::kill_tachidesk(&app);
std::thread::sleep(std::time::Duration::from_millis(500));
let data_dir = suwayomi_data_dir(); let data_dir = suwayomi_data_dir();
for entry_name in &["database.mv.db", "extensions", "settings", "logs", "local"] { let targets = ["database.mv.db", "extensions", "settings", "logs", "local"];
// Wait up to 10s for the JVM to release file locks
for entry_name in &targets {
let p = data_dir.join(entry_name);
if p.exists() {
wait_until_deletable(&p, 10);
}
}
for entry_name in &targets {
let p = data_dir.join(entry_name); let p = data_dir.join(entry_name);
if p.is_dir() { if p.is_dir() {
std::fs::remove_dir_all(&p).map_err(|e| format!("{entry_name}: {e}"))?; std::fs::remove_dir_all(&p).map_err(|e| format!("{entry_name}: {e}"))?;
+10 -8
View File
@@ -91,6 +91,13 @@
return () => clearTimeout(timer); return () => clearTimeout(timer);
}); });
$effect(() => {
if (!appReady) return;
downloadStore.poll();
const dlInterval = setInterval(() => downloadStore.poll(), 2000);
return () => clearInterval(dlInterval);
});
$effect(() => { $effect(() => {
if (store.settings.discordRpc) { if (store.settings.discordRpc) {
initRpc(); initRpc();
@@ -125,9 +132,11 @@
applyZoom(); applyZoom();
}); });
const unlistenClose = await win.listen("tauri://close-requested", handleCloseRequested); const unlistenClose = await win.listen("tauri://close-requested", handleCloseRequested);
await initStore();
startProbe();
if (store.settings.autoStartServer) { if (store.settings.autoStartServer) {
invoke<void>("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => { invoke<void>("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => {
if (err?.kind === "NotConfigured") boot.notConfigured = true; if (err?.kind === "NotConfigured") boot.notConfigured = true;
@@ -135,20 +144,13 @@
}); });
} }
await initStore();
startProbe();
const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>( const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>(
"download-progress", "download-progress",
e => setActiveDownloads(e.payload), e => setActiveDownloads(e.payload),
); );
await downloadStore.poll();
const dlInterval = setInterval(() => downloadStore.poll(), 2000);
return () => { return () => {
stopProbe(); stopProbe();
clearInterval(dlInterval);
unlistenResize(); unlistenResize();
unlistenScale(); unlistenScale();
unlistenDownload(); unlistenDownload();
@@ -559,15 +559,15 @@
.page-loader-single { .page-loader-single {
width: min(100%, var(--effective-width, 100%)); width: min(100%, var(--effective-width, 100%));
max-width: var(--effective-width, 100%); max-width: var(--effective-width, 100%);
max-height: calc(100vh - 80px); max-height: calc(var(--visual-vh, 100vh) - 80px);
aspect-ratio: 2 / 3; aspect-ratio: 2 / 3;
} }
.img { display: block; user-select: none; image-rendering: auto; } .img { display: block; user-select: none; image-rendering: auto; }
.img:global(.optimize-contrast) { image-rendering: -webkit-optimize-contrast; } .img:global(.optimize-contrast) { image-rendering: -webkit-optimize-contrast; }
:global(.fit-width) { max-width: var(--effective-width, 100%); width: 100%; height: auto; } :global(.fit-width) { max-width: var(--effective-width, 100%); width: 100%; height: auto; }
:global(.fit-height) { max-height: calc(100vh - 80px); width: auto; max-width: var(--effective-width, 100%); height: auto; } :global(.fit-height) { max-height: calc(var(--visual-vh, 100vh) - 80px); width: auto; max-width: var(--effective-width, 100%); height: auto; }
:global(.fit-screen) { max-width: var(--effective-width, 100%); max-height: calc(100vh - 80px); object-fit: contain; height: auto; } :global(.fit-screen) { max-width: var(--effective-width, 100%); max-height: calc(var(--visual-vh, 100vh) - 80px); object-fit: contain; height: auto; }
:global(.fit-original) { max-width: 100%; width: auto; height: auto; } :global(.fit-original) { max-width: 100%; width: auto; height: auto; }
:global(.strip-gap) { margin-bottom: 8px; } :global(.strip-gap) { margin-bottom: 8px; }
+63 -40
View File
@@ -4,7 +4,8 @@ import { trackingState } from "@features/tracking/store/tracki
import { loadAllStores } from "@core/persistence/persist"; import { loadAllStores } from "@core/persistence/persist";
import { notifyReauthSuccess } from "@api/client"; import { notifyReauthSuccess } from "@api/client";
const MAX_ATTEMPTS = 40; const MAX_ATTEMPTS = 15;
const BG_MAX_ATTEMPTS = 60;
export const boot = $state({ export const boot = $state({
serverProbeOk: false, serverProbeOk: false,
@@ -26,6 +27,44 @@ export async function initStore() {
store.hydrate(saved); store.hydrate(saved);
} }
function handleProbeSuccess(gen: number) {
if (gen !== probeGeneration) return;
boot.serverProbeOk = true;
boot.failed = false;
boot.skipped = false;
trackingState.bootSync().catch(() => {});
}
function handleAuthRequired(gen: number) {
if (gen !== probeGeneration) return;
boot.serverProbeOk = true;
boot.failed = false;
const mode = store.settings.serverAuthMode ?? "NONE";
if (mode === "BASIC_AUTH") {
const user = store.settings.serverAuthUser?.trim() ?? "";
const pass = store.settings.serverAuthPass?.trim() ?? "";
if (user && pass) {
loginBasic(user, pass)
.then(() => { if (gen === probeGeneration) trackingState.bootSync().catch(() => {}); })
.catch(() => {
if (gen !== probeGeneration) return;
boot.loginUser = store.settings.serverAuthUser ?? "";
boot.loginRequired = true;
});
return;
}
boot.loginUser = store.settings.serverAuthUser ?? "";
boot.loginRequired = true;
return;
}
if (mode === "UI_LOGIN") {
boot.loginUser = store.settings.serverAuthUser ?? "";
boot.loginRequired = true;
return;
}
trackingState.bootSync().catch(() => {});
}
export function startProbe() { export function startProbe() {
const gen = ++probeGeneration; const gen = ++probeGeneration;
boot.failed = false; boot.failed = false;
@@ -36,51 +75,36 @@ export function startProbe() {
async function probe() { async function probe() {
if (gen !== probeGeneration) return; if (gen !== probeGeneration) return;
tries++; tries++;
const result = await probeServer(); const result = await probeServer();
if (gen !== probeGeneration) return; if (gen !== probeGeneration) return;
if (result === "ok") { if (result === "ok") { handleProbeSuccess(gen); return; }
boot.serverProbeOk = true; if (result === "auth_required") { handleAuthRequired(gen); return; }
trackingState.bootSync().catch(() => {}); if (tries >= MAX_ATTEMPTS) { boot.failed = true; startBackgroundProbe(gen); return; }
return;
setTimeout(probe, Math.min(300 + tries * 150, 1500));
} }
if (result === "auth_required") { setTimeout(probe, 100);
boot.serverProbeOk = true; }
const mode = store.settings.serverAuthMode ?? "NONE";
if (mode === "BASIC_AUTH") { function startBackgroundProbe(gen: number) {
const user = store.settings.serverAuthUser?.trim() ?? ""; let bgTries = 0;
const pass = store.settings.serverAuthPass?.trim() ?? "";
if (user && pass) { async function bgProbe() {
try {
await loginBasic(user, pass);
if (gen !== probeGeneration) return; if (gen !== probeGeneration) return;
trackingState.bootSync().catch(() => {}); bgTries++;
return; const result = await probeServer();
} catch {} if (gen !== probeGeneration) return;
}
boot.loginUser = store.settings.serverAuthUser ?? ""; if (result === "ok") { handleProbeSuccess(gen); return; }
boot.loginRequired = true; if (result === "auth_required") { handleAuthRequired(gen); return; }
return; if (bgTries >= BG_MAX_ATTEMPTS) return;
setTimeout(bgProbe, 2000);
} }
if (mode === "UI_LOGIN") { setTimeout(bgProbe, 2000);
boot.loginUser = store.settings.serverAuthUser ?? "";
boot.loginRequired = true;
return;
}
trackingState.bootSync().catch(() => {});
return;
}
if (tries >= MAX_ATTEMPTS) { boot.failed = true; return; }
setTimeout(probe, Math.min(750 + tries * 250, 3000));
}
setTimeout(probe, 2000);
} }
export function stopProbe() { export function stopProbe() {
@@ -94,7 +118,6 @@ export async function submitLogin(onSuccess: () => void): Promise<void> {
} }
boot.loginBusy = true; boot.loginBusy = true;
boot.loginError = null; boot.loginError = null;
try { try {
const mode = store.settings.serverAuthMode ?? "NONE"; const mode = store.settings.serverAuthMode ?? "NONE";
if (mode === "UI_LOGIN") { if (mode === "UI_LOGIN") {
@@ -102,7 +125,6 @@ export async function submitLogin(onSuccess: () => void): Promise<void> {
} else { } else {
await loginBasic(boot.loginUser.trim(), boot.loginPass.trim()); await loginBasic(boot.loginUser.trim(), boot.loginPass.trim());
} }
boot.loginRequired = false; boot.loginRequired = false;
boot.sessionExpired = false; boot.sessionExpired = false;
boot.skipped = false; boot.skipped = false;
@@ -128,10 +150,11 @@ export function retryBoot() {
} }
export function bypassBoot(onReady: () => void) { export function bypassBoot(onReady: () => void) {
probeGeneration++; const gen = probeGeneration;
boot.serverProbeOk = true; boot.serverProbeOk = true;
boot.loginRequired = false; boot.loginRequired = false;
boot.sessionExpired = false; boot.sessionExpired = false;
boot.skipped = true; boot.skipped = true;
onReady(); onReady();
startBackgroundProbe(gen);
} }