Skip to content

Fix various things that might keep thumbnail files locked (BL-16122)#7808

Open
JohnThomson wants to merge 1 commit intomasterfrom
unlociThumbnails
Open

Fix various things that might keep thumbnail files locked (BL-16122)#7808
JohnThomson wants to merge 1 commit intomasterfrom
unlociThumbnails

Conversation

@JohnThomson
Copy link
Copy Markdown
Contributor

@JohnThomson JohnThomson commented Apr 3, 2026


Open with Devin

This change is Reviewable

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 7 files

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 RebuildThumbNail has an additional undisposed image path for readonly thumbnails

In RebuildThumbNail at BookThumbNailer.cs:536-543, when the thumbnail is readonly, TryGetPremadeThumbnail returns an image loaded from file (caller-owned per the new comments). This image is passed to callback(book.BookInfo, thumb). For RebuildThumbNailNow (line 501), the callback disposes it correctly. For CollectionModel.UpdateThumbnailAsync (line 724-728), it also disposes correctly. However, for the 4-parameter UpdateThumbnailAsync overload at line 706, callers provide their own callbacks and may not dispose the image — this is a pre-existing concern not introduced by this PR, but worth noting since the PR adds ownership documentation.

(Refers to lines 536-543)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

book,
thumbnailOptions,
(info, image) => { },
(info, image) => image?.Dispose(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Disposing shared Resources.Error70x70 singleton in RebuildThumbNailNow callback

The new callback (info, image) => image?.Dispose() will dispose Resources.Error70x70 when error paths are hit. In GetThumbNailOfBookCover (BookThumbNailer.cs:59,66,100,106), the callback receives Resources.Error70x70 directly (not a clone). This flows through RebuildThumbNail at BookThumbNailer.cs:551 (image => callback(book.BookInfo, image)) and reaches the disposing callback. Since ResourceManager.GetObject caches and returns the same Bitmap instance on each access, disposing it corrupts the shared resource. All subsequent uses of Resources.Error70x70 elsewhere (e.g., CollectionModel.cs:746 calling .Save(), CollectionApi.cs:775 calling ConvertImageToStream()) will fail with ObjectDisposedException or GDI+ errors.

Prompt for agents
The callback in RebuildThumbNailNow disposes the image it receives, but GetThumbNailOfBookCover (lines 59, 66, 100, 106) passes the shared Resources.Error70x70 singleton directly to the callback in several error paths. Disposing this shared resource corrupts it for all future uses across the application.

Possible fixes:
1. In RebuildThumbNailNow, only dispose images that are not Resources.Error70x70 (fragile, not recommended).
2. In GetThumbNailOfBookCover, clone Resources.Error70x70 before passing it to callbacks, so callers can safely dispose the clone. This would be consistent with CreateThumbnailOfCoverImage (line 319) which already clones its image before calling the callback.
3. Have the callback check whether the image is a shared resource before disposing it.

Option 2 is likely the cleanest: change all callback(Resources.Error70x70) calls in GetThumbNailOfBookCover to callback(Resources.Error70x70.Clone() as Image), matching the pattern already used in CreateThumbnailOfCoverImage at line 319.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +724 to +728
(bookInfo, image) =>
{
RefreshOneThumbnail(bookInfo, image);
image?.Dispose();
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Disposing shared Resources.Error70x70 singleton in UpdateThumbnailAsync callback

The new callback wrapping RefreshOneThumbnail calls image?.Dispose() on the image it receives. Through the same error paths in GetThumbNailOfBookCover (BookThumbNailer.cs:66,100,106), the image can be the shared Resources.Error70x70 singleton. The flow is: UpdateThumbnailAsyncRebuildThumbNailAsyncRebuildThumbNailGetThumbNailOfBookCover → error path passes Resources.Error70x70 to callback → disposed. Note that while ErrorBook is filtered at CollectionModel.cs:713, the HasFatalError, CreateThumbnailOfCoverImage failure, and exception-catch paths in GetThumbNailOfBookCover are still reachable. This is the same root cause as the RebuildThumbNailNow bug but in a different call site.

Prompt for agents
The callback added in UpdateThumbnailAsync(Book.Book book) disposes the image, but error paths in GetThumbNailOfBookCover (BookThumbNailer.cs:66,100,106) pass the shared Resources.Error70x70 singleton to the callback. Disposing it corrupts the shared resource.

The fix should be coordinated with the same issue in RebuildThumbNailNow. The cleanest approach is to ensure GetThumbNailOfBookCover always passes a clone of Resources.Error70x70 (matching the pattern in CreateThumbnailOfCoverImage at line 319 which already clones). This way all callers can safely dispose the image they receive.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant