瀏覽代碼

Safety: track alive books to avoid worker using freed objects

- Add CoverWorkerRegisterBook/Unregister + alive set; check BookIsAlive in worker\n- Register books on creation and after loading; unregister on deletion/clear/destroy\n- Prevents crashes if a book is freed while queued
Codex CLI 4 月之前
父節點
當前提交
3969456d7a
共有 3 個文件被更改,包括 57 次插入3 次删除
  1. 4 0
      src/bookcollection.pas
  2. 4 1
      src/main.pas
  3. 49 2
      src/unitCoverWorker.pas

+ 4 - 0
src/bookcollection.pas

@@ -51,6 +51,8 @@ begin
     // Explicitly free the cover control to avoid orphaned images
     if Assigned(book) and Assigned(book.Cover) then
       book.Cover.Free;
+    CoverWorkerRemoveBook(book);
+    CoverWorkerUnregisterBook(book);
     book.Free;               // free the book itself
   end;
   mList.Clear;
@@ -128,6 +130,8 @@ begin
     book := TBook(mList.Items[i]);
     if Assigned(book) and Assigned(book.Cover) then
       book.Cover.Free;
+    CoverWorkerRemoveBook(book);
+    CoverWorkerUnregisterBook(book);
     FreeAndNil(book);
   end;
 

+ 4 - 1
src/main.pas

@@ -386,6 +386,7 @@ var
     end;
 
     book:=TBook.Create(PanelBackground);
+    CoverWorkerRegisterBook(book);
     book.FilePath:= dest;
     if optExtractMeta then
     begin
@@ -557,7 +558,7 @@ begin
     SetPdfCoverGenerationEnabled(autoPdfCover); // re-enable per settings
   end;
 
- for i:=0 to bookList.Count-1 do
+for i:=0 to bookList.Count-1 do
 begin
   with bookList.Books[i] do
   begin
@@ -566,6 +567,7 @@ begin
     Cover.Parent:=PanelBackground;
     EnsureScaledToCoverSize;
   end;
+  CoverWorkerRegisterBook(bookList.Books[i]);
 end;
 
  RearrangeBooksOnScreen();
@@ -604,6 +606,7 @@ begin
          b := bookList.Books[i];
          // Ensure the background worker won't touch this book anymore
          CoverWorkerRemoveBook(b);
+         CoverWorkerUnregisterBook(b);
          if Assigned(b.Cover) then b.Cover.Free;
          bookList.Remove(b);
          b.Free;

+ 49 - 2
src/unitCoverWorker.pas

@@ -26,6 +26,10 @@ procedure CoverWorkerStop;
 { Remove a specific book from the pending queue (e.g., before deleting it) }
 procedure CoverWorkerRemoveBook(B: TBook);
 
+{ Register/unregister books so the worker can avoid using freed objects }
+procedure CoverWorkerRegisterBook(B: TBook);
+procedure CoverWorkerUnregisterBook(B: TBook);
+
 implementation
 
 type
@@ -45,6 +49,7 @@ type
 var
   GPdfQueue: TThreadList; // holds TBook references
   GWorker  : TCoverWorker;
+  GAliveBooks: TThreadList; // tracks currently alive books
 
 {--- helpers ------------------------------------------------------------------}
 
@@ -152,6 +157,8 @@ procedure EnsureQueue;
 begin
   if GPdfQueue = nil then
     GPdfQueue := TThreadList.Create;
+  if GAliveBooks = nil then
+    GAliveBooks := TThreadList.Create;
 end;
 
 {--- public API ----------------------------------------------------------------}
@@ -195,6 +202,45 @@ begin
     GPdfQueue.UnlockList;
   end;
 end;
+
+procedure CoverWorkerRegisterBook(B: TBook);
+var l: TList;
+begin
+  if B = nil then Exit;
+  EnsureQueue;
+  l := GAliveBooks.LockList;
+  try
+    if l.IndexOf(B) < 0 then l.Add(B);
+  finally
+    GAliveBooks.UnlockList;
+  end;
+end;
+
+procedure CoverWorkerUnregisterBook(B: TBook);
+var l: TList; idx: Integer;
+begin
+  if (B = nil) or (GAliveBooks = nil) then Exit;
+  l := GAliveBooks.LockList;
+  try
+    idx := l.IndexOf(B);
+    if idx >= 0 then l.Delete(idx);
+  finally
+    GAliveBooks.UnlockList;
+  end;
+end;
+
+function BookIsAlive(B: TBook): Boolean;
+var l: TList;
+begin
+  Result := False;
+  if (B = nil) or (GAliveBooks = nil) then Exit;
+  l := GAliveBooks.LockList;
+  try
+    Result := l.IndexOf(B) >= 0;
+  finally
+    GAliveBooks.UnlockList;
+  end;
+end;
 procedure CoverWorkerEnqueueBookIfMissing(B: TBook);
 var
   l: TList;
@@ -322,13 +368,14 @@ begin
     end;
 
     // Skip if it no longer needs a cover
-    if not (IsPdf(B.FilePath) and HasGenericCover(B)) then
+    if (not BookIsAlive(B)) or (not (IsPdf(B.FilePath) and HasGenericCover(B))) then
     begin
       Sleep(5);
       Continue;
     end;
 
     // Read current cover size in main thread to avoid cross-thread UI access
+    if not BookIsAlive(B) then Continue;
     FSizeBook := B; FSizeW := 0; FSizeH := 0;
     Synchronize(@ReadCoverSize);
     W := FSizeW; H := FSizeH;
@@ -337,7 +384,7 @@ begin
     LogInfoFmt('Generating cover for: %s', [B.FilePath]);
     Img := GeneratePdfCover(B.FilePath, W, H);
 
-    if (Img <> '') and FileExists(Img) then
+    if BookIsAlive(B) and (Img <> '') and FileExists(Img) then
     begin
       // Pass data to main thread via fields + Synchronize
       FApplyBook := B;