diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs index a17bdf5f3e..4ae07c0a55 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 1cacd769e3..d20b3de2ae 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; @@ -88,7 +87,7 @@ public async Task GetCredentialsAsync(Uri targetUri, st // We need at least one mode! if (modes == AuthenticationModes.None) { - throw new ArgumentException(@$"Must specify at least one {nameof(AuthenticationModes)}", nameof(modes)); + throw new ArgumentOutOfRangeException(nameof(modes), @$"At least one {nameof(AuthenticationModes)} must be supplied"); } // Shell out to the UI helper and show the Bitbucket u/p prompt @@ -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 f1950aca18..f3f653f013 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/DataCenter/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs index 0688e03235..689eba15f0 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/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs index 8839eda374..9ab5770c8c 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.UI/Commands/CredentialsCommand.cs b/src/shared/Core.UI/Commands/CredentialsCommand.cs index 02da8472f6..0016933598 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 863e24a1de..4df787caf1 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 aa48d7eb17..619b1d921b 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 d700f815f4..c16c3e0a88 100644 --- a/src/shared/Core/Authentication/AuthenticationBase.cs +++ b/src/shared/Core/Authentication/AuthenticationBase.cs @@ -46,7 +46,10 @@ protected AuthenticationBase(ICommandContext context) 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 AuthenticationBase(ICommandContext context) 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 9d485105ee..b194893d3a 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 491fffd0f3..0278fe8878 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 3fc6020288..39b1d8d14a 100644 --- a/src/shared/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/shared/Core/Authentication/OAuth/OAuth2Client.cs @@ -163,17 +163,19 @@ public IOAuth2CodeGenerator CodeGenerator 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); @@ -183,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); @@ -383,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 8da18faad4..aa5cc93ee6 100644 --- a/src/shared/Core/Authentication/OAuthAuthentication.cs +++ b/src/shared/Core/Authentication/OAuthAuthentication.cs @@ -82,7 +82,7 @@ public OAuthAuthentication(ICommandContext context) 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 OAuthAuthentication(ICommandContext context) 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/Commands/GitCommandBase.cs b/src/shared/Core/Commands/GitCommandBase.cs index 2bb6d0fe53..4db6974e4a 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 7a4f078d50..8085e87eda 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 3c0d221c83..8245b41c96 100644 --- a/src/shared/Core/CredentialStore.cs +++ b/src/shared/Core/CredentialStore.cs @@ -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 c85fc6b0f0..7514658f35 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -194,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 da283bec77..0740093b79 100644 --- a/src/shared/Core/Git.cs +++ b/src/shared/Core/Git.cs @@ -134,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); } } } @@ -158,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'); @@ -210,7 +212,9 @@ public ChildProcess CreateProcess(string args) 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)) @@ -239,9 +243,13 @@ public ChildProcess CreateProcess(string args) 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 70f2d3b550..f1d85caa7d 100644 --- a/src/shared/Core/Gpg.cs +++ b/src/shared/Core/Gpg.cs @@ -46,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(); @@ -55,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(); @@ -78,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); @@ -90,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 237c32e541..7907ff7dc7 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 @@ async Task MatchProviderAsync(HostProviderPriority priority, bool } 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 a3ea8ab6f0..b34aecc1a4 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 2d483556ff..f7ea6f89a6 100644 --- a/src/shared/Core/Interop/Linux/LinuxTerminal.cs +++ b/src/shared/Core/Interop/Linux/LinuxTerminal.cs @@ -36,7 +36,7 @@ public TtyContext(ITrace trace, ITrace2 trace2, 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, ITrace2 trace2, 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 0cc339ac22..a4c9d21208 100644 --- a/src/shared/Core/Interop/MacOS/MacOSTerminal.cs +++ b/src/shared/Core/Interop/MacOS/MacOSTerminal.cs @@ -36,7 +36,7 @@ public TtyContext(ITrace trace, ITrace2 trace2, 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, ITrace2 trace2, 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/Windows/Native/Win32Error.cs b/src/shared/Core/Interop/Windows/Native/Win32Error.cs index f5c4363398..f6a170bda6 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 a261bd2748..b8f5f34757 100644 --- a/src/shared/Core/Interop/Windows/WindowsTerminal.cs +++ b/src/shared/Core/Interop/Windows/WindowsTerminal.cs @@ -60,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); } } } @@ -118,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, @@ -132,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 @@ -152,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); } } @@ -163,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; @@ -191,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/GitHub.UI/Commands/CredentialsCommand.cs b/src/shared/GitHub.UI/Commands/CredentialsCommand.cs index 0f168c020c..36028ec19c 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 6004055fe0..a98f58ab74 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 192116725f..1ba0d0a0d8 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 55270a8c5c..ca985511ee 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; @@ -260,7 +261,8 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, // 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 @@ -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 040a14dfc2..039c85d44f 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/GitLab.UI/Commands/CredentialsCommand.cs b/src/shared/GitLab.UI/Commands/CredentialsCommand.cs index d9fa6c4a57..6cfa9cad86 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 3f4b658fb1..990e2b72f7 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 @@ -206,7 +207,8 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, // 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 { }; diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs index 2836f41c63..decd1d8df1 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/Microsoft.AzureRepos/AzureDevOpsRestApi.cs b/src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs index d4b5743248..0f4ab8497c 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 3dbc56bf65..cc069e76e9 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);