diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 65b862a5396f..50534346ffeb 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -19,6 +19,8 @@ --> ## Upcoming Release +* Fixed an issue that may cause authentication errors in multi-process scenarios such as running multiple Azure PowerShell cmdlets using `Start-Job` [#9448] + ## Version 1.9.0 * Supported discovering environment setting by default and adding environment via `Add-AzEnvironment` * Update preloaded assemblies [#12024], [#11976] diff --git a/src/Accounts/Authentication.ResourceManager/ProtectedFileProvider.cs b/src/Accounts/Authentication.ResourceManager/ProtectedFileProvider.cs index dcdb4e384219..a8aaa3f709e1 100644 --- a/src/Accounts/Authentication.ResourceManager/ProtectedFileProvider.cs +++ b/src/Accounts/Authentication.ResourceManager/ProtectedFileProvider.cs @@ -36,13 +36,18 @@ public abstract class ProtectedFileProvider : IFileProvider, IDisposable public const int MaxTries = 30; static readonly TimeSpan RetryInterval = TimeSpan.FromMilliseconds(500); protected Stream _stream; - object _initializationLock = new object(); + + /// + /// Use a Mutex to prevent cross-process file I/O + /// + /// + private static readonly Mutex _initializationLock = new Mutex(false, @"Local\AzurePowerShellProtectedFileProviderInit"); public string FilePath { get; set; } protected IDataStore DataStore { get; set; } /// - /// + /// /// public Stream Stream { @@ -87,7 +92,8 @@ public StreamWriter CreateWriter() protected virtual void InitializeStream() { - lock (_initializationLock) + _initializationLock.WaitOne(); + try { if (_stream == null) { @@ -96,10 +102,13 @@ protected virtual void InitializeStream() { throw new UnauthorizedAccessException(string.Format(Resources.FileLockFailure, FilePath)); } - _stream = stream; } } + finally + { + _initializationLock.ReleaseMutex(); + } } protected abstract Stream AcquireLock(string filePath); diff --git a/src/Accounts/Authentication/Authentication/ProtectedFileTokenCache.cs b/src/Accounts/Authentication/Authentication/ProtectedFileTokenCache.cs index c6306f62f40d..1a5984d6a930 100644 --- a/src/Accounts/Authentication/Authentication/ProtectedFileTokenCache.cs +++ b/src/Accounts/Authentication/Authentication/ProtectedFileTokenCache.cs @@ -12,12 +12,14 @@ // limitations under the License. // ---------------------------------------------------------------------------------- +using Hyak.Common; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Properties; using Microsoft.IdentityModel.Clients.ActiveDirectory; using System; using System.IO; using System.Security.Cryptography; +using System.Threading; #if NETSTANDARD namespace Microsoft.Azure.Commands.Common.Authentication.Core @@ -41,7 +43,10 @@ public class ProtectedFileTokenCache : TokenCache, IAzureTokenCache #endif "TokenCache.dat"); - private static readonly object fileLock = new object(); + /// + /// A mutex to prevent IO to token cache file across threads / processes. + /// + private static readonly Mutex fileLock = new Mutex(false, @"Local\AzurePowerShellAdalTokenCacheFile"); private static readonly Lazy instance = new Lazy(() => new ProtectedFileTokenCache()); @@ -123,12 +128,13 @@ void EnsureStateSaved() private void ReadFileIntoCache(string cacheFileName = null) { - if(cacheFileName == null) + if (cacheFileName == null) { cacheFileName = ProtectedFileTokenCache.CacheFileName; } - lock (fileLock) + fileLock.WaitOne(); + try { if (_store.FileExists(cacheFileName)) { @@ -150,11 +156,15 @@ private void ReadFileIntoCache(string cacheFileName = null) } } } + finally + { + fileLock.ReleaseMutex(); + } } private void WriteCacheIntoFile(string cacheFileName = null) { - if(cacheFileName == null) + if (cacheFileName == null) { cacheFileName = ProtectedFileTokenCache.CacheFileName; } @@ -165,19 +175,25 @@ private void WriteCacheIntoFile(string cacheFileName = null) var dataToWrite = Serialize(); #endif - lock(fileLock) + fileLock.WaitOne(); + try { if (HasStateChanged) { _store.WriteFile(cacheFileName, dataToWrite); - HasStateChanged = false; + HasStateChanged = false; } } + finally + { + fileLock.ReleaseMutex(); + } } private void EnsureCacheFile(string cacheFileName = null) { - lock (fileLock) + fileLock.WaitOne(); + try { if (_store.FileExists(cacheFileName)) { @@ -207,6 +223,10 @@ private void EnsureCacheFile(string cacheFileName = null) #endif _store.WriteFile(cacheFileName, dataToWrite); } + finally + { + fileLock.ReleaseMutex(); + } } } } diff --git a/src/Accounts/Authentication/AzureSessionInitializer.cs b/src/Accounts/Authentication/AzureSessionInitializer.cs index 647b3e37b120..b06d57dbf5ab 100644 --- a/src/Accounts/Authentication/AzureSessionInitializer.cs +++ b/src/Accounts/Authentication/AzureSessionInitializer.cs @@ -26,6 +26,7 @@ #if NETSTANDARD using Microsoft.Azure.Commands.Common.Authentication.Core; #endif +using Hyak.Common; namespace Microsoft.Azure.Commands.Common.Authentication { @@ -73,8 +74,10 @@ static IAzureTokenCache InitializeTokenCache(IDataStore store, string cacheDirec var cachePath = Path.Combine(cacheDirectory, cacheFile); result = new ProtectedFileTokenCache(cachePath, store); } - catch + catch (Exception ex) { + TracingAdapter.Information("[AzureSessionInitializer]: Cannot initialize token cache in 'CurrentUser' mode. Falling back to 'Process' mode."); + TracingAdapter.Information($"[AzureSessionInitializer]: Message: {ex.Message}; Stacktrace: {ex.StackTrace}"); } } @@ -159,10 +162,12 @@ static ContextAutosaveSettings InitializeSessionSettings(IDataStore store, strin store.WriteFile(autoSavePath, JsonConvert.SerializeObject(result)); } } - catch + catch (Exception ex) { // ignore exceptions in reading settings from disk result.Mode = ContextSaveMode.Process; + TracingAdapter.Information("[AzureSessionInitializer]: Cannot read settings from disk. Falling back to 'Process' mode."); + TracingAdapter.Information($"[AzureSessionInitializer]: Message: {ex.Message}; Stacktrace: {ex.StackTrace}"); } return result;