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)