Skip to content

Commit 85e4125

Browse files
authored
Merge pull request #181 from microsoft/master
New release to fix Linux web browser launching
2 parents 7988edd + 13f53e0 commit 85e4125

File tree

12 files changed

+136
-72
lines changed

12 files changed

+136
-72
lines changed

src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public async Task<OAuth2TokenResult> CreateOAuthCredentialsAsync(Uri targetUri)
129129
FailureResponseHtmlFormat = BitbucketResources.AuthenticationResponseFailureHtmlFormat
130130
};
131131

132-
var browser = new OAuth2SystemWebBrowser(browserOptions);
132+
var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions);
133133
var authCodeResult = await oauthClient.GetAuthorizationCodeAsync(Scopes, browser, CancellationToken.None);
134134

135135
return await oauthClient.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public async Task<OAuth2TokenResult> GetOAuthTokenAsync(Uri targetUri, IEnumerab
197197
SuccessResponseHtml = GitHubResources.AuthenticationResponseSuccessHtml,
198198
FailureResponseHtmlFormat = GitHubResources.AuthenticationResponseFailureHtmlFormat
199199
};
200-
var browser = new OAuth2SystemWebBrowser(browserOptions);
200+
var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions);
201201

202202
// Write message to the terminal (if any is attached) for some feedback that we're waiting for a web response
203203
Context.Terminal.WriteLine("info: please complete authentication in your browser...");

src/shared/Microsoft.Git.CredentialManager/Authentication/OAuth/OAuth2SystemWebBrowser.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@ public class OAuth2WebBrowserOptions
3535

3636
public class OAuth2SystemWebBrowser : IOAuth2WebBrowser
3737
{
38+
private readonly IEnvironment _environment;
3839
private readonly OAuth2WebBrowserOptions _options;
3940

40-
public OAuth2SystemWebBrowser(OAuth2WebBrowserOptions options)
41+
public OAuth2SystemWebBrowser(IEnvironment environment, OAuth2WebBrowserOptions options)
4142
{
43+
EnsureArgument.NotNull(environment, nameof(environment));
44+
EnsureArgument.NotNull(options, nameof(options));
45+
46+
_environment = environment;
4247
_options = options;
4348
}
4449

@@ -75,18 +80,56 @@ public async Task<Uri> GetAuthenticationCodeAsync(Uri authorizationUri, Uri redi
7580

7681
private void OpenDefaultBrowser(Uri uri)
7782
{
78-
if (!StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttp, uri.Scheme) &&
79-
!StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttps, uri.Scheme))
83+
if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
84+
!uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
8085
{
8186
throw new ArgumentException("Can only open HTTP/HTTPS URIs", nameof(uri));
8287
}
8388

84-
var pci = new ProcessStartInfo(uri.ToString())
89+
string url = uri.ToString();
90+
91+
ProcessStartInfo psi = null;
92+
if (PlatformUtils.IsLinux())
8593
{
86-
UseShellExecute = true
87-
};
94+
// On Linux, 'shell execute' utilities like xdg-open launch a process without
95+
// detaching from the standard in/out descriptors. Some applications (like
96+
// Chromium) write messages to stdout, which is currently hooked up and being
97+
// consumed by Git, and cause errors.
98+
//
99+
// Sadly, the Framework does not allow us to redirect standard streams if we
100+
// set ProcessStartInfo::UseShellExecute = true, so we must manually launch
101+
// these utilities and redirect the standard streams manually.
102+
//
103+
// We try and use the same 'shell execute' utilities as the Framework does,
104+
// searching for them in the same order until we find one.
105+
foreach (string shellExec in new[] { "xdg-open", "gnome-open", "kfmclient" })
106+
{
107+
if (_environment.TryLocateExecutable(shellExec, out string shellExecPath))
108+
{
109+
psi = new ProcessStartInfo(shellExecPath, url)
110+
{
111+
RedirectStandardOutput = true,
112+
RedirectStandardError = true
113+
};
114+
115+
// We found a way to open the URI; stop searching!
116+
break;
117+
}
118+
}
119+
120+
if (psi is null)
121+
{
122+
throw new Exception("Failed to locate a utility to launch the default web browser.");
123+
}
124+
}
125+
else
126+
{
127+
// On Windows and macOS, `ShellExecute` and `/usr/bin/open` disconnect the child process
128+
// from our standard in/out streams, so we can just use the Framework to do this.
129+
psi = new ProcessStartInfo(url) {UseShellExecute = true};
130+
}
88131

