Skip to content

Engines: updating calibre no longer fails on its read-only files - #1124

Merged
KnifeLemon merged 1 commit into
mainfrom
fix/calibre-update-read-only
Nov 4, 2026
Merged

KnifeLemon merged 1 commit into
mainfrom
fix/calibre-update-read-only

Conversation

@KnifeLemon

Copy link
Copy Markdown
Owner

Fixes #1084
Fixes #1092
Fixes #1109

Root cause

Updating calibre fails, every time. A fresh install works, which is why not everyone is affected. 2.3.0 was the first release to change the calibre pin (9.15.0 to 9.17.0), so it was the first time anyone had an old copy to replace.

  1. The calibre MSI marks every file read-only (msidbFileAttributesReadOnly on all 1345 rows of 9.15.0 and all 1357 of 9.17.0), and msiexec /a keeps that flag.
  2. Replace deleted the old copy in place with Directory.Delete(target, recursive: true), which stops at the first read-only file. In name order that is app\bin\amatch.dll. By then the marker has already gone, so the card falls back to "Install" with no Update or Remove, as both screenshots show, and every retry hits the same file.
  3. The finally then tried to delete the staging folder, a tree from the same MSI, and threw the identical message over the real exception. That is why the stack pointed at the cleanup. Each attempt also left its staging folder behind (about 666 MB).

How it was confirmed

The real EngineInstaller.InstallAsync was run against the real pinned MSIs (SHA-256 checked) in temporary engines roots:

Scenario Code Result
Fresh 9.17.0 main OK
Fresh 9.15.0 main OK
9.15.0 to 9.17.0 main Access to the path 'amatch.dll' is denied, same stack as #1092, marker gone, ~xxxxxx left behind
Retry, with only the finally made non-throwing main + probe Real exception shown: Replace deleting the old copy, and the cleanup failing as well
Retry from that state (no marker, leftovers) this PR OK, ebook-convert --version reports 9.17.0
Clean 9.15.0 to 9.17.0 this PR OK, nothing left in the root

#1092's log has the same stack. #1084 and #1109 have no log but show the same message, and #1084's screenshot shows the same card state.

Changes

  • Root cause: Replace moves the previous version aside, as Uninstall has done since Engine install/remove: half-deleted engines, leftover .part downloads, busy message #963, instead of deleting it in place. While an engine runs the move fails and the working copy keeps its marker. Extensions (oxt) now use Replace too.
  • Cleanup can't hide the real error: the staging cleanup goes through TryDeleteFolder for every kind, so a failure is logged and the error from the try block is the one reported. TryDelete in the other finally now also catches UnauthorizedAccessException.
  • Cleanup copes with read-only files and brief locks: new DeleteFolder clears the flag (not through links) and retries for about 2 seconds. TryDeleteFolder and UninstallCleanup use it. Before this, removing calibre left a ~removed-calibre-* folder that could never be cleared, and the uninstall cleanup failed on it.

LibreOffice is not affected today: its pinned 26.2.6 MSI has no read-only files (0 of 19496 File rows). It gets the same protection anyway.

Tests

Seven new tests in EngineInstallerTests, building their data in code. All seven fail with the old delete behaviour restored, the main one with the reporters' exact amatch.dll message. InStagingAsync is split out so a test can put a read-only file in staging and fail inside it.

Focused runs pass: engine installer, portable, real path and own copy tests (55), EnginesCommandTests (13), EnginesPageTests (6). dotnet format --verify-no-changes is clean on the changed files.

Not in this PR

Staging folders left by failed 2.3.0 attempts are not removed. Their ~xxxxxx names don't say which package or process they belong to, so clearing them safely needs an age rule. The reporter comments cover deleting them by hand.

🤖 Generated with Claude Code

Every file of the calibre MSI is read-only. Replace deleted the previous copy
in place, which stopped at the first one (amatch.dll), after the marker had
gone, so the card fell back to Install and every retry failed the same way.
The staging cleanup then threw the same message over the real error.

Replace moves the previous version aside as Uninstall does, the cleanup can no
longer replace the error from the try block, and DeleteFolder copes with
read-only files and brief locks for every kind and the uninstall cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant