diff --git a/auth/CHANGELOG.md b/auth/CHANGELOG.md index 401b9e956c..e55450d375 100644 --- a/auth/CHANGELOG.md +++ b/auth/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 5.1.1 - 2025-10-28 + +* [#2111](https://github.com/microsoft/vscode-azuretools/pull/2111) Same as https://github.com/microsoft/vscode-azuretools/pull/2110 but a better fix. + ## 5.1.0 - 2025-10-27 * [#2102](https://github.com/microsoft/vscode-azuretools/pull/2102) Fixes an issue causing infinite event loops especially in https://vscode.dev/azure diff --git a/auth/package-lock.json b/auth/package-lock.json index 6c4dc1e031..002741776f 100644 --- a/auth/package-lock.json +++ b/auth/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/vscode-azext-azureauth", - "version": "5.1.0", + "version": "5.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/vscode-azext-azureauth", - "version": "5.1.0", + "version": "5.1.1", "license": "MIT", "dependencies": { "@azure/arm-resources-subscriptions": "^2.1.0", diff --git a/auth/package.json b/auth/package.json index 2b49dd3fb5..fe7c5742aa 100644 --- a/auth/package.json +++ b/auth/package.json @@ -1,7 +1,7 @@ { "name": "@microsoft/vscode-azext-azureauth", "author": "Microsoft Corporation", - "version": "5.1.0", + "version": "5.1.1", "description": "Azure authentication helpers for Visual Studio Code", "tags": [ "azure", diff --git a/auth/src/VSCodeAzureSubscriptionProvider.ts b/auth/src/VSCodeAzureSubscriptionProvider.ts index 43a4f3f2df..f8a74525cc 100644 --- a/auth/src/VSCodeAzureSubscriptionProvider.ts +++ b/auth/src/VSCodeAzureSubscriptionProvider.ts @@ -25,82 +25,95 @@ let armSubs: typeof import('@azure/arm-resources-subscriptions') | undefined; * provider. */ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvider, vscode.Disposable { - private lastSignInEventFired: number = 0; - private suppressSignInEvents: boolean = false; + private listenerDisposable: vscode.Disposable | undefined; - private lastSignOutEventFired: number = 0; + private readonly onDidSignInEmitter = new vscode.EventEmitter(); + private readonly onDidSignOutEmitter = new vscode.EventEmitter(); + private lastEventFired: number = 0; + private suppressEvents: boolean = false; private priorAccounts: vscode.AuthenticationSessionAccountInformation[] | undefined; + private accountsRemovedPromise: Promise | undefined; // So that customers can easily share logs, try to only log PII using trace level public constructor(private readonly logger?: vscode.LogOutputChannel) { - // Load accounts initially, then onDidChangeSessions can compare against them - void vscode.authentication.getAccounts(getConfiguredAuthProviderId()).then(accounts => { - this.priorAccounts = Array.from(accounts); // The Array.from is to get rid of the readonly marker on the array returned by the API - }); + void this.accountsRemoved(); // Initialize priorAccounts } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public onDidSignIn(callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable { - return this.onDidChangeSessions(true, callback, thisArg, disposables); + public dispose(): void { + this.listenerDisposable?.dispose(); + this.onDidSignInEmitter.dispose(); + this.onDidSignOutEmitter.dispose(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public onDidSignOut(callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable { - return this.onDidChangeSessions(false, callback, thisArg, disposables); + public onDidSignIn(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable { + this.listenIfNeeded(); + return this.onDidSignInEmitter.event(callback, thisArg, disposables); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private onDidChangeSessions(signIn: boolean, callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable { - const isASignInEvent = async () => { - const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId())); // The Array.from is to get rid of the readonly marker on the array returned by the API - const priorAccountCount = this.priorAccounts?.length ?? 0; - this.priorAccounts = currentAccounts; - - // The only way a sign out happens is if an account is removed entirely from the list of accounts - if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) { - return false; - } - - return true; - } + public onDidSignOut(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable { + this.listenIfNeeded(); + return this.onDidSignOutEmitter.event(callback, thisArg, disposables); + } - const wrappedCallback = () => { - const immediate = setImmediate(() => { - clearImmediate(immediate); - void callback.call(thisArg); - }); + private listenIfNeeded(): void { + if (this.listenerDisposable) { + return; } - const disposable = vscode.authentication.onDidChangeSessions(async e => { + this.listenerDisposable = vscode.authentication.onDidChangeSessions(async e => { // Ignore any sign in that isn't for the configured auth provider if (e.provider.id !== getConfiguredAuthProviderId()) { return; } - if (signIn) { - if (this.suppressSignInEvents || Date.now() < this.lastSignInEventFired + EventDebounce) { - return; - } else if (await isASignInEvent()) { - this.lastSignInEventFired = Date.now(); - wrappedCallback(); - } + if (this.suppressEvents || Date.now() < this.lastEventFired + EventDebounce) { + return; + } + + this.lastEventFired = Date.now(); + + if (!await this.accountsRemoved()) { + this.logger?.debug('auth: Firing onDidSignIn event'); + this.onDidSignInEmitter.fire(); } else { - if (Date.now() < this.lastSignOutEventFired + EventDebounce) { - return; - } else if (!await isASignInEvent()) { - this.lastSignOutEventFired = Date.now(); - wrappedCallback(); - } + this.logger?.debug('auth: Firing onDidSignOut event'); + this.onDidSignOutEmitter.fire(); } }); - - disposables?.push(disposable); - return disposable; } - public dispose(): void { - // No-op, this class no longer has disposables + private async accountsRemoved(): Promise { + // If there's already an ongoing accountsRemoved operation, return its result + if (this.accountsRemovedPromise) { + return this.accountsRemovedPromise; + } + + // Create a new promise for this operation + this.accountsRemovedPromise = (async () => { + try { + this.suppressEvents = true; + const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId())); + const priorAccountCount = this.priorAccounts?.length ?? 0; + this.priorAccounts = currentAccounts; + + // The only way a sign out happens is if an account is removed entirely from the list of accounts + if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) { + return true; + } + + return false; + } finally { + this.suppressEvents = false; + } + })(); + + try { + return await this.accountsRemovedPromise; + } finally { + // Clear the promise when done so future calls can proceed + this.accountsRemovedPromise = undefined; + } } /** @@ -166,7 +179,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide const allSubscriptions: AzureSubscription[] = []; let accountCount: number; // only used for logging try { - this.suppressSignInEvents = true; + this.suppressEvents = true; // Get the list of tenants from each account (filtered or all) const accounts = isGetSubscriptionsAccountFilter(filter) ? [filter.account] : await vscode.authentication.getAccounts(getConfiguredAuthProviderId()); accountCount = accounts.length; @@ -186,7 +199,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide allSubscriptions.push(...await this.getSubscriptionsForTenant(account)); } } finally { - this.suppressSignInEvents = false; + this.suppressEvents = false; } // It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions @@ -262,7 +275,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide public async signIn(tenantId?: string, account?: vscode.AuthenticationSessionAccountInformation): Promise { this.logger?.debug(`auth: Signing in (account="${account?.label ?? 'none'}") (tenantId="${tenantId ?? 'none'}")`); try { - this.suppressSignInEvents = true; + this.suppressEvents = true; const session = await getSessionFromVSCode([], tenantId, { createIfNone: true, // If no account is provided, then clear the session preference which tells VS Code to show the account picker @@ -271,7 +284,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide }); return !!session; } finally { - this.suppressSignInEvents = false; + this.suppressEvents = false; } }