89-
Process.Start(pci);
132+
Process.Start(psi);
90133
}
91134

92135
private async Task<Uri> InterceptRequestsAsync(Uri listenUri, CancellationToken ct)

src/shared/Microsoft.Git.CredentialManager/BrowserHelper.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/shared/Microsoft.Git.CredentialManager/EnvironmentBase.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ public interface IEnvironment
4141
/// Locate an executable on the current PATH.
4242
/// </summary>
4343
/// <param name="program">Executable program name.</param>
44-
/// <returns>List of all instances of the found executable program, in order of most specific to least.</returns>
45-
string LocateExecutable(string program);
44+
/// <param name="path">First instance of the found executable program.</param>
45+
/// <returns>True if the executable was found, false otherwise.</returns>
46+
bool TryLocateExecutable(string program, out string path);
4647
}
4748

4849
public abstract class EnvironmentBase : IEnvironment
@@ -75,6 +76,25 @@ public bool IsDirectoryOnPath(string directoryPath)
7576

7677
protected abstract string[] SplitPathVariable(string value);
7778

78-
public abstract string LocateExecutable(string program);
79+
public abstract bool TryLocateExecutable(string program, out string path);
80+
}
81+
82+
public static class EnvironmentExtensions
83+
{
84+
/// <summary>
85+
/// Locate an executable on the current PATH.
86+
/// </summary>
87+
/// <param name="environment">The <see cref="IEnvironment"/>.</param>
88+
/// <param name="program">Executable program name.</param>
89+
/// <returns>List of all instances of the found executable program, in order of most specific to least.</returns>
90+
public static string LocateExecutable(this IEnvironment environment, string program)
91+
{
92+
if (environment.TryLocateExecutable(program, out string path))
93+
{
94+
return path;
95+
}
96+
97+
throw new Exception($"Failed to locate '{program}' executable on the path.");
98+
}
7999
}
80100
}

src/shared/Microsoft.Git.CredentialManager/Interop/Posix/PosixEnvironment.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected override string[] SplitPathVariable(string value)
3131
return value.Split(':');
3232
}
3333

34-
public override string LocateExecutable(string program)
34+
public override bool TryLocateExecutable(string program, out string path)
3535
{
3636
const string whichPath = "/usr/bin/which";
3737
var psi = new ProcessStartInfo(whichPath, program)
@@ -45,19 +45,21 @@ public override string LocateExecutable(string program)
4545
where.Start();
4646
where.WaitForExit();
4747

48-
if (where.ExitCode != 0)
48+
switch (where.ExitCode)
4949
{
50-
throw new Exception($"Failed to locate '{program}' using {whichPath}. Exit code: {where.ExitCode}.");
51-
}
50+
case 0: // found
51+
string stdout = where.StandardOutput.ReadToEnd();
52+
string[] results = stdout.Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
53+
path = results.First();
54+
return true;
5255

53-
string stdout = where.StandardOutput.ReadToEnd();
54-
if (string.IsNullOrWhiteSpace(stdout))
55-
{
56-
return null;
57-
}
56+
case 1: // not found
57+
path = null;
58+
return false;
5859

59-
string[] results = stdout.Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
60-
return results.FirstOrDefault();
60+
default:
61+
throw new Exception($"Unknown error locating '{program}' using {whichPath}. Exit code: {where.ExitCode}.");
62+
}
6163
}
6264
}
6365

src/shared/Microsoft.Git.CredentialManager/Interop/Windows/WindowsEnvironment.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVa
6565
}
6666
}
6767

