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;