diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs index 8e6da45c60..b98a8991dc 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -129,7 +129,7 @@ public async Task CreateOAuthCredentialsAsync(Uri targetUri) FailureResponseHtmlFormat = BitbucketResources.AuthenticationResponseFailureHtmlFormat }; - var browser = new OAuth2SystemWebBrowser(browserOptions); + var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions); var authCodeResult = await oauthClient.GetAuthorizationCodeAsync(Scopes, browser, CancellationToken.None); return await oauthClient.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None); diff --git a/src/shared/GitHub/GitHubAuthentication.cs b/src/shared/GitHub/GitHubAuthentication.cs index 36566265e4..284adc671f 100644 --- a/src/shared/GitHub/GitHubAuthentication.cs +++ b/src/shared/GitHub/GitHubAuthentication.cs @@ -197,7 +197,7 @@ public async Task GetOAuthTokenAsync(Uri targetUri, IEnumerab SuccessResponseHtml = GitHubResources.AuthenticationResponseSuccessHtml, FailureResponseHtmlFormat = GitHubResources.AuthenticationResponseFailureHtmlFormat }; - var browser = new OAuth2SystemWebBrowser(browserOptions); + var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions); // Write message to the terminal (if any is attached) for some feedback that we're waiting for a web response Context.Terminal.WriteLine("info: please complete authentication in your browser..."); diff --git a/src/shared/Microsoft.Git.CredentialManager/Authentication/OAuth/OAuth2SystemWebBrowser.cs b/src/shared/Microsoft.Git.CredentialManager/Authentication/OAuth/OAuth2SystemWebBrowser.cs index 5f66f43f92..5de3b661a1 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Authentication/OAuth/OAuth2SystemWebBrowser.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Authentication/OAuth/OAuth2SystemWebBrowser.cs @@ -35,10 +35,15 @@ public class OAuth2WebBrowserOptions public class OAuth2SystemWebBrowser : IOAuth2WebBrowser { + private readonly IEnvironment _environment; private readonly OAuth2WebBrowserOptions _options; - public OAuth2SystemWebBrowser(OAuth2WebBrowserOptions options) + public OAuth2SystemWebBrowser(IEnvironment environment, OAuth2WebBrowserOptions options) { + EnsureArgument.NotNull(environment, nameof(environment)); + EnsureArgument.NotNull(options, nameof(options)); + + _environment = environment; _options = options; } @@ -75,18 +80,56 @@ public async Task GetAuthenticationCodeAsync(Uri authorizationUri, Uri redi private void OpenDefaultBrowser(Uri uri) { - if (!StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttp, uri.Scheme) && - !StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttps, uri.Scheme)) + if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && + !uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Can only open HTTP/HTTPS URIs", nameof(uri)); } - var pci = new ProcessStartInfo(uri.ToString()) + string url = uri.ToString(); + + ProcessStartInfo psi = null; + if (PlatformUtils.IsLinux()) { - UseShellExecute = true - }; + // On Linux, 'shell execute' utilities like xdg-open launch a process without + // detaching from the standard in/out descriptors. Some applications (like + // Chromium) write messages to stdout, which is currently hooked up and being + // consumed by Git, and cause errors. + // + // Sadly, the Framework does not allow us to redirect standard streams if we + // set ProcessStartInfo::UseShellExecute = true, so we must manually launch + // these utilities and redirect the standard streams manually. + // + // We try and use the same 'shell execute' utilities as the Framework does, + // searching for them in the same order until we find one. + foreach (string shellExec in new[] { "xdg-open", "gnome-open", "kfmclient" }) + { + if (_environment.TryLocateExecutable(shellExec, out string shellExecPath)) + { + psi = new ProcessStartInfo(shellExecPath, url) + { + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + // We found a way to open the URI; stop searching! + break; + } + } + + if (psi is null) + { + throw new Exception("Failed to locate a utility to launch the default web browser."); + } + } + else + { + // On Windows and macOS, `ShellExecute` and `/usr/bin/open` disconnect the child process + // from our standard in/out streams, so we can just use the Framework to do this. + psi = new ProcessStartInfo(url) {UseShellExecute = true}; + } - Process.Start(pci); + Process.Start(psi); } private async Task InterceptRequestsAsync(Uri listenUri, CancellationToken ct) diff --git a/src/shared/Microsoft.Git.CredentialManager/BrowserHelper.cs b/src/shared/Microsoft.Git.CredentialManager/BrowserHelper.cs deleted file mode 100644 index 1427570192..0000000000 --- a/src/shared/Microsoft.Git.CredentialManager/BrowserHelper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Diagnostics; - -namespace Microsoft.Git.CredentialManager -{ - public static class BrowserHelper - { - public static void OpenDefaultBrowser(string url) - { - if (!url.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && - !url.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("Can only open HTTP/HTTPS URLs", nameof(url)); - } - - var psi = new ProcessStartInfo(url) - { - UseShellExecute = true - }; - - Process.Start(psi); - } - - public static void OpenDefaultBrowser(Uri uri) => OpenDefaultBrowser(uri.ToString()); - } -}