feat!: thread CancellationToken through ICredentialManager - #76
Merged
Conversation
BREAKING: every ICredentialManager member gains a trailing `CancellationToken cancellationToken = default`. Callers are unaffected -- the parameter is optional -- but any external implementation must update its signatures. ICredentialCollector and ICredentialSummaryProvider are untouched, so the provider packages are unaffected. The libsecret backend binds the token to a GCancellable for every call, via a CancelScope that registers g_cancellable_cancel on the token and unrefs the handle on dispose. g_cancellable_cancel is thread-safe by contract, which is the point: it fires from the token callback while another thread sits blocked inside a synchronous libsecret call. A cancelled call now surfaces as OperationCanceledException rather than as a generic wrapped GError, because cancellation is checked before the GError is translated. The Keychain backend honours the token at entry. Security.framework's SecItem* API takes no cancellable, so there is no mid-call cancellation there; that is documented rather than faked. What this does NOT fix, established by experiment rather than assumed: it does not rescue the KWallet wedge that prompted the issue. Against ksecretd the cancellation callback demonstrably fires -- instrumented and observed at 4.0s on a pool thread -- and the call still never returns. ksecretd emits the prompt's Completed signal with an `ao` payload where libsecret expects `o`, so libsecret logs "received unexpected result type ao from Completed signal" and its secret_service_real_prompt_async GTask is "finalized without ever returning". The nested main loop the sync wrapper runs never exits. Cancellation cannot rescue a task libsecret has already abandoned. That is an upstream incompatibility, recorded in the backend's remarks. xUnit1051 is suppressed in the test project: it fires at all 530 call sites into ICredentialManager now that the members accept a token, and threading one through every existing assertion would bury the change. New tests that exercise cancellation pass a real token explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #74. Breaking — this makes the next release 2.0.0.
What changed
Every
ICredentialManagermember gains a trailingCancellationToken cancellationToken = default.ICredentialCollectorandICredentialSummaryProviderare untouched, so the provider packages are unaffected.The libsecret backend binds the token to a
GCancellableper call.g_cancellable_cancelis thread-safe by contract, which is the whole point — it fires from the token callback while another thread sits blocked inside a synchronous libsecret call. A cancelled call surfaces asOperationCanceledExceptionrather than a generic wrapped GError, because cancellation is checked before the GError is translated.The Keychain backend honours the token at entry.
SecItem*takes no cancellable, so there is no mid-call cancellation there — documented, not faked.It does not fix the KWallet wedge, and I checked rather than assumed
This is the part worth your attention. I instrumented the KWallet scenario to see whether cancellation actually escapes it:
The cancellation works — the callback fires, on time, on a pool thread, while the main thread is blocked. The call still never returns, because
ksecretdemits the prompt'sCompletedsignal with anaopayload where libsecret expectso. libsecret's prompt GTask is then abandoned without ever returning, so the nested main loop the synchronous wrapper runs never exits. Cancellation cannot rescue a task libsecret has already given up on.That is an upstream libsecret/KWallet incompatibility. It is recorded in
LibsecretCredentialManager's remarks with the exact log lines, and the interface remarks now say plainly that honouring a token is not a guarantee every block is escapable. I had written the opposite in an earlier draft of these docs and corrected it once the experiment disproved it.So: the API is now cancellable, which is what was chosen and is worth having. KWallet remains unsupported for the reason #19 already documented.
Verification
Build clean, 416 tests (358 passed, 58 skipped), up from 412.
New tests assert that an already-cancelled token surfaces as
OperationCanceledExceptionon both the file and libsecret backends — the libsecret one proves theGCancellablepath is reached, since that is where the cancellation is observed.One suppression, deliberately
xUnit1051is now suppressed in the test project. It fires at 530 call sites intoICredentialManagerthe moment its members accept a token. Threading one through every existing assertion would bury the actual change in mechanical noise for a benefit — prompter teardown of sub-second tests — that does not apply here. The rationale is written into the csproj alongside the existing suppressions. New cancellation tests pass a real token explicitly.Not in this PR
The version bump.
[Unreleased]records that this makes the next release 2.0.0; cutting it is a separate release PR, matching #40.🤖 Generated with Claude Code