Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,40 @@ public Task<string> AddCredentialAsync(string providerName, string accountName,
};

AddItem(attrs);

// SecItemAdd can lag SecItemCopyMatching visibility under concurrent
// keychain access, so a caller doing add-then-select/delete — e.g.
// `accounts add` offering to activate the new credential — can race
// the item's own appearance and see the follow-up lookup miss it.
// Confirm the item is queryable before returning so that can't happen.
ConfirmItemVisible(attrs.Service, accountId);

return Task.FromResult(accountId);
}

/// <summary>
/// Polls for a just-added item to become visible to
/// <c>SecItemCopyMatching</c>, closing the brief add-visibility window.
/// Best-effort: returns after a bounded wait even if the item never
/// appears, leaving any genuine failure to the caller's own lookup.
/// </summary>
private static void ConfirmItemVisible(string service, string account)
{
const int maxAttempts = 20;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
if (QuerySingleItem(service, account, includeData: false) is not null)
{
return;
}

if (attempt < maxAttempts)
{
Thread.Sleep(25);
}
}
}

/// <inheritdoc />
public Task<IEnumerable<CredentialSummary>> ListCredentialsAsync(string providerName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,39 @@ public Task<string> AddCredentialAsync(string providerName, string accountName,
var label = $"{_appIdentifier}: {providerName}/{accountName}";

StoreItem(attrs, label, credentialData);

// A just-stored Secret Service item isn't always immediately visible
// to a follow-up search under concurrent access, so a caller doing
// add-then-select/delete can race the item's appearance. Confirm it's
// queryable before returning so that can't happen.
ConfirmItemVisible(providerName, accountId);

return Task.FromResult(accountId);
}

/// <summary>
/// Polls for a just-stored item to become visible to a Secret Service
/// lookup, closing the brief store-visibility window. Best-effort:
/// returns after a bounded wait even if the item never appears, leaving
/// any genuine failure to the caller's own lookup.
/// </summary>
private void ConfirmItemVisible(string providerName, string accountId)
{
const int maxAttempts = 20;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
if (LookupCredentialByAccountId(providerName, accountId) is not null)
{
return;
}

if (attempt < maxAttempts)
{
Thread.Sleep(25);
}
}
}

/// <inheritdoc />
public Task<IEnumerable<CredentialSummary>> ListCredentialsAsync(string providerName)
{
Expand Down