68-
public override string LocateExecutable(string program)
68+
public override bool TryLocateExecutable(string program, out string path)
6969
{
7070
string wherePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "where.exe");
7171
var psi = new ProcessStartInfo(wherePath, program)
@@ -79,19 +79,21 @@ public override string LocateExecutable(string program)
7979
where.Start();
8080
where.WaitForExit();
8181

82-
if (where.ExitCode != 0)
82+
switch (where.ExitCode)
8383
{
84-
throw new Exception($"Failed to locate '{program}' using where.exe. Exit code: {where.ExitCode}.");
84+
case 0: // found
85+
string stdout = where.StandardOutput.ReadToEnd();
86+
string[] results = stdout.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
87+
path = results.First();
88+
return true;
89+
90+
case 1: // not found
91+
path = null;
92+
return false;
93+
94+
default:
95+
throw new Exception($"Unknown error locating '{program}' using where.exe. Exit code: {where.ExitCode}.");
8596
}
86-
87-
string stdout = where.StandardOutput.ReadToEnd();
88-
if (string.IsNullOrWhiteSpace(stdout))
89-
{
90-
return null;
91-
}
92-
93-
string[] results = stdout.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
94-
return results.FirstOrDefault();
9597
}
9698
}
9799

src/shared/TestInfrastructure/Objects/TestEnvironment.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,24 @@ public void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTar
8080
Variables["PATH"] = string.Join(_envPathSeparator, Path);
8181
}
8282

83-
public string LocateExecutable(string program)
83+
public bool TryLocateExecutable(string program, out string path)
8484
{
8585
if (WhichFiles.TryGetValue(program, out ICollection<string> paths))
8686
{
87-
return paths.FirstOrDefault();
87+
path = paths.First();
88+
return true;
8889
}
8990

9091
if (!System.IO.Path.HasExtension(program) && PlatformUtils.IsWindows())
9192
{
9293
// If we're testing on a Windows platform, don't have a file extension, and were unable to locate
9394
// the executable file.. try appending .exe.
94-
return WhichFiles.TryGetValue($"{program}.exe", out paths) ? paths.FirstOrDefault() : null;
95+
path = WhichFiles.TryGetValue($"{program}.exe", out paths) ? paths.First() : null;
96+
return !(path is null);
9597
}
9698

97-
return null;
99+
path = null;
100+
return false;
98101
}
99102

100103
#endregion

src/windows/Atlassian.Bitbucket.UI.Windows/ViewModels/CredentialsViewModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System.Diagnostics;
34
using System.Security;
45
using System.Windows.Input;
56
using Microsoft.Git.CredentialManager;
@@ -23,8 +24,8 @@ public CredentialsViewModel(string username)
2324
{
2425
LoginCommand = new RelayCommand(Accept, () => IsValid);
2526
CancelCommand = new RelayCommand(Cancel);
26-
ForgotPasswordCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
27-
SignUpCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
27+
ForgotPasswordCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
28+
SignUpCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
2829

2930
LoginValidator = PropertyValidator.For(this, x => x.Login).Required(BitbucketResources.LoginRequired);
3031
PasswordValidator = PropertyValidator.For(this, x => x.Password).Required(BitbucketResources.PasswordRequired);

src/windows/Atlassian.Bitbucket.UI.Windows/ViewModels/OAuthViewModel.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System.Diagnostics;
34
using System.Windows.Input;
45
using Microsoft.Git.CredentialManager;
56
using Microsoft.Git.CredentialManager.UI;
@@ -16,9 +17,9 @@ public OAuthViewModel()
1617
{
1718
OkCommand = new RelayCommand(Accept);
1819
CancelCommand = new RelayCommand(Cancel);
19-
LearnMoreCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.TwoFactorLearnMoreLinkUrl));
20-
ForgotPasswordCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
21-
SignUpCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
20+
LearnMoreCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.TwoFactorLearnMoreLinkUrl));
21+
ForgotPasswordCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
22+
SignUpCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
2223
}
2324

2425
public override string Title => BitbucketResources.OAuthWindowTitle;

0 commit comments

Comments
 (0)