diff --git a/src/shared/Core/BrowserUtils.cs b/src/shared/Core/BrowserUtils.cs
index c34164ef0e..6e908d1fcc 100644
--- a/src/shared/Core/BrowserUtils.cs
+++ b/src/shared/Core/BrowserUtils.cs
@@ -25,7 +25,7 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
string url = uri.ToString();
- ProcessStartInfo psi = null;
+ ProcessStartInfo psi;
if (PlatformUtils.IsLinux())
{
//
@@ -41,29 +41,17 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
// We try and use the same 'shell execute' utilities as the Framework does,
// searching for them in the same order until we find one.
//
- // One additional 'shell execute' utility we also attempt to use is `wslview`
- // that is commonly found on WSL (Windows Subsystem for Linux) distributions that
- // opens the browser on the Windows host.
- foreach (string shellExec in new[] {"xdg-open", "gnome-open", "kfmclient", "wslview"})
+ if (!TryGetLinuxShellExecuteHandler(environment, out string shellExecPath))
{
- if (environment.TryLocateExecutable(shellExec, out string shellExecPath))
- {
- psi = new ProcessStartInfo(shellExecPath, url)
- {
- RedirectStandardOutput = true,
- // Ok to redirect stderr for non-git-related processes
- RedirectStandardError = true
- };
-
- // We found a way to open the URI; stop searching!
- break;
- }
+ throw new Exception("Failed to locate a utility to launch the default web browser.");
}
- if (psi is null)
+ psi = new ProcessStartInfo(shellExecPath, url)
{
- throw new Exception("Failed to locate a utility to launch the default web browser.");
- }
+ RedirectStandardOutput = true,
+ // Ok to redirect stderr for non-git-related processes
+ RedirectStandardError = true
+ };
}
else
{
@@ -78,5 +66,23 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
// is no need to add the extra overhead associated with ChildProcess here.
Process.Start(psi);
}
+
+ public static bool TryGetLinuxShellExecuteHandler(IEnvironment env, out string shellExecPath)
+ {
+ // One additional 'shell execute' utility we also attempt to use over the Framework
+ // is `wslview` that is commonly found on WSL (Windows Subsystem for Linux) distributions
+ // that opens the browser on the Windows host.
+ string[] shellHandlers = { "xdg-open", "gnome-open", "kfmclient", WslUtils.WslViewShellHandlerName };
+ foreach (string shellExec in shellHandlers)
+ {
+ if (env.TryLocateExecutable(shellExec, out shellExecPath))
+ {
+ return true;
+ }
+ }
+
+ shellExecPath = null;
+ return false;
+ }
}
}
diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs
index 3cd5abe4cb..45ef609b09 100644
--- a/src/shared/Core/CommandContext.cs
+++ b/src/shared/Core/CommandContext.cs
@@ -102,8 +102,8 @@ public CommandContext()
if (PlatformUtils.IsWindows())
{
FileSystem = new WindowsFileSystem();
- SessionManager = new WindowsSessionManager();
Environment = new WindowsEnvironment(FileSystem);
+ SessionManager = new WindowsSessionManager(Environment, FileSystem);
ProcessManager = new ProcessManager(Trace2);
Terminal = new WindowsTerminal(Trace);
string gitPath = GetGitPath(Environment, FileSystem, Trace);
@@ -118,7 +118,7 @@ public CommandContext()
else if (PlatformUtils.IsMacOS())
{
FileSystem = new MacOSFileSystem();
- SessionManager = new MacOSSessionManager();
+ SessionManager = new MacOSSessionManager(Environment, FileSystem);
Environment = new MacOSEnvironment(FileSystem);
ProcessManager = new WindowsProcessManager(Trace2);
Terminal = new MacOSTerminal(Trace);
@@ -134,7 +134,7 @@ public CommandContext()
else if (PlatformUtils.IsLinux())
{
FileSystem = new LinuxFileSystem();
- SessionManager = new PosixSessionManager();
+ SessionManager = new LinuxSessionManager(Environment, FileSystem);
Environment = new PosixEnvironment(FileSystem);
ProcessManager = new ProcessManager(Trace2);
Terminal = new LinuxTerminal(Trace);
diff --git a/src/shared/Core/ISessionManager.cs b/src/shared/Core/ISessionManager.cs
index 165393153d..2569833f41 100644
--- a/src/shared/Core/ISessionManager.cs
+++ b/src/shared/Core/ISessionManager.cs
@@ -17,6 +17,15 @@ public interface ISessionManager
public abstract class SessionManager : ISessionManager
{
+ protected IEnvironment Environment { get; }
+ protected IFileSystem FileSystem { get; }
+
+ protected SessionManager(IEnvironment env, IFileSystem fs)
+ {
+ Environment = env;
+ FileSystem = fs;
+ }
+
public abstract bool IsDesktopSession { get; }
public virtual bool IsWebBrowserAvailable => IsDesktopSession;
diff --git a/src/shared/Core/Interop/Linux/LinuxSessionManager.cs b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs
new file mode 100644
index 0000000000..2147289ac7
--- /dev/null
+++ b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs
@@ -0,0 +1,64 @@
+using GitCredentialManager.Interop.Posix;
+
+namespace GitCredentialManager.Interop.Linux;
+
+public class LinuxSessionManager : PosixSessionManager
+{
+ private bool? _isWebBrowserAvailable;
+
+ public LinuxSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
+ {
+ PlatformUtils.EnsureLinux();
+ }
+
+ public override bool IsWebBrowserAvailable
+ {
+ get
+ {
+ return _isWebBrowserAvailable ??= GetWebBrowserAvailable();
+ }
+ }
+
+ private bool GetWebBrowserAvailable()
+ {
+ // If this is a Windows Subsystem for Linux distribution we may
+ // be able to launch the web browser of the host Windows OS.
+ if (WslUtils.IsWslDistribution(Environment, FileSystem, out _))
+ {
+ // We need a shell execute handler to be able to launch to browser
+ if (!BrowserUtils.TryGetLinuxShellExecuteHandler(Environment, out _))
+ {
+ return false;
+ }
+
+ //
+ // If we are in Windows logon session 0 then the user can never interact,
+ // even in the WinSta0 window station. This is typical when SSH-ing into a
+ // Windows 10+ machine using the default OpenSSH Server configuration,
+ // which runs in the 'services' session 0.
+ //
+ // If we're in any other session, and in the WinSta0 window station then
+ // the user can possibly interact. However, since it's hard to determine
+ // the window station from PowerShell cmdlets (we'd need to write P/Invoke
+ // code and that's just messy and too many levels of indirection quite
+ // frankly!) we just assume any non session 0 is interactive.
+ //
+ // This assumption doesn't hold true if the user has changed the user that
+ // the OpenSSH Server service runs as (not a built-in NT service) *AND*
+ // they've SSH-ed into the Windows host (and then started a WSL shell).
+ // This feels like a very small subset of users...
+ //
+ if (WslUtils.GetWindowsSessionId(FileSystem) == 0)
+ {
+ return false;
+ }
+
+ // If we are not in session 0, or we cannot get the Windows session ID,
+ // assume that we *CAN* launch the browser so that users are never blocked.
+ return true;
+ }
+
+ // We require an interactive desktop session to be able to launch a browser
+ return IsDesktopSession;
+ }
+}
diff --git a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
index febcd20c24..584965ca1b 100644
--- a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
+++ b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
@@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.MacOS
{
public class MacOSSessionManager : PosixSessionManager
{
- public MacOSSessionManager()
+ public MacOSSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureMacOS();
}
diff --git a/src/shared/Core/Interop/Posix/PosixSessionManager.cs b/src/shared/Core/Interop/Posix/PosixSessionManager.cs
index b9624e69a2..8709e12e7e 100644
--- a/src/shared/Core/Interop/Posix/PosixSessionManager.cs
+++ b/src/shared/Core/Interop/Posix/PosixSessionManager.cs
@@ -1,17 +1,15 @@
-using System;
-
namespace GitCredentialManager.Interop.Posix
{
- public class PosixSessionManager : SessionManager
+ public abstract class PosixSessionManager : SessionManager
{
- public PosixSessionManager()
+ protected PosixSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsurePosix();
}
// Check if we have an X11 or Wayland display environment available
public override bool IsDesktopSession =>
- !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY")) ||
- !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
+ !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("DISPLAY")) ||
+ !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
}
}
diff --git a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs
index 464f2c3fd0..d87d76347f 100644
--- a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs
+++ b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs
@@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.Windows
{
public class WindowsSessionManager : SessionManager
{
- public WindowsSessionManager()
+ public WindowsSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureWindows();
}
diff --git a/src/shared/Core/ProcessManager.cs b/src/shared/Core/ProcessManager.cs
index 618e9d04ec..30c9d402f8 100644
--- a/src/shared/Core/ProcessManager.cs
+++ b/src/shared/Core/ProcessManager.cs
@@ -1,5 +1,4 @@
using System.Diagnostics;
-using System.Threading.Tasks;
namespace GitCredentialManager;
diff --git a/src/shared/Core/WslUtils.cs b/src/shared/Core/WslUtils.cs
index 3e062a6fa3..1db63d3292 100644
--- a/src/shared/Core/WslUtils.cs
+++ b/src/shared/Core/WslUtils.cs
@@ -23,6 +23,14 @@ public static class WslUtils
private const string DefaultWslMountPrefix = "/mnt";
private const string DefaultWslSysDriveMountName = "c";
+ internal const string WslViewShellHandlerName = "wslview";
+
+ ///
+ /// Cached Windows host session ID.
+ ///
+ /// A value less than 0 represents "unknown".
+ private static int _windowsSessionId = -1;
+
///
/// Cached WSL version.
///
@@ -176,6 +184,34 @@ public static bool IsWslPath(string path)
return new Process { StartInfo = psi };
}
+ ///
+ /// Get the host Windows session ID.
+ ///
+ /// Windows session ID, or a negative value if it is not known.
+ public static int GetWindowsSessionId(IFileSystem fs)
+ {
+ if (_windowsSessionId < 0)
+ {
+ const string script = @"(Get-Process -ID $PID).SessionId";
+ using (Process proc = CreateWindowsShellProcess(fs, WindowsShell.PowerShell, script))
+ {
+ proc.Start();
+ proc.WaitForExit();
+
+ if (proc.ExitCode == 0)
+ {
+ string output = proc.StandardOutput.ReadToEnd().Trim();
+ if (int.TryParse(output, out int sessionId))
+ {
+ _windowsSessionId = sessionId;
+ }
+ }
+ }
+ }
+
+ return _windowsSessionId;
+ }
+
private static string GetSystemDriveMountPath(IFileSystem fs)
{
if (_sysDriveMountPath is null)