diff --git a/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs index a1afb8f62..1a6866fb6 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs @@ -7,6 +7,7 @@ using Atlassian.Bitbucket.Cloud; using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; +using GitCredentialManager.Tests.Objects; using Moq; using Xunit; @@ -16,7 +17,6 @@ public class BitbucketOAuth2ClientTest { private Mock httpClient = new Mock(MockBehavior.Strict); private Mock settings = new Mock(MockBehavior.Loose); - private Mock trace = new Mock(MockBehavior.Loose); private Mock browser = new Mock(MockBehavior.Strict); private Mock codeGenerator = new Mock(MockBehavior.Strict); private IEnumerable scopes = new List(); @@ -55,7 +55,7 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient Uri finalCallbackUri = MockFinalCallbackUri(); Bitbucket.Cloud.BitbucketOAuth2Client client = GetBitbucketOAuth2Client(); - + MockGetAuthenticationCodeAsync(finalCallbackUri, clientId, client.Scopes); MockCodeGenerator(); @@ -68,8 +68,9 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient [Fact] public async Task BitbucketOAuth2Client_GetDeviceCodeAsync() { - var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); - await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct)); + var trace2 = new NullTrace2(); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); + await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct)); } [Theory] @@ -79,7 +80,8 @@ public async Task BitbucketOAuth2Client_GetDeviceCodeAsync() [InlineData("https", "example.com/", "john", "https://example.com/refresh_token")] public void BitbucketOAuth2Client_GetRefreshTokenServiceName(string protocol, string host, string username, string expectedResult) { - var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); + var trace2 = new NullTrace2(); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); var input = new InputArguments(new Dictionary { ["protocol"] = protocol, @@ -100,7 +102,8 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result) private Bitbucket.Cloud.BitbucketOAuth2Client GetBitbucketOAuth2Client() { - var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); + var trace2 = new NullTrace2(); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); client.CodeGenerator = codeGenerator.Object; return client; } diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs index 97d194764..e2e7225db 100644 --- a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs +++ b/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs @@ -7,6 +7,7 @@ using Atlassian.Bitbucket.DataCenter; using GitCredentialManager; using GitCredentialManager.Authentication.OAuth; +using GitCredentialManager.Tests.Objects; using Moq; using Xunit; @@ -16,7 +17,6 @@ public class BitbucketOAuth2ClientTest { private Mock httpClient = new Mock(MockBehavior.Strict); private Mock settings = new Mock(MockBehavior.Loose); - private Mock trace = new Mock(MockBehavior.Loose); private Mock browser = new Mock(MockBehavior.Strict); private Mock codeGenerator = new Mock(MockBehavior.Strict); private CancellationToken ct = new CancellationToken(); @@ -77,7 +77,8 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result, private Bitbucket.DataCenter.BitbucketOAuth2Client GetBitbucketOAuth2Client() { - var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace.Object); + var trace2 = new NullTrace2(); + var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); client.CodeGenerator = codeGenerator.Object; return client; } diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs index a17bdf5f3..4ae07c0a5 100644 --- a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs +++ b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs @@ -48,7 +48,7 @@ private async Task ExecuteAsync(Uri url, string userName, bool showOAuth, b if (!viewModel.WindowResult || viewModel.SelectedMode == AuthenticationModes.None) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } switch (viewModel.SelectedMode) diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs index 1cacd769e..01471edf8 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -4,7 +4,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Atlassian.Bitbucket.Cloud; using GitCredentialManager; using GitCredentialManager.Authentication; using GitCredentialManager.Authentication.OAuth; @@ -128,12 +127,12 @@ public async Task GetCredentialsAsync(Uri targetUri, st { if (!output.TryGetValue("username", out userName)) { - throw new Exception("Missing username in response"); + throw new Trace2Exception(Context.Trace2, "Missing username in response"); } if (!output.TryGetValue("password", out password)) { - throw new Exception("Missing password in response"); + throw new Trace2Exception(Context.Trace2, "Missing password in response"); } return new CredentialsPromptResult( diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index f1950aca1..f3f653f01 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Net; using System.Net.Http; using System.Threading.Tasks; using Atlassian.Bitbucket.Cloud; @@ -79,7 +78,8 @@ public async Task GetCredentialAsync(InputArguments input) if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") && BitbucketHelper.IsBitbucketOrg(input)) { - throw new Exception("Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS."); + throw new Trace2Exception(_context.Trace2, + "Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS."); } var authModes = await GetSupportedAuthenticationModesAsync(input); @@ -145,8 +145,9 @@ private async Task GetRefreshedCredentials(InputArguments input, Au var result = await _bitbucketAuth.GetCredentialsAsync(remoteUri, input.UserName, authModes); if (result is null || result.AuthenticationMode == AuthenticationModes.None) { - _context.Trace.WriteLine("User cancelled credential prompt"); - throw new Exception("User cancelled credential prompt."); + var message = "User cancelled credential prompt"; + _context.Trace.WriteLine(message); + throw new Trace2Exception(_context.Trace2, message); } switch (result.AuthenticationMode) @@ -176,8 +177,10 @@ private async Task GetRefreshedCredentials(InputArguments input, Au } catch (OAuth2Exception ex) { - _context.Trace.WriteLine("Failed to refresh existing OAuth credential using refresh token"); + var message = "Failed to refresh existing OAuth credential using refresh token"; + _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); + _context.Trace2.WriteError(message); // We failed to refresh the AT using the RT; log the refresh failure and fall through to restart // the OAuth authentication flow @@ -279,7 +282,7 @@ public async Task GetSupportedAuthenticationModesAsync(Inpu try { var authenticationMethods = await _restApiRegistry.Get(input).GetAuthenticationMethodsAsync(); - + var modes = AuthenticationModes.None; if (authenticationMethods.Contains(AuthenticationMethod.BasicAuth)) @@ -298,10 +301,14 @@ public async Task GetSupportedAuthenticationModesAsync(Inpu } catch (Exception ex) { - _context.Trace.WriteLine($"Failed to query '{input.GetRemoteUri()}' for supported authentication schemes."); + var format = "Failed to query '{0}' for supported authentication schemes."; + var message = string.Format(format, input.GetRemoteUri()); + + _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); + _context.Trace2.WriteError(message, format); - _context.Terminal.WriteLine($"warning: failed to query '{input.GetRemoteUri()}' for supported authentication schemes."); + _context.Terminal.WriteLine($"warning: {message}"); // Fall-back to offering all modes so the user is never blocked from authenticating by at least one mode return AuthenticationModes.All; @@ -356,7 +363,8 @@ private async Task ResolveOAuthUserNameAsync(InputArguments input, strin return result.Response.UserName; } - throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}"); + throw new Trace2Exception(_context.Trace2, + $"Failed to resolve username. HTTP: {result.StatusCode}"); } private async Task ResolveBasicAuthUserNameAsync(InputArguments input, string username, string password) @@ -367,7 +375,8 @@ private async Task ResolveBasicAuthUserNameAsync(InputArguments input, s return result.Response.UserName; } - throw new Exception($"Failed to resolve username. HTTP: {result.StatusCode}"); + throw new Trace2Exception(_context.Trace2, + $"Failed to resolve username. HTTP: {result.StatusCode}"); } private async Task ValidateCredentialsWork(InputArguments input, ICredential credentials, AuthenticationModes authModes) @@ -404,8 +413,10 @@ private async Task ValidateCredentialsWork(InputArguments input, ICredenti } catch (Exception ex) { - _context.Trace.WriteLine($"Failed to validate existing credentials using OAuth"); + var message = "Failed to validate existing credentials using OAuth"; + _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); + _context.Trace2.WriteError(message); } } @@ -419,8 +430,10 @@ private async Task ValidateCredentialsWork(InputArguments input, ICredenti } catch (Exception ex) { - _context.Trace.WriteLine($"Failed to validate existing credentials using Basic Auth"); + var message = "Failed to validate existing credentials using Basic Auth"; + _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); + _context.Trace2.WriteError(message); return false; } } diff --git a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs index 22c793239..1ca23d0f5 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs @@ -10,7 +10,12 @@ namespace Atlassian.Bitbucket { public abstract class BitbucketOAuth2Client : OAuth2Client { - public BitbucketOAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri, string clientSecret, ITrace trace) : base(httpClient, endpoints, clientId, redirectUri, clientSecret, false) + public BitbucketOAuth2Client(HttpClient httpClient, + OAuth2ServerEndpoints endpoints, + string clientId, + Uri redirectUri, + string clientSecret, + ITrace2 trace2) : base(httpClient, endpoints, clientId, trace2, redirectUri, clientSecret, false) { } diff --git a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs index 9e49c10c6..4b5edbbf7 100644 --- a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs +++ b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs @@ -10,9 +10,9 @@ namespace Atlassian.Bitbucket.Cloud { public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client { - public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace trace) + public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2) : base(httpClient, GetEndpoints(), - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace) + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2) { } @@ -62,7 +62,7 @@ private static string GetClientSecret(ISettings settings) return CloudConstants.OAuth2ClientSecret; } - + private static OAuth2ServerEndpoints GetEndpoints() { return new OAuth2ServerEndpoints( diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs index 378a30f94..97abd533c 100644 --- a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs +++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs @@ -12,9 +12,9 @@ namespace Atlassian.Bitbucket.DataCenter { public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client { - public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace trace) + public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2) : base(httpClient, GetEndpoints(settings), - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace) + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2) { } diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs index 0688e0323..689eba15f 100644 --- a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs +++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs @@ -20,7 +20,7 @@ public BitbucketRestApi(ICommandContext context) EnsureArgument.NotNull(context, nameof(context)); _context = context; - + } public async Task> GetUserInformationAsync(string userName, string password, bool isBearerToken) @@ -35,7 +35,7 @@ public async Task> GetUserInformationAsync(string userN } // Bitbucket Server/DC doesn't actually provide a REST API we can use to trade an access_token for the owning username, - // therefore this is always going to return a placeholder username, however this call does provide a way to validate the + // therefore this is always going to return a placeholder username, however this call does provide a way to validate the // credentials we do have var requestUri = new Uri(ApiUri, "api/1.0/users"); using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri)) @@ -131,9 +131,9 @@ public void Dispose() private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient(); - private Uri ApiUri + private Uri ApiUri { - get + get { var remoteUri = _context.Settings?.RemoteUri; if (remoteUri == null) diff --git a/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs b/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs index 573140eb3..db1a0689c 100644 --- a/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs +++ b/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs @@ -8,6 +8,7 @@ public class OAuth2ClientRegistry : IRegistry private readonly HttpClient http; private ISettings settings; private readonly ITrace trace; + private readonly ITrace2 trace2; private Cloud.BitbucketOAuth2Client cloudClient; private DataCenter.BitbucketOAuth2Client dataCenterClient; @@ -16,6 +17,7 @@ public OAuth2ClientRegistry(ICommandContext context) this.http = context.HttpClientFactory.CreateClient(); this.settings = context.Settings; this.trace = context.Trace; + this.trace2 = context.Trace2; } public BitbucketOAuth2Client Get(InputArguments input) @@ -36,7 +38,7 @@ public void Dispose() dataCenterClient = null; } - private Cloud.BitbucketOAuth2Client CloudClient => cloudClient ??= new Cloud.BitbucketOAuth2Client(http, settings, trace); - private DataCenter.BitbucketOAuth2Client DataCenterClient => dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(http, settings, trace); + private Cloud.BitbucketOAuth2Client CloudClient => cloudClient ??= new Cloud.BitbucketOAuth2Client(http, settings, trace2); + private DataCenter.BitbucketOAuth2Client DataCenterClient => dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(http, settings, trace2); } -} \ No newline at end of file +} diff --git a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs index 8839eda37..9ab5770c8 100644 --- a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs +++ b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs @@ -23,7 +23,7 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenA var msAuth = new MicrosoftAuthentication(context); - await Assert.ThrowsAsync( + await Assert.ThrowsAsync( () => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName)); } } diff --git a/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs b/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs index ffd4ca730..3dd87cc93 100644 --- a/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs +++ b/src/shared/Core.Tests/Authentication/OAuth2ClientTests.cs @@ -36,7 +36,8 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync() IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, null, CancellationToken.None); @@ -81,7 +82,8 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams() IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None); @@ -116,7 +118,8 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams_Overri IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); await Assert.ThrowsAsync(() => client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None)); @@ -143,7 +146,8 @@ public async Task OAuth2Client_GetDeviceCodeAsync() server.TokenGenerator.UserCodes.Add(expectedUserCode); server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); OAuth2DeviceCodeResult result = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None); @@ -174,7 +178,8 @@ public async Task OAuth2Client_GetTokenByAuthorizationCodeAsync() server.TokenGenerator.AccessTokens.Add(expectedAccessToken); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); var authCodeResult = new OAuth2AuthorizationCodeResult(authCode, TestRedirectUri); OAuth2TokenResult result = await client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None); @@ -211,7 +216,8 @@ public async Task OAuth2Client_GetTokenByRefreshTokenAsync() server.TokenGenerator.AccessTokens.Add(expectedAccessToken); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); OAuth2TokenResult result = await client.GetTokenByRefreshTokenAsync(oldRefreshToken, CancellationToken.None); @@ -249,7 +255,8 @@ public async Task OAuth2Client_GetTokenByDeviceCodeAsync() server.TokenGenerator.AccessTokens.Add(expectedAccessToken); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); var deviceCodeResult = new OAuth2DeviceCodeResult(expectedDeviceCode, expectedUserCode, null, null); @@ -294,7 +301,8 @@ public async Task OAuth2Client_E2E_InteractiveWebFlowAndRefresh() IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); OAuth2AuthorizationCodeResult authCodeResult = await client.GetAuthorizationCodeAsync( expectedScopes, browser, null, CancellationToken.None); @@ -343,7 +351,8 @@ public async Task OAuth2Client_E2E_DeviceFlowAndRefresh() server.TokenGenerator.AccessTokens.Add(expectedAccessToken1); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1); - OAuth2Client client = CreateClient(httpHandler, endpoints); + var trace2 = new NullTrace2(); + OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); OAuth2DeviceCodeResult deviceResult = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None); @@ -376,9 +385,9 @@ public async Task OAuth2Client_E2E_DeviceFlowAndRefresh() RedirectUris = new[] {TestRedirectUri} }; - private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, IOAuth2CodeGenerator generator = null) + private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, ITrace2 trace2, IOAuth2CodeGenerator generator = null) { - return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, TestRedirectUri, TestClientSecret) + return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, trace2, TestRedirectUri, TestClientSecret) { CodeGenerator = generator }; diff --git a/src/shared/Core.Tests/GitConfigurationTests.cs b/src/shared/Core.Tests/GitConfigurationTests.cs index be25b7ade..498651f73 100644 --- a/src/shared/Core.Tests/GitConfigurationTests.cs +++ b/src/shared/Core.Tests/GitConfigurationTests.cs @@ -47,9 +47,9 @@ public void GitProcess_GetConfiguration_ReturnsConfiguration() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath); + var git = new GitProcess(trace, trace2, processManager, gitPath); var config = git.GetConfiguration(); Assert.NotNull(config); } @@ -71,9 +71,9 @@ public void GitConfiguration_Enumerate_CallbackReturnsTrue_InvokesCallbackForEac string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); var actualVisitedEntries = new List<(string name, string value)>(); @@ -110,9 +110,9 @@ public void GitConfiguration_Enumerate_CallbackReturnsFalse_InvokesCallbackForEa string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); var actualVisitedEntries = new List<(string name, string value)>(); @@ -138,13 +138,12 @@ public void GitConfiguration_TryGet_Name_Exists_ReturnsTrueOutString() { string repoPath = CreateRepository(out string workDirPath); ExecGit(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess(); - string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet("user.name", false, out string value); @@ -160,9 +159,10 @@ public void GitConfiguration_TryGet_Name_DoesNotExists_ReturnsFalse() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}"; @@ -180,9 +180,9 @@ public void GitConfiguration_TryGet_IsPath_True_ReturnsCanonicalPath() string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet("example.path", true, out string value); @@ -199,9 +199,9 @@ public void GitConfiguration_TryGet_IsPath_False_ReturnsRawConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet("example.path", false, out string value); @@ -218,9 +218,9 @@ public void GitConfiguration_TryGet_BoolType_ReturnsCanonicalBool() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Bool, @@ -238,9 +238,9 @@ public void GitConfiguration_TryGet_BoolWithoutType_ReturnsRawConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Raw, @@ -258,9 +258,10 @@ public void GitConfiguration_Get_Name_Exists_ReturnsString() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); string value = config.Get("user.name"); @@ -275,9 +276,9 @@ public void GitConfiguration_Get_Name_DoesNotExists_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}"; @@ -291,16 +292,16 @@ public void GitConfiguration_Set_Local_SetsLocalConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);; IGitConfiguration config = git.GetConfiguration(); config.Set(GitConfigurationLevel.Local, "core.foobar", "foo123"); GitResult localResult = ExecGit(repoPath, workDirPath, "config --local core.foobar"); - Assert.Equal("foo123", localResult.StandardOutput.Trim()); + Assert.Equal("foo123", localResult.StandardOutput.Trim()); } [Fact] @@ -310,12 +311,13 @@ public void GitConfiguration_Set_All_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - Assert.Throws(() => config.Set(GitConfigurationLevel.All, "core.foobar", "test123")); + Assert.Throws(() => + config.Set(GitConfigurationLevel.All, "core.foobar", "test123")); } [Fact] @@ -324,15 +326,14 @@ public void GitConfiguration_Unset_Global_UnsetsGlobalConfig() string repoPath = CreateRepository(out string workDirPath); try { - ExecGit(repoPath, workDirPath, "config --global core.foobar alice").AssertSuccess(); ExecGit(repoPath, workDirPath, "config --local core.foobar bob").AssertSuccess(); string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); config.Unset(GitConfigurationLevel.Global, "core.foobar"); @@ -362,9 +363,9 @@ public void GitConfiguration_Unset_Local_UnsetsLocalConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); config.Unset(GitConfigurationLevel.Local, "core.foobar"); @@ -389,9 +390,9 @@ public void GitConfiguration_Unset_All_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); Assert.Throws(() => config.Unset(GitConfigurationLevel.All, "core.foobar")); @@ -407,9 +408,9 @@ public void GitConfiguration_UnsetAll_UnsetsAllConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); config.UnsetAll(GitConfigurationLevel.Local, "core.foobar", "foo*"); @@ -426,12 +427,14 @@ public void GitConfiguration_UnsetAll_All_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, repoPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); - Assert.Throws(() => config.UnsetAll(GitConfigurationLevel.All, "core.foobar", Constants.RegexPatterns.Any)); + Assert.Throws(() => + config.UnsetAll(GitConfigurationLevel.All, "core.foobar", Constants.RegexPatterns.Any)); } } } diff --git a/src/shared/Core.Tests/GitTests.cs b/src/shared/Core.Tests/GitTests.cs index d9e1c9e05..a6905bb8f 100644 --- a/src/shared/Core.Tests/GitTests.cs +++ b/src/shared/Core.Tests/GitTests.cs @@ -13,9 +13,9 @@ public void Git_GetCurrentRepository_NoLocalRepo_ReturnsNull() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, Path.GetTempPath()); + var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); string actual = git.GetCurrentRepository(); @@ -29,9 +29,9 @@ public void Git_GetCurrentRepository_LocalRepo_ReturnsNotNull() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); string actual = git.GetCurrentRepository(); @@ -43,9 +43,9 @@ public void Git_GetRemotes_NoLocalRepo_ReturnsEmpty() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, Path.GetTempPath()); + var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); GitRemote[] remotes = git.GetRemotes().ToArray(); @@ -59,9 +59,9 @@ public void Git_GetRemotes_NoRemotes_ReturnsEmpty() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); @@ -78,10 +78,10 @@ public void Git_GetRemotes_OneRemote_ReturnsRemote() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -100,10 +100,10 @@ public void Git_GetRemotes_OneRemoteFetchAndPull_ReturnsRemote() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -124,10 +124,10 @@ public void Git_GetRemotes_NonHttpRemote_ReturnsRemote(string url) string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -150,10 +150,10 @@ public void Git_GetRemotes_MultipleRemotes_ReturnsAllRemotes() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Equal(3, remotes.Length); @@ -175,10 +175,10 @@ public void Git_GetRemotes_RemoteNoFetchOnlyPull_ReturnsRemote() string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -191,13 +191,14 @@ public void Git_Version_ReturnsVersion() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var env = new TestEnvironment(); + var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, processManager, gitPath, Path.GetTempPath()); + var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); GitVersion version = git.Version; Assert.NotEqual(new GitVersion(), version); + } #region Test Helpers diff --git a/src/shared/Core.UI/Commands/CredentialsCommand.cs b/src/shared/Core.UI/Commands/CredentialsCommand.cs index 02da8472f..001693359 100644 --- a/src/shared/Core.UI/Commands/CredentialsCommand.cs +++ b/src/shared/Core.UI/Commands/CredentialsCommand.cs @@ -63,7 +63,7 @@ private async Task ExecuteAsync(CommandOptions options) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } WriteResult( diff --git a/src/shared/Core.UI/Commands/DeviceCodeCommand.cs b/src/shared/Core.UI/Commands/DeviceCodeCommand.cs index 863e24a1d..4df787caf 100644 --- a/src/shared/Core.UI/Commands/DeviceCodeCommand.cs +++ b/src/shared/Core.UI/Commands/DeviceCodeCommand.cs @@ -41,7 +41,7 @@ private async Task ExecuteAsync(string code, string url, bool noLogo) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } return 0; diff --git a/src/shared/Core.UI/Commands/OAuthCommand.cs b/src/shared/Core.UI/Commands/OAuthCommand.cs index aa48d7eb1..619b1d921 100644 --- a/src/shared/Core.UI/Commands/OAuthCommand.cs +++ b/src/shared/Core.UI/Commands/OAuthCommand.cs @@ -66,7 +66,7 @@ private async Task ExecuteAsync(CommandOptions options) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/shared/Core/Authentication/AuthenticationBase.cs b/src/shared/Core/Authentication/AuthenticationBase.cs index d700f815f..c16c3e0a8 100644 --- a/src/shared/Core/Authentication/AuthenticationBase.cs +++ b/src/shared/Core/Authentication/AuthenticationBase.cs @@ -46,7 +46,10 @@ protected internal virtual async Task> InvokeHelperA var process = ChildProcess.Start(Context.Trace2, procStartInfo, Trace2ProcessClass.UIHelper); if (process is null) { - throw new Exception($"Failed to start helper process: {path} {args}"); + var format = "Failed to start helper process: {0} {1}"; + var message = string.Format(format, path, args); + + throw new Trace2Exception(Context.Trace2, message, format); } // Kill the process upon a cancellation request @@ -69,7 +72,7 @@ protected internal virtual async Task> InvokeHelperA errorMessage = "Unknown"; } - throw new Exception($"helper error ({exitCode}): {errorMessage}"); + throw new Trace2Exception(Context.Trace2, $"helper error ({exitCode}): {errorMessage}"); } return resultDict; @@ -85,8 +88,7 @@ protected void ThrowIfUserInteractionDisabled() Constants.GitConfiguration.Credential.Interactive); Context.Trace.WriteLine($"{envName} / {cfgName} is false/never; user interactivity has been disabled."); - - throw new InvalidOperationException("Cannot prompt because user interactivity has been disabled."); + throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because user interactivity has been disabled."); } } @@ -95,8 +97,7 @@ protected void ThrowIfGuiPromptsDisabled() if (!Context.Settings.IsGuiPromptsEnabled) { Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; GUI prompts have been disabled."); - - throw new InvalidOperationException("Cannot show prompt because GUI prompts have been disabled."); + throw new Trace2InvalidOperationException(Context.Trace2, "Cannot show prompt because GUI prompts have been disabled."); } } @@ -105,8 +106,7 @@ protected void ThrowIfTerminalPromptsDisabled() if (!Context.Settings.IsTerminalPromptsEnabled) { Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled."); - - throw new InvalidOperationException("Cannot prompt because terminal prompts have been disabled."); + throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because terminal prompts have been disabled."); } } diff --git a/src/shared/Core/Authentication/BasicAuthentication.cs b/src/shared/Core/Authentication/BasicAuthentication.cs index 9d485105e..b194893d3 100644 --- a/src/shared/Core/Authentication/BasicAuthentication.cs +++ b/src/shared/Core/Authentication/BasicAuthentication.cs @@ -85,12 +85,12 @@ private async Task GetCredentialsByUiAsync(string command, string a if (!resultDict.TryGetValue("username", out userName)) { - throw new Exception("Missing 'username' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Exception("Missing 'password' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'password' in response"); } return new GitCredential(userName, password); diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 491fffd0f..0278fe887 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -333,9 +333,11 @@ private async Task RegisterTokenCacheAsync(IPublicClientApplication app, ITrace2 } catch (MsalCachePersistenceException ex) { + var message = "Cannot persist Microsoft Authentication data securely!"; Context.Streams.Error.WriteLine("warning: cannot persist Microsoft authentication token cache securely!"); - Context.Trace.WriteLine("Cannot persist Microsoft Authentication data securely!"); + Context.Trace.WriteLine(message); Context.Trace.WriteException(ex); + Context.Trace2.WriteError(message); if (PlatformUtils.IsMacOS()) { @@ -498,10 +500,12 @@ private void EnsureCanUseEmbeddedWebView() #if NETFRAMEWORK if (!Context.SessionManager.IsDesktopSession) { - throw new InvalidOperationException("Embedded web view is not available without a desktop session."); + throw new Trace2InvalidOperationException(Context.Trace2, + "Embedded web view is not available without a desktop session."); } #else - throw new InvalidOperationException("Embedded web view is not available on .NET Core."); + throw new Trace2InvalidOperationException(Context.Trace2, + "Embedded web view is not available on .NET Core."); #endif } @@ -515,17 +519,20 @@ private void EnsureCanUseSystemWebView(IPublicClientApplication app, Uri redirec { if (!Context.SessionManager.IsWebBrowserAvailable) { - throw new InvalidOperationException("System web view is not available without a way to start a browser."); + throw new Trace2InvalidOperationException(Context.Trace2, + "System web view is not available without a way to start a browser."); } if (!app.IsSystemWebViewAvailable) { - throw new InvalidOperationException("System web view is not available on this platform."); + throw new Trace2InvalidOperationException(Context.Trace2, + "System web view is not available on this platform."); } if (!redirectUri.IsLoopback) { - throw new InvalidOperationException("System web view is not available for this service configuration."); + throw new Trace2InvalidOperationException(Context.Trace2, + "System web view is not available for this service configuration."); } } diff --git a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs index 949054282..39b1d8d14 100644 --- a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs @@ -70,16 +70,24 @@ public class OAuth2Client : IOAuth2Client private readonly OAuth2ServerEndpoints _endpoints; private readonly Uri _redirectUri; private readonly string _clientId; + private readonly ITrace2 _trace2; private readonly string _clientSecret; private readonly bool _addAuthHeader; private IOAuth2CodeGenerator _codeGenerator; - public OAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri = null, string clientSecret = null, bool addAuthHeader = true) + public OAuth2Client(HttpClient httpClient, + OAuth2ServerEndpoints endpoints, + string clientId, + ITrace2 trace2, + Uri redirectUri = null, + string clientSecret = null, + bool addAuthHeader = true) { _httpClient = httpClient; _endpoints = endpoints; _clientId = clientId; + _trace2 = trace2; _redirectUri = redirectUri; _clientSecret = clientSecret; _addAuthHeader = addAuthHeader; @@ -155,17 +163,19 @@ public async Task GetAuthorizationCodeAsync(IEnum IDictionary redirectQueryParams = finalUri.GetQueryParameters(); if (!redirectQueryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState)) { - throw new OAuth2Exception($"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response."); + throw new Trace2OAuth2Exception(_trace2, $"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response."); } if (!StringComparer.Ordinal.Equals(state, replyState)) { - throw new OAuth2Exception($"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response. Does not match initial request."); + throw new Trace2OAuth2Exception(_trace2, + $"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response."); } // We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason) if (!redirectQueryParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode)) { - throw new OAuth2Exception($"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response."); + throw new Trace2OAuth2Exception(_trace2, + $"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response."); } return new OAuth2AuthorizationCodeResult(authCode, redirectUri, codeVerifier); @@ -175,7 +185,8 @@ public async Task GetDeviceCodeAsync(IEnumerable { if (_endpoints.DeviceAuthorizationEndpoint is null) { - throw new InvalidOperationException("No device authorization endpoint has been configured for this client."); + throw new Trace2InvalidOperationException(_trace2, + "No device authorization endpoint has been configured for this client."); } string scopesStr = string.Join(" ", scopes); @@ -375,10 +386,13 @@ protected Exception CreateExceptionFromResponse(string json) { if (TryCreateExceptionFromResponse(json, out OAuth2Exception exception)) { + _trace2.WriteError(exception.Message); return exception; } - return new OAuth2Exception($"Unknown OAuth error: {json}"); + var format = "Unknown OAuth error: {0}"; + var message = string.Format(format, json); + return new Trace2OAuth2Exception(_trace2, message, format); } protected static bool TryDeserializeJson(string json, out T obj) diff --git a/src/shared/Core/Authentication/OAuthAuthentication.cs b/src/shared/Core/Authentication/OAuthAuthentication.cs index 8da18faad..aa5cc93ee 100644 --- a/src/shared/Core/Authentication/OAuthAuthentication.cs +++ b/src/shared/Core/Authentication/OAuthAuthentication.cs @@ -82,7 +82,7 @@ public async Task GetAuthenticationModeAsync( if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Exception("Missing 'mode' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -94,7 +94,8 @@ public async Task GetAuthenticationModeAsync( return OAuthAuthenticationModes.DeviceCode; default: - throw new Exception($"Unknown mode value in response '{responseMode}'"); + throw new Trace2Exception(Context.Trace2, + $"Unknown mode value in response '{responseMode}'"); } } else @@ -137,7 +138,8 @@ public async Task GetTokenByBrowserAsync(OAuth2Client client, // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) { - throw new InvalidOperationException("Browser authentication requires a desktop session"); + throw new Trace2InvalidOperationException(Context.Trace2, + "Browser authentication requires a desktop session"); } var browserOptions = new OAuth2WebBrowserOptions(); @@ -185,7 +187,7 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie } catch (OperationCanceledException) { - throw new Exception("User canceled device code authentication"); + throw new Trace2Exception(Context.Trace2, "User canceled device code authentication"); } // Close the dialog diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index 304bdb683..f9cfc0160 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -105,10 +105,11 @@ public CommandContext() Environment = new WindowsEnvironment(FileSystem); SessionManager = new WindowsSessionManager(Environment, FileSystem); ProcessManager = new WindowsProcessManager(Trace2); - Terminal = new WindowsTerminal(Trace); + Terminal = new WindowsTerminal(Trace, Trace2); string gitPath = GetGitPath(Environment, FileSystem, Trace); Git = new GitProcess( Trace, + Trace2, ProcessManager, gitPath, FileSystem.GetCurrentDirectory() @@ -121,10 +122,11 @@ public CommandContext() SessionManager = new MacOSSessionManager(Environment, FileSystem); Environment = new MacOSEnvironment(FileSystem); ProcessManager = new ProcessManager(Trace2); - Terminal = new MacOSTerminal(Trace); + Terminal = new MacOSTerminal(Trace, Trace2); string gitPath = GetGitPath(Environment, FileSystem, Trace); Git = new GitProcess( Trace, + Trace2, ProcessManager, gitPath, FileSystem.GetCurrentDirectory() @@ -137,10 +139,11 @@ public CommandContext() SessionManager = new LinuxSessionManager(Environment, FileSystem); Environment = new PosixEnvironment(FileSystem); ProcessManager = new ProcessManager(Trace2); - Terminal = new LinuxTerminal(Trace); + Terminal = new LinuxTerminal(Trace, Trace2); string gitPath = GetGitPath(Environment, FileSystem, Trace); Git = new GitProcess( Trace, + Trace2, ProcessManager, gitPath, FileSystem.GetCurrentDirectory() diff --git a/src/shared/Core/Commands/GitCommandBase.cs b/src/shared/Core/Commands/GitCommandBase.cs index 2bb6d0fe5..4db6974e4 100644 --- a/src/shared/Core/Commands/GitCommandBase.cs +++ b/src/shared/Core/Commands/GitCommandBase.cs @@ -58,22 +58,24 @@ protected virtual void EnsureMinimumInputArguments(InputArguments input) { if (input.Protocol is null) { - throw new InvalidOperationException("Missing 'protocol' input argument"); + throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'protocol' input argument"); } if (string.IsNullOrWhiteSpace(input.Protocol)) { - throw new InvalidOperationException("Invalid 'protocol' input argument (cannot be empty)"); + throw new Trace2InvalidOperationException(Context.Trace2, + "Invalid 'protocol' input argument (cannot be empty)"); } if (input.Host is null) { - throw new InvalidOperationException("Missing 'host' input argument"); + throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'host' input argument"); } if (string.IsNullOrWhiteSpace(input.Host)) { - throw new InvalidOperationException("Invalid 'host' input argument (cannot be empty)"); + throw new Trace2InvalidOperationException(Context.Trace2, + "Invalid 'host' input argument (cannot be empty)"); } } diff --git a/src/shared/Core/Commands/StoreCommand.cs b/src/shared/Core/Commands/StoreCommand.cs index 7a4f078d5..8085e87ed 100644 --- a/src/shared/Core/Commands/StoreCommand.cs +++ b/src/shared/Core/Commands/StoreCommand.cs @@ -23,12 +23,12 @@ protected override void EnsureMinimumInputArguments(InputArguments input) // An empty string username/password are valid inputs, so only check for `null` (not provided) if (input.UserName is null) { - throw new InvalidOperationException("Missing 'username' input argument"); + throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'username' input argument"); } if (input.Password is null) { - throw new InvalidOperationException("Missing 'password' input argument"); + throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'password' input argument"); } } } diff --git a/src/shared/Core/CredentialStore.cs b/src/shared/Core/CredentialStore.cs index 730447c83..8245b41c9 100644 --- a/src/shared/Core/CredentialStore.cs +++ b/src/shared/Core/CredentialStore.cs @@ -79,7 +79,7 @@ private void EnsureBackingStore() case StoreNames.Gpg: ValidateGpgPass(out string gpgStoreRoot, out string gpgExec); - IGpg gpg = new Gpg(gpgExec, _context.SessionManager, _context.ProcessManager); + IGpg gpg = new Gpg(gpgExec, _context.SessionManager, _context.ProcessManager, _context.Trace2); _backingStore = new GpgPassCredentialStore(_context.FileSystem, gpg, gpgStoreRoot, ns); break; @@ -98,6 +98,7 @@ private void EnsureBackingStore() sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName) ? "No credential store has been selected." : $"Unknown credential store '{credStoreName}'."); + _context.Trace2.WriteError(sb.ToString()); sb.AppendFormat( "{3}Set the {0} environment variable or the {1}.{2} Git configuration setting to one of the following options:{3}{3}", Constants.EnvironmentVariables.GcmCredentialStore, @@ -166,18 +167,18 @@ private void ValidateWindowsCredentialManager() { if (!PlatformUtils.IsWindows()) { - throw new Exception( - $"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = $"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } if (!WindowsCredentialManager.CanPersist()) { - throw new Exception( - $"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store." + - Environment.NewLine + + var message = $"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } @@ -187,9 +188,9 @@ private void ValidateDpapi(out string storeRoot) { if (!PlatformUtils.IsWindows()) { - throw new Exception( - $"Can only use the '{StoreNames.Dpapi}' credential store on Windows." + - Environment.NewLine + + var message = $"Can only use the '{StoreNames.Dpapi}' credential store on Windows."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } @@ -210,10 +211,10 @@ private void ValidateMacOSKeychain() { if (!PlatformUtils.IsMacOS()) { - throw new Exception( - $"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = $"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } } @@ -222,19 +223,19 @@ private void ValidateSecretService() { if (!PlatformUtils.IsLinux()) { - throw new Exception( - $"Can only use the '{StoreNames.SecretService}' credential store on Linux." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = $"Can only use the '{StoreNames.SecretService}' credential store on Linux."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } if (!_context.SessionManager.IsDesktopSession) { - throw new Exception( - $"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = $"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } } @@ -243,10 +244,10 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) { if (!PlatformUtils.IsPosix()) { - throw new Exception( - $"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = $"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } @@ -258,10 +259,10 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) !_context.Environment.Variables.ContainsKey("GPG_TTY") && !_context.Environment.Variables.ContainsKey("SSH_TTY")) { - throw new Exception( - "GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = "GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } @@ -279,10 +280,12 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) string gpgIdFile = Path.Combine(storeRoot, ".gpg-id"); if (!_context.FileSystem.FileExists(gpgIdFile)) { - throw new Exception( - $"Password store has not been initialized at '{storeRoot}'; run `pass init ` to initialize the store." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var format = + "Password store has not been initialized at '{0}'; run `pass init ` to initialize the store."; + var message = string.Format(format, storeRoot); + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } } @@ -291,10 +294,10 @@ private void ValidateCredentialCache(out string options) { if (PlatformUtils.IsWindows()) { - throw new Exception( - $"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows." + - Environment.NewLine + - $"See {Constants.HelpUrls.GcmCredentialStores} for more information." + var message = $"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows."; + _context.Trace2.WriteError(message); + throw new Exception(message + Environment.NewLine + + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); } @@ -337,7 +340,9 @@ private string GetGpgPath() return gpgPath; } - throw new Exception($"GPG executable does not exist with path '{gpgPath}'"); + var format = "GPG executable does not exist with path '{0}'"; + var message = string.Format(format, gpgPath); + throw new Trace2Exception(_context.Trace2, message, format); } // If no explicit GPG path is specified, mimic the way `pass` diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index 70f6b44bc..7514658f3 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -76,7 +76,7 @@ public override async Task GenerateCredentialAsync(InputArguments i Context.Trace.WriteLine($"\tUseAuthHeader = {oauthConfig.UseAuthHeader}"); Context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}"); - return await GetOAuthAccessToken(uri, input.UserName, oauthConfig); + return await GetOAuthAccessToken(uri, input.UserName, oauthConfig, Context.Trace2); } // Try detecting WIA for this remote, if permitted else if (IsWindowsAuthAllowed) @@ -114,7 +114,7 @@ public override async Task GenerateCredentialAsync(InputArguments i return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName); } - private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config) + private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config, ITrace2 trace2) { // TODO: Determined user info from a webcall? ID token? Need OIDC support string oauthUser = userName ?? config.DefaultUserName; @@ -123,6 +123,7 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa HttpClient, config.Endpoints, config.ClientId, + trace2, config.RedirectUri, config.ClientSecret, config.UseAuthHeader); @@ -193,7 +194,7 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa break; default: - throw new Exception("No authentication mode selected!"); + throw new Trace2Exception(Context.Trace2, "No authentication mode selected!"); } // Store the refresh token if we have one diff --git a/src/shared/Core/Git.cs b/src/shared/Core/Git.cs index 7c96c829e..0740093b7 100644 --- a/src/shared/Core/Git.cs +++ b/src/shared/Core/Git.cs @@ -65,17 +65,20 @@ public GitRemote(string name, string fetchUrl, string pushUrl) public class GitProcess : IGit { private readonly ITrace _trace; + private readonly ITrace2 _trace2; private readonly IProcessManager _processManager; private readonly string _gitPath; private readonly string _workingDirectory; - public GitProcess(ITrace trace, IProcessManager processManager, string gitPath, string workingDirectory = null) + public GitProcess(ITrace trace, ITrace2 trace2, IProcessManager processManager, string gitPath, string workingDirectory = null) { EnsureArgument.NotNull(trace, nameof(trace)); + EnsureArgument.NotNull(trace2, nameof(trace2)); EnsureArgument.NotNull(processManager, nameof(processManager)); EnsureArgument.NotNullOrWhiteSpace(gitPath, nameof(gitPath)); _trace = trace; + _trace2 = trace2; _processManager = processManager; _gitPath = gitPath; _workingDirectory = workingDirectory; @@ -131,8 +134,9 @@ public string GetCurrentRepository() case 128: // Not inside a Git repository return null; default: - _trace.WriteLine($"Failed to get current Git repository (exit={git.ExitCode})"); - throw CreateGitException(git, "Failed to get current Git repository"); + var message = "Failed to get current Git repository"; + _trace.WriteLine($"{message} (exit={git.ExitCode})"); + throw CreateGitException(git, message, _trace2); } } } @@ -155,8 +159,9 @@ public IEnumerable GetRemotes() case 128 when stderr.Contains("not a git repository"): // Not inside a Git repository yield break; default: - _trace.WriteLine($"Failed to enumerate Git remotes (exit={git.ExitCode})"); - throw CreateGitException(git, "Failed to enumerate Git remotes"); + var message = "Failed to enumerate Git remotes"; + _trace.WriteLine($"{message} (exit={git.ExitCode})"); + throw CreateGitException(git, message, _trace2); } string[] lines = data.Split('\n'); @@ -207,7 +212,9 @@ public async Task> InvokeHelperAsync(string args, ID var process = _processManager.CreateProcess(procStartInfo); if (process is null) { - throw new Exception($"Failed to start Git helper '{args}'"); + var format = "Failed to start Git helper '{0}'"; + var message = string.Format(format, args); + throw new Trace2Exception(_trace2, message, format); } if (!(standardInput is null)) @@ -236,9 +243,13 @@ public async Task> InvokeHelperAsync(string args, ID return resultDict; } - public static GitException CreateGitException(ChildProcess git, string message) + public static GitException CreateGitException(ChildProcess git, string message, ITrace2 trace2 = null) { - string gitMessage = git.StandardError.ReadToEnd(); + var gitMessage = git.StandardError.ReadToEnd(); + + if (trace2 != null) + throw new Trace2GitException(trace2, message, git.ExitCode, gitMessage); + throw new GitException(message, gitMessage, git.ExitCode); } } diff --git a/src/shared/Core/Gpg.cs b/src/shared/Core/Gpg.cs index 84fa3366c..f1d85caa7 100644 --- a/src/shared/Core/Gpg.cs +++ b/src/shared/Core/Gpg.cs @@ -15,15 +15,18 @@ public class Gpg : IGpg private readonly string _gpgPath; private readonly ISessionManager _sessionManager; private readonly IProcessManager _processManager; + private readonly ITrace2 _trace2; - public Gpg(string gpgPath, ISessionManager sessionManager, IProcessManager processManager) + public Gpg(string gpgPath, ISessionManager sessionManager, IProcessManager processManager, ITrace2 trace2) { EnsureArgument.NotNullOrWhiteSpace(gpgPath, nameof(gpgPath)); EnsureArgument.NotNull(sessionManager, nameof(sessionManager)); + EnsureArgument.NotNull(trace2, nameof(trace2)); _gpgPath = gpgPath; _sessionManager = sessionManager; _processManager = processManager; + _trace2 = trace2; } public string DecryptFile(string path) @@ -43,7 +46,7 @@ public string DecryptFile(string path) { if (gpg is null) { - throw new Exception("Failed to start gpg."); + throw new Trace2Exception(_trace2, "Failed to start gpg."); } gpg.WaitForExit(); @@ -52,7 +55,9 @@ public string DecryptFile(string path) { string stdout = gpg.StandardOutput.ReadToEnd(); string stderr = gpg.StandardError.ReadToEnd(); - throw new Exception($"Failed to decrypt file '{path}' with gpg. exit={gpg.ExitCode}, out={stdout}, err={stderr}"); + var format = "Failed to decrypt file '{0}' with gpg. exit={1}, out={2}, err={3}"; + var message = string.Format(format, path, gpg.ExitCode, stdout, stderr); + throw new Trace2Exception(_trace2, message, format); } return gpg.StandardOutput.ReadToEnd(); @@ -75,7 +80,7 @@ public void EncryptFile(string path, string gpgId, string contents) { if (gpg is null) { - throw new Exception("Failed to start gpg."); + throw new Trace2Exception(_trace2, "Failed to start gpg."); } gpg.StandardInput.Write(contents); @@ -87,7 +92,9 @@ public void EncryptFile(string path, string gpgId, string contents) { string stdout = gpg.StandardOutput.ReadToEnd(); string stderr = gpg.StandardError.ReadToEnd(); - throw new Exception($"Failed to encrypt file '{path}' with gpg. exit={gpg.ExitCode}, out={stdout}, err={stderr}"); + var format = "Failed to encrypt file '{0}' with gpg. exit={1}, out={2}, err={3}"; + var message = string.Format(format, path, gpg.ExitCode, stdout, stderr); + throw new Trace2Exception(_trace2, message, format); } } } diff --git a/src/shared/Core/HostProviderRegistry.cs b/src/shared/Core/HostProviderRegistry.cs index 237c32e54..7907ff7dc 100644 --- a/src/shared/Core/HostProviderRegistry.cs +++ b/src/shared/Core/HostProviderRegistry.cs @@ -151,7 +151,7 @@ public async Task GetProviderAsync(InputArguments input) var uri = input.GetRemoteUri(); if (uri is null) { - throw new Exception("Unable to detect host provider without a remote URL"); + throw new Trace2Exception(_context.Trace2, "Unable to detect host provider without a remote URL"); } // We can only probe HTTP(S) URLs - for SMTP, IMAP, etc we cannot do network probing @@ -240,8 +240,10 @@ await MatchProviderAsync(HostProviderPriority.Low, canProbeUri) ?? } catch (Exception ex) { - _context.Trace.WriteLine("Failed to set host provider!"); + var message = "Failed to set host provider!"; + _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); + _context.Trace2.WriteError(message); _context.Streams.Error.WriteLine("warning: failed to remember result of host provider detection!"); _context.Streams.Error.WriteLine($"warning: try setting this manually: `git config --global {keyName} {match.Id}`"); diff --git a/src/shared/Core/HttpClientFactory.cs b/src/shared/Core/HttpClientFactory.cs index a3ea8ab6f..b34aecc1a 100644 --- a/src/shared/Core/HttpClientFactory.cs +++ b/src/shared/Core/HttpClientFactory.cs @@ -115,7 +115,9 @@ public HttpClient CreateClient() // Throw exception if cert bundle file not found if (!_fileSystem.FileExists(certBundlePath)) { - throw new FileNotFoundException($"Custom certificate bundle not found at path: {certBundlePath}", certBundlePath); + var format = "Custom certificate bundle not found at path: {0}"; + var message = string.Format(format, certBundlePath); + throw new Trace2FileNotFoundException(_trace2, message, format, certBundlePath); } Func validationCallback = (cert, chain, errors) => @@ -280,8 +282,11 @@ public bool TryCreateProxy(out IWebProxy proxy) } catch (Exception ex) { - _trace.WriteLine("Failed to convert proxy bypass hosts to regular expressions; ignoring bypass list"); + var message = + "Failed to convert proxy bypass hosts to regular expressions; ignoring bypass list"; + _trace.WriteLine(message); _trace.WriteException(ex); + _trace2.WriteError(message); dict["bypass"] = "<< failed to convert >>"; } } diff --git a/src/shared/Core/Interop/Linux/LinuxTerminal.cs b/src/shared/Core/Interop/Linux/LinuxTerminal.cs index 1ad8b7d24..f7ea6f89a 100644 --- a/src/shared/Core/Interop/Linux/LinuxTerminal.cs +++ b/src/shared/Core/Interop/Linux/LinuxTerminal.cs @@ -7,12 +7,12 @@ namespace GitCredentialManager.Interop.Linux { public class LinuxTerminal : PosixTerminal { - public LinuxTerminal(ITrace trace) - : base(trace) { } + public LinuxTerminal(ITrace trace, ITrace2 trace2) + : base(trace, trace2) { } protected override IDisposable CreateTtyContext(int fd, bool echo) { - return new TtyContext(Trace, fd, echo); + return new TtyContext(Trace, Trace2, fd, echo); } private class TtyContext : IDisposable @@ -23,7 +23,7 @@ private class TtyContext : IDisposable private termios_Linux _originalTerm; private bool _isDisposed; - public TtyContext(ITrace trace, int fd, bool echo) + public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo) { EnsureArgument.NotNull(trace, nameof(trace)); EnsureArgument.PositiveOrZero(fd, nameof(fd)); @@ -36,7 +36,7 @@ public TtyContext(ITrace trace, int fd, bool echo) // Capture current terminal settings so we can restore them later if ((error = Termios_Linux.tcgetattr(_fd, out termios_Linux t)) != 0) { - throw new InteropException("Failed to get initial terminal settings", error); + throw new Trace2InteropException(trace2, "Failed to get initial terminal settings", error); } _originalTerm = t; @@ -50,7 +50,7 @@ public TtyContext(ITrace trace, int fd, bool echo) if ((error = Termios_Linux.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0) { - throw new InteropException("Failed to set terminal settings", error); + throw new Trace2InteropException(trace2, "Failed to set terminal settings", error); } } diff --git a/src/shared/Core/Interop/MacOS/MacOSTerminal.cs b/src/shared/Core/Interop/MacOS/MacOSTerminal.cs index e56a3fa54..a4c9d2120 100644 --- a/src/shared/Core/Interop/MacOS/MacOSTerminal.cs +++ b/src/shared/Core/Interop/MacOS/MacOSTerminal.cs @@ -7,12 +7,12 @@ namespace GitCredentialManager.Interop.MacOS { public class MacOSTerminal : PosixTerminal { - public MacOSTerminal(ITrace trace) - : base(trace) { } + public MacOSTerminal(ITrace trace, ITrace2 trace2) + : base(trace, trace2) { } protected override IDisposable CreateTtyContext(int fd, bool echo) { - return new TtyContext(Trace, fd, echo); + return new TtyContext(Trace, Trace2, fd, echo); } private class TtyContext : IDisposable @@ -23,7 +23,7 @@ private class TtyContext : IDisposable private termios_MacOS _originalTerm; private bool _isDisposed; - public TtyContext(ITrace trace, int fd, bool echo) + public TtyContext(ITrace trace, ITrace2 trace2, int fd, bool echo) { EnsureArgument.NotNull(trace, nameof(trace)); EnsureArgument.PositiveOrZero(fd, nameof(fd)); @@ -36,7 +36,7 @@ public TtyContext(ITrace trace, int fd, bool echo) // Capture current terminal settings so we can restore them later if ((error = Termios_MacOS.tcgetattr(_fd, out termios_MacOS t)) != 0) { - throw new InteropException("Failed to get initial terminal settings", error); + throw new Trace2InteropException(trace2, "Failed to get initial terminal settings", error); } _originalTerm = t; @@ -50,7 +50,7 @@ public TtyContext(ITrace trace, int fd, bool echo) if ((error = Termios_MacOS.tcsetattr(_fd, SetActionFlags.TCSAFLUSH, ref t)) != 0) { - throw new InteropException("Failed to set terminal settings", error); + throw new Trace2InteropException(trace2, "Failed to set terminal settings", error); } } diff --git a/src/shared/Core/Interop/Posix/PosixTerminal.cs b/src/shared/Core/Interop/Posix/PosixTerminal.cs index d110b1408..b37d6d6cf 100644 --- a/src/shared/Core/Interop/Posix/PosixTerminal.cs +++ b/src/shared/Core/Interop/Posix/PosixTerminal.cs @@ -13,8 +13,9 @@ public abstract class PosixTerminal : ITerminal private const byte DeleteChar = 127; protected readonly ITrace Trace; + protected readonly ITrace2 Trace2; - public PosixTerminal(ITrace trace) + public PosixTerminal(ITrace trace, ITrace2 trace2) { PlatformUtils.EnsurePosix(); EnsureArgument.NotNull(trace, nameof(trace)); diff --git a/src/shared/Core/Interop/Windows/Native/Win32Error.cs b/src/shared/Core/Interop/Windows/Native/Win32Error.cs index f5c436339..f6a170bda 100644 --- a/src/shared/Core/Interop/Windows/Native/Win32Error.cs +++ b/src/shared/Core/Interop/Windows/Native/Win32Error.cs @@ -97,6 +97,18 @@ public static int GetLastError(bool success) return Marshal.GetLastWin32Error(); } + /// + /// Throw an if is not true. + /// + /// The application's TRACE2 tracer. + /// Windows API return code. + /// Default error message. + /// Throw if is not true. + public static void ThrowIfError(ITrace2 trace2, bool succeeded, string defaultErrorMessage = "Unknown error.") + { + ThrowIfError(GetLastError(succeeded), defaultErrorMessage, trace2); + } + /// /// Throw an if is not true. /// @@ -113,8 +125,9 @@ public static void ThrowIfError(bool succeeded, string defaultErrorMessage = "Un /// /// Windows API error code. /// Default error message. + /// The application's TRACE2 tracer. /// Throw if is not . - public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.") + public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.", ITrace2 trace2 = null) { switch (error) { @@ -123,6 +136,8 @@ public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown default: // The Win32Exception constructor will automatically get the human-readable // message for the error code. + if (trace2 != null) + throw new Trace2InteropException(trace2, defaultErrorMessage, new Win32Exception(error)); throw new InteropException(defaultErrorMessage, new Win32Exception(error)); } } diff --git a/src/shared/Core/Interop/Windows/WindowsTerminal.cs b/src/shared/Core/Interop/Windows/WindowsTerminal.cs index 140f69ad3..b8f5f3475 100644 --- a/src/shared/Core/Interop/Windows/WindowsTerminal.cs +++ b/src/shared/Core/Interop/Windows/WindowsTerminal.cs @@ -17,12 +17,14 @@ public class WindowsTerminal : ITerminal private const string ConsoleOutName = "CONOUT$"; private readonly ITrace _trace; + private readonly ITrace2 _trace2; - public WindowsTerminal(ITrace trace) + public WindowsTerminal(ITrace trace, ITrace2 trace2) { PlatformUtils.EnsureWindows(); _trace = trace; + _trace2 = trace2; } public void WriteLine(string format, params object[] args) @@ -58,7 +60,7 @@ public void WriteLine(string format, params object[] args) numberOfCharsWritten: out uint written, reserved: IntPtr.Zero)) { - Win32Error.ThrowIfError(Marshal.GetLastWin32Error()); + Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), trace2: _trace2); } } } @@ -116,13 +118,13 @@ private string Prompt(string prompt, bool echo) numberOfCharsWritten: out written, reserved: IntPtr.Zero)) { - Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to write prompt text"); + Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to write prompt text", _trace2); } sb.Clear(); // Read input from the user - using (new TtyContext(_trace, stdin, echo)) + using (new TtyContext(_trace, _trace2, stdin, echo)) { if (!Kernel32.ReadConsole(buffer: sb, consoleInputHandle: stdin, @@ -130,7 +132,8 @@ private string Prompt(string prompt, bool echo) numberOfCharsRead: out read, reserved: IntPtr.Zero)) { - Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Unable to read prompt input from standard input"); + Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), + "Unable to read prompt input from standard input", _trace2); } // Record input from the user into local storage, stripping any EOL chars @@ -150,7 +153,8 @@ private string Prompt(string prompt, bool echo) numberOfCharsWritten: out written, reserved: IntPtr.Zero)) { - Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to write final newline in secret prompting"); + Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), + "Failed to write final newline in secret prompting", _trace2); } } @@ -161,23 +165,25 @@ private string Prompt(string prompt, bool echo) private class TtyContext : IDisposable { private readonly ITrace _trace; + private readonly ITrace2 _trace2; private readonly SafeFileHandle _stream; private ConsoleMode _originalMode; private bool _isDisposed; - public TtyContext(ITrace trace, SafeFileHandle stream, bool echo) + public TtyContext(ITrace trace, ITrace2 trace2, SafeFileHandle stream, bool echo) { EnsureArgument.NotNull(stream, nameof(stream)); _trace = trace; + _trace2 = trace2; _stream = stream; // Capture current console mode so we can restore it later ConsoleMode consoleMode; if (!Kernel32.GetConsoleMode(consoleMode: out consoleMode, consoleHandle: stream)) { - Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to get initial console mode"); + Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to get initial console mode", trace2); } _originalMode = consoleMode; @@ -189,7 +195,8 @@ public TtyContext(ITrace trace, SafeFileHandle stream, bool echo) ConsoleMode newConsoleMode = consoleMode ^ ConsoleMode.EchoInput; if (!Kernel32.SetConsoleMode(consoleMode: newConsoleMode, consoleHandle: _stream)) { - Win32Error.ThrowIfError(Marshal.GetLastWin32Error(), "Failed to set console mode"); + Win32Error.ThrowIfError( + Marshal.GetLastWin32Error(), "Failed to set console mode", trace2); } } } diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs index 813ebbf3b..1f5236097 100644 --- a/src/shared/Core/Trace2.cs +++ b/src/shared/Core/Trace2.cs @@ -20,7 +20,8 @@ public enum Trace2Event Start = 1, Exit = 2, ChildStart = 3, - ChildExit = 4 + ChildExit = 4, + Error = 5, } /// @@ -120,6 +121,21 @@ void WriteChildExit( string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0); + + /// + /// Writes an error as a message to the trace writer. + /// + /// The error message to write. + /// The error format string. + /// Path of the file this method is called from. + /// Line number of file this method is called from. + void WriteError( + string errorMessage, + string parameterizedMessage = null, + [System.Runtime.CompilerServices.CallerFilePath] + string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] + int lineNumber = 0); } public class Trace2 : DisposableObject, ITrace2 @@ -214,7 +230,7 @@ public void WriteChildStart(DateTimeOffset startTime, Event = Trace2Event.ChildStart, Sid = _sid, Time = startTime, - File = Path.GetFileName(filePath).ToLower(), + File = Path.GetFileName(filePath), Line = lineNumber, Id = ++_childProcCounter, Classification = processClass, @@ -245,7 +261,7 @@ public void WriteChildExit( Event = Trace2Event.ChildExit, Sid = _sid, Time = DateTimeOffset.UtcNow, - File = Path.GetFileName(filePath).ToLower(), + File = Path.GetFileName(filePath), Line = lineNumber, Id = _childProcCounter, Pid = pid, @@ -255,6 +271,34 @@ public void WriteChildExit( }); } + public void WriteError( + string errorMessage, + string parameterizedMessage = null, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + { + // It is possible for an error to be thrown before TRACE2 can be initialized. + // Since certain dependencies are not available until initialization, + // we must immediately return if this method is invoked prior to + // initialization. + if (!_initialized) + { + return; + } + + WriteMessage(new ErrorMessage() + { + Event = Trace2Event.Error, + Sid = _sid, + Time = DateTimeOffset.UtcNow, + File = Path.GetFileName(filePath), + Line = lineNumber, + Message = errorMessage, + ParameterizedMessage = parameterizedMessage ?? errorMessage, + Depth = ProcessManager.Depth + }); + } + protected override void ReleaseManagedResources() { lock (_writersLock) @@ -340,7 +384,7 @@ private void WriteVersion( Event = Trace2Event.Version, Sid = _sid, Time = DateTimeOffset.UtcNow, - File = Path.GetFileName(filePath).ToLower(), + File = Path.GetFileName(filePath), Line = lineNumber, Evt = eventFormatVersion, Exe = gcmVersion @@ -369,7 +413,7 @@ private void WriteStart( Event = Trace2Event.Start, Sid = _sid, Time = DateTimeOffset.UtcNow, - File = Path.GetFileName(filePath).ToLower(), + File = Path.GetFileName(filePath), Line = lineNumber, Argv = argv, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds @@ -385,7 +429,7 @@ private void WriteExit(int code, string filePath = "", int lineNumber = 0) Event = Trace2Event.Exit, Sid = _sid, Time = DateTimeOffset.Now, - File = Path.GetFileName(filePath).ToLower(), + File = Path.GetFileName(filePath), Line = lineNumber, Code = code, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds @@ -441,27 +485,27 @@ public abstract class Trace2Message protected const string EmptyPerformanceSpan = "| | | | "; protected const string TimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffff'Z'"; - [JsonProperty("event", Order = 1)] + [JsonProperty("event")] public Trace2Event Event { get; set; } - [JsonProperty("sid", Order = 2)] + [JsonProperty("sid")] public string Sid { get; set; } // TODO: Remove this default value when TRACE2 regions are introduced. - [JsonProperty("thread", Order = 3)] + [JsonProperty("thread")] public string Thread { get; set; } = "main"; - [JsonProperty("time", Order = 4)] + [JsonProperty("time")] public DateTimeOffset Time { get; set; } - [JsonProperty("file", Order = 5)] + [JsonProperty("file")] public string File { get; set; } - [JsonProperty("line", Order = 6)] + [JsonProperty("line")] public int Line { get; set; } - [JsonProperty("depth", Order = 7)] + [JsonProperty("depth")] public int Depth { get; set; } public abstract string ToJson(); @@ -539,7 +583,7 @@ internal static string BuildTimeSpan(double time) private string GetSource() { // Source column format is file:line - string source = $"{File.ToLower()}:{Line}"; + string source = $"{File}:{Line}"; if (source.Length > SourceColumnMaxWidth) { return TraceUtils.FormatSource(source, SourceColumnMaxWidth); @@ -573,10 +617,10 @@ private static void AdjustPadding(PerformanceFormatSpan span, string data) public class VersionMessage : Trace2Message { - [JsonProperty("evt", Order = 8)] + [JsonProperty("evt")] public string Evt { get; set; } - [JsonProperty("exe", Order = 9)] + [JsonProperty("exe")] public string Exe { get; set; } public override string ToJson() @@ -612,10 +656,10 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) public class StartMessage : Trace2Message { - [JsonProperty("t_abs", Order = 8)] + [JsonProperty("t_abs")] public double ElapsedTime { get; set; } - [JsonProperty("argv", Order = 9)] + [JsonProperty("argv")] public List Argv { get; set; } public override string ToJson() @@ -651,10 +695,10 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) public class ExitMessage : Trace2Message { - [JsonProperty("t_abs", Order = 8)] + [JsonProperty("t_abs")] public double ElapsedTime { get; set; } - [JsonProperty("code", Order = 9)] + [JsonProperty("code")] public int Code { get; set; } public override string ToJson() @@ -690,16 +734,16 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) public class ChildStartMessage : Trace2Message { - [JsonProperty("child_id", Order = 7)] + [JsonProperty("child_id")] public long Id { get; set; } - [JsonProperty("child_class", Order = 8)] + [JsonProperty("child_class")] public Trace2ProcessClass Classification { get; set; } - [JsonProperty("use_shell", Order = 9)] + [JsonProperty("use_shell")] public bool UseShell { get; set; } - [JsonProperty("argv", Order = 10)] + [JsonProperty("argv")] public IList Argv { get; set; } public override string ToJson() @@ -745,16 +789,16 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) public class ChildExitMessage : Trace2Message { - [JsonProperty("child_id", Order = 7)] + [JsonProperty("child_id")] public long Id { get; set; } - [JsonProperty("pid", Order = 8)] + [JsonProperty("pid")] public int Pid { get; set; } - [JsonProperty("code", Order = 9)] + [JsonProperty("code")] public int Code { get; set; } - [JsonProperty("t_rel", Order = 10)] + [JsonProperty("t_rel")] public double ElapsedTime { get; set; } public override string ToJson() @@ -796,3 +840,42 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) return sb.ToString(); } } + +public class ErrorMessage : Trace2Message +{ + [JsonProperty("msg")] + public string Message { get; set; } + + [JsonProperty("format")] + public string ParameterizedMessage { get; set; } + + public override string ToJson() + { + return JsonConvert.SerializeObject(this, + new StringEnumConverter(typeof(SnakeCaseNamingStrategy)), + new IsoDateTimeConverter() + { + DateTimeFormat = TimeFormat + }); + } + + public override string ToNormalString() + { + return BuildNormalString(); + } + + public override string ToPerformanceString() + { + return BuildPerformanceString(); + } + + protected override string BuildPerformanceSpan() + { + return EmptyPerformanceSpan; + } + + protected override string GetEventMessage(Trace2FormatTarget formatTarget) + { + return Message; + } +} diff --git a/src/shared/Core/Trace2Exception.cs b/src/shared/Core/Trace2Exception.cs new file mode 100644 index 000000000..292ec1516 --- /dev/null +++ b/src/shared/Core/Trace2Exception.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel; +using System.IO; +using GitCredentialManager.Authentication.OAuth; +using GitCredentialManager.Interop; + +namespace GitCredentialManager; + +public class Trace2Exception : Exception +{ + public Trace2Exception(ITrace2 trace2, string message) : base(message) + { + trace2.WriteError(message); + } + + public Trace2Exception(ITrace2 trace2, string message, string messageFormat) : base(message) + { + trace2.WriteError(message, messageFormat); + } +} + +public class Trace2InvalidOperationException : InvalidOperationException +{ + public Trace2InvalidOperationException(ITrace2 trace2, string message) : base(message) + { + trace2.WriteError(message); + } +} + +public class Trace2OAuth2Exception : OAuth2Exception +{ + public Trace2OAuth2Exception(ITrace2 trace2, string message) : base(message) + { + trace2.WriteError(message); + } + + public Trace2OAuth2Exception(ITrace2 trace2, string message, string messageFormat) : base(message) + { + trace2.WriteError(message, messageFormat); + } +} + +public class Trace2InteropException : InteropException +{ + public Trace2InteropException(ITrace2 trace2, string message, int errorCode) : base(message, errorCode) + { + trace2.WriteError($"message: {message} error code: {errorCode}"); + } + + public Trace2InteropException(ITrace2 trace2, string message, Win32Exception ex) : base(message, ex) + { + trace2.WriteError(message); + } +} + +public class Trace2GitException : GitException +{ + public Trace2GitException(ITrace2 trace2, string message, int errorCode, string gitMessage) : + base(message, gitMessage, errorCode) + { + var format = $"message: '{message}' error code: '{errorCode}' git message: '{{0}}'"; + var traceMessage = string.Format(format, gitMessage); + + trace2.WriteError(traceMessage, format); + } +} + +public class Trace2FileNotFoundException : FileNotFoundException +{ + public Trace2FileNotFoundException(ITrace2 trace2, string message, string messageFormat, string fileName) : + base(message, fileName) + { + trace2.WriteError(message, messageFormat); + } +} diff --git a/src/shared/GitHub.Tests/GitHubAuthenticationTests.cs b/src/shared/GitHub.Tests/GitHubAuthenticationTests.cs index 7a8d18dad..ad3eb724d 100644 --- a/src/shared/GitHub.Tests/GitHubAuthenticationTests.cs +++ b/src/shared/GitHub.Tests/GitHubAuthenticationTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using GitCredentialManager; using GitCredentialManager.Tests.Objects; using Moq; using Moq.Protected; @@ -43,7 +44,7 @@ public async Task GitHubAuthentication_GetAuthenticationAsync_TerminalPromptsDis var context = new TestCommandContext(); context.Settings.IsTerminalPromptsEnabled = false; var auth = new GitHubAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because terminal prompts have been disabled.", exception.Message); @@ -68,7 +69,7 @@ public async Task GitHubAuthentication_GetAuthenticationAsync_AuthenticationMode var context = new TestCommandContext(); context.Settings.IsInteractionAllowed = false; var auth = new GitHubAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(new Uri("https://github.com"), null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because user interactivity has been disabled.", exception.Message); diff --git a/src/shared/GitHub.Tests/GitHubHostProviderTests.cs b/src/shared/GitHub.Tests/GitHubHostProviderTests.cs index a25c95ffc..8a861b513 100644 --- a/src/shared/GitHub.Tests/GitHubHostProviderTests.cs +++ b/src/shared/GitHub.Tests/GitHubHostProviderTests.cs @@ -166,7 +166,7 @@ public async Task GitHubHostProvider_GenerateCredentialAsync_UnencryptedHttp_Thr var provider = new GitHubHostProvider(context, ghApi, ghAuth); - await Assert.ThrowsAsync(() => provider.GenerateCredentialAsync(input)); + await Assert.ThrowsAsync(() => provider.GenerateCredentialAsync(input)); } [Fact] diff --git a/src/shared/GitHub.UI/Commands/CredentialsCommand.cs b/src/shared/GitHub.UI/Commands/CredentialsCommand.cs index 0f168c020..36028ec19 100644 --- a/src/shared/GitHub.UI/Commands/CredentialsCommand.cs +++ b/src/shared/GitHub.UI/Commands/CredentialsCommand.cs @@ -81,7 +81,7 @@ private async Task ExecuteAsync(CommandOptions options) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/shared/GitHub.UI/Commands/DeviceCommand.cs b/src/shared/GitHub.UI/Commands/DeviceCommand.cs index 6004055fe..a98f58ab7 100644 --- a/src/shared/GitHub.UI/Commands/DeviceCommand.cs +++ b/src/shared/GitHub.UI/Commands/DeviceCommand.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; using System.Threading; @@ -38,7 +37,7 @@ private async Task ExecuteAsync(string code, string url) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } return 0; diff --git a/src/shared/GitHub.UI/Commands/TwoFactorCommand.cs b/src/shared/GitHub.UI/Commands/TwoFactorCommand.cs index 192116725..1ba0d0a0d 100644 --- a/src/shared/GitHub.UI/Commands/TwoFactorCommand.cs +++ b/src/shared/GitHub.UI/Commands/TwoFactorCommand.cs @@ -33,7 +33,7 @@ private async Task ExecuteAsync(bool sms) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } WriteResult(new Dictionary diff --git a/src/shared/GitHub/GitHubAuthentication.cs b/src/shared/GitHub/GitHubAuthentication.cs index 95b62a5d2..ca985511e 100644 --- a/src/shared/GitHub/GitHubAuthentication.cs +++ b/src/shared/GitHub/GitHubAuthentication.cs @@ -109,7 +109,7 @@ public async Task GetAuthenticationAsync(Uri targetU if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Exception("Missing 'mode' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -117,7 +117,7 @@ public async Task GetAuthenticationAsync(Uri targetU case "pat": if (!resultDict.TryGetValue("pat", out string pat)) { - throw new Exception("Missing 'pat' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'pat' in response"); } return new AuthenticationPromptResult( @@ -132,19 +132,20 @@ public async Task GetAuthenticationAsync(Uri targetU case "basic": if (!resultDict.TryGetValue("username", out userName)) { - throw new Exception("Missing 'username' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Exception("Missing 'password' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'password' in response"); } return new AuthenticationPromptResult( AuthenticationModes.Basic, new GitCredential(userName, password)); default: - throw new Exception($"Unknown mode value in response '{responseMode}'"); + throw new Trace2Exception(Context.Trace2, + $"Unknown mode value in response '{responseMode}'"); } } else @@ -227,7 +228,7 @@ public async Task GetTwoFactorCodeAsync(Uri targetUri, bool isSms) if (!resultDict.TryGetValue("code", out string authCode)) { - throw new Exception("Missing 'code' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'code' in response"); } return authCode; @@ -255,12 +256,13 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, { ThrowIfUserInteractionDisabled(); - var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri); + var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); // Can we launch the user's default web browser? if (!Context.SessionManager.IsWebBrowserAvailable) { - throw new InvalidOperationException("Browser authentication requires a desktop session"); + throw new Trace2InvalidOperationException(Context.Trace2, + "Browser authentication requires a desktop session"); } var browserOptions = new OAuth2WebBrowserOptions @@ -293,7 +295,7 @@ public async Task GetOAuthTokenViaDeviceCodeAsync(Uri targetU { ThrowIfUserInteractionDisabled(); - var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri); + var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); OAuth2DeviceCodeResult dcr = await oauthClient.GetDeviceCodeAsync(scopes, CancellationToken.None); // If we have a desktop session show the device code in a dialog @@ -329,7 +331,8 @@ public async Task GetOAuthTokenViaDeviceCodeAsync(Uri targetU } catch (OperationCanceledException) { - throw new Exception("User canceled device code authentication"); + throw new Trace2InvalidOperationException(Context.Trace2, + "User canceled device code authentication"); } // Close the dialog diff --git a/src/shared/GitHub/GitHubHostProvider.cs b/src/shared/GitHub/GitHubHostProvider.cs index 040a14dfc..039c85d44 100644 --- a/src/shared/GitHub/GitHubHostProvider.cs +++ b/src/shared/GitHub/GitHubHostProvider.cs @@ -123,7 +123,8 @@ public override async Task GenerateCredentialAsync(InputArguments i // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { - throw new Exception("Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); + throw new Trace2Exception(Context.Trace2, + "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); @@ -226,7 +227,9 @@ private async Task GeneratePersonalAccessTokenAsync(Uri targetUri return new GitCredential(userInfo.Login, token); } - throw new Exception($"Interactive logon for '{targetUri}' failed."); + var format = "Interactive logon for '{0}' failed."; + var message = string.Format(format, targetUri); + throw new Trace2Exception(Context.Trace2, message, format); } internal async Task GetSupportedAuthenticationModesAsync(Uri targetUri) @@ -285,10 +288,14 @@ internal async Task GetSupportedAuthenticationModesAsync(Ur } catch (Exception ex) { - Context.Trace.WriteLine($"Failed to query '{targetUri}' for supported authentication schemes."); + var format = "Failed to query '{0}' for supported authentication schemes."; + var message = string.Format(format, targetUri); + + Context.Trace.WriteLine(message); Context.Trace.WriteException(ex); + Context.Trace2.WriteError(message, format); - Context.Terminal.WriteLine($"warning: failed to query '{targetUri}' for supported authentication schemes."); + Context.Terminal.WriteLine($"warning: {message}"); // Fall-back to offering all modes so the user is never blocked from authenticating by at least one mode return AuthenticationModes.All; diff --git a/src/shared/GitHub/GitHubOAuth2Client.cs b/src/shared/GitHub/GitHubOAuth2Client.cs index 3c46a2ad2..f37b13f86 100644 --- a/src/shared/GitHub/GitHubOAuth2Client.cs +++ b/src/shared/GitHub/GitHubOAuth2Client.cs @@ -7,9 +7,9 @@ namespace GitHub { public class GitHubOAuth2Client : OAuth2Client { - public GitHubOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri) + public GitHubOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri, ITrace2 trace2) : base(httpClient, CreateEndpoints(baseUri), - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings)) { } + GetClientId(settings), trace2, GetRedirectUri(settings), GetClientSecret(settings)) { } private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri) { diff --git a/src/shared/GitLab.Tests/GitLabAuthenticationTests.cs b/src/shared/GitLab.Tests/GitLabAuthenticationTests.cs index 6054e2434..b4352c3e0 100644 --- a/src/shared/GitLab.Tests/GitLabAuthenticationTests.cs +++ b/src/shared/GitLab.Tests/GitLabAuthenticationTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using GitCredentialManager; using GitCredentialManager.Tests.Objects; using Xunit; @@ -36,7 +37,7 @@ public async Task GitLabAuthentication_GetAuthenticationAsync_TerminalPromptsDis var context = new TestCommandContext(); context.Settings.IsTerminalPromptsEnabled = false; var auth = new GitLabAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because terminal prompts have been disabled.", exception.Message); @@ -87,7 +88,7 @@ public async Task GitLabAuthentication_GetAuthenticationAsync_AuthenticationMode var context = new TestCommandContext(); context.Settings.IsInteractionAllowed = false; var auth = new GitLabAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(new Uri("https://GitLab.com"), null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because user interactivity has been disabled.", exception.Message); diff --git a/src/shared/GitLab.UI/Commands/CredentialsCommand.cs b/src/shared/GitLab.UI/Commands/CredentialsCommand.cs index d9fa6c4a5..6cfa9cad8 100644 --- a/src/shared/GitLab.UI/Commands/CredentialsCommand.cs +++ b/src/shared/GitLab.UI/Commands/CredentialsCommand.cs @@ -76,7 +76,7 @@ private async Task ExecuteAsync(CommandOptions options) if (!viewModel.WindowResult) { - throw new Exception("User cancelled dialog."); + throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/shared/GitLab/GitLabAuthentication.cs b/src/shared/GitLab/GitLabAuthentication.cs index 40983491e..990e2b72f 100644 --- a/src/shared/GitLab/GitLabAuthentication.cs +++ b/src/shared/GitLab/GitLabAuthentication.cs @@ -87,7 +87,7 @@ public async Task GetAuthenticationAsync(Uri targetU if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Exception("Missing 'mode' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -95,7 +95,7 @@ public async Task GetAuthenticationAsync(Uri targetU case "pat": if (!resultDict.TryGetValue("pat", out string pat)) { - throw new Exception("Missing 'pat' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'pat' in response"); } if (!resultDict.TryGetValue("username", out string patUserName)) @@ -112,19 +112,20 @@ public async Task GetAuthenticationAsync(Uri targetU case "basic": if (!resultDict.TryGetValue("username", out userName)) { - throw new Exception("Missing 'username' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Exception("Missing 'password' in response"); + throw new Trace2Exception(Context.Trace2, "Missing 'password' in response"); } return new AuthenticationPromptResult( AuthenticationModes.Basic, new GitCredential(userName, password)); default: - throw new Exception($"Unknown mode value in response '{responseMode}'"); + throw new Trace2Exception(Context.Trace2, + $"Unknown mode value in response '{responseMode}'"); } } else @@ -201,12 +202,13 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, { ThrowIfUserInteractionDisabled(); - var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri); + var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) { - throw new InvalidOperationException("Browser authentication requires a desktop session"); + throw new Trace2InvalidOperationException(Context.Trace2, + "Browser authentication requires a desktop session"); } var browserOptions = new OAuth2WebBrowserOptions { }; @@ -223,7 +225,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, public async Task GetOAuthTokenViaRefresh(Uri targetUri, string refreshToken) { - var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri); + var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); return await oauthClient.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None); } diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs index 2836f41c6..decd1d8df 100644 --- a/src/shared/GitLab/GitLabHostProvider.cs +++ b/src/shared/GitLab/GitLabHostProvider.cs @@ -91,7 +91,8 @@ public override async Task GenerateCredentialAsync(InputArguments i // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { - throw new Exception("Unencrypted HTTP is not supported for GitLab. Ensure the repository remote URL is using HTTPS."); + throw new Trace2Exception(Context.Trace2, + "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); diff --git a/src/shared/GitLab/GitLabOAuth2Client.cs b/src/shared/GitLab/GitLabOAuth2Client.cs index fd3004d0d..3b146aaeb 100644 --- a/src/shared/GitLab/GitLabOAuth2Client.cs +++ b/src/shared/GitLab/GitLabOAuth2Client.cs @@ -7,9 +7,9 @@ namespace GitLab { public class GitLabOAuth2Client : OAuth2Client { - public GitLabOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri) + public GitLabOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri, ITrace2 trace2) : base(httpClient, CreateEndpoints(baseUri), - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings)) + GetClientId(settings), trace2, GetRedirectUri(settings), GetClientSecret(settings)) { } private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri) diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs index 0911a5d89..41c8aeb5d 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs @@ -274,7 +274,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_LocSvcReturn context.HttpClientFactory.MessageHandler = httpHandler; var api = new AzureDevOpsRestApi(context); - await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); + await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); } [Fact] @@ -305,7 +305,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu context.HttpClientFactory.MessageHandler = httpHandler; var api = new AzureDevOpsRestApi(context); - await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); + await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); } [Fact] @@ -339,7 +339,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu context.HttpClientFactory.MessageHandler = httpHandler; var api = new AzureDevOpsRestApi(context); - Exception exception = await Assert.ThrowsAsync( + Exception exception = await Assert.ThrowsAsync( () => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); Assert.Contains(serverErrorMessage, exception.Message, StringComparison.Ordinal); diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index 500b1be5d..a55c8331a 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -136,7 +136,7 @@ public async Task AzureReposProvider_GetCredentialAsync_UnencryptedHttp_ThrowsEx var provider = new AzureReposHostProvider(context, azDevOps, msAuth, authorityCache, userMgr); - await Assert.ThrowsAsync(() => provider.GetCredentialAsync(input)); + await Assert.ThrowsAsync(() => provider.GetCredentialAsync(input)); } [Fact] diff --git a/src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs b/src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs index d4b574324..0f4ab8497 100644 --- a/src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs +++ b/src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs @@ -142,13 +142,13 @@ public async Task CreatePersonalAccessTokenAsync(Uri organizationUri, st { if (TryGetFirstJsonStringField(responseText, "message", out string errorMessage)) { - throw new Exception($"Failed to create PAT: {errorMessage}"); + throw new Trace2Exception(_context.Trace2, $"Failed to create PAT: {errorMessage}"); } } } } - throw new Exception("Failed to create PAT"); + throw new Trace2Exception(_context.Trace2, "Failed to create PAT"); } #region Private Methods @@ -181,7 +181,7 @@ private async Task GetIdentityServiceUriAsync(Uri organizationUri, string a } } - throw new Exception("Failed to find location service"); + throw new Trace2Exception(_context.Trace2, "Failed to find location service"); } #endregion diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 3dbc56bf6..cc069e76e 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -184,7 +184,8 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { - throw new Exception("Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); + throw new Trace2Exception(_context.Trace2, + "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } Uri remoteUri = input.GetRemoteUri(); @@ -227,7 +228,8 @@ private async Task GetAzureAccessTokenAsync(Uri // We should not allow unencrypted communication and should inform the user if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) { - throw new Exception("Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); + throw new Trace2Exception(_context.Trace2, + "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); } Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out string orgName); diff --git a/src/shared/TestInfrastructure/Objects/NullTrace.cs b/src/shared/TestInfrastructure/Objects/NullTrace.cs index c1c5ac7a5..0b1092d5d 100644 --- a/src/shared/TestInfrastructure/Objects/NullTrace.cs +++ b/src/shared/TestInfrastructure/Objects/NullTrace.cs @@ -78,6 +78,12 @@ public void WriteChildExit( string filePath = "", int lineNumber = 0) { } + public void WriteError(string errorMessage, + string parameterizedMessage = null, + string filePath = "", + int lineNumber = 0) + { } + public string SetSid() { return ""; } #endregion