diff --git a/src/shared/Core/BrowserUtils.cs b/src/shared/Core/BrowserUtils.cs index 3fe5f31449..e13607da0e 100644 --- a/src/shared/Core/BrowserUtils.cs +++ b/src/shared/Core/BrowserUtils.cs @@ -15,6 +15,43 @@ public static bool IsWebBrowserAvailable(IEnvironment env, IFileSystem fs, ISess private static bool GetWebBrowserAvailable(IEnvironment env, IFileSystem fs, ISessionManager sm) { + // If this is a Windows Subsystem for Linux distribution we may + // be able to launch the web browser of the host Windows OS. + if (PlatformUtils.IsLinux() && WslUtils.IsWslDistribution(env, fs, out _)) + { + // We need a shell execute handler to be able to launch to browser + if (!TryGetLinuxShellExecuteHandler(env, 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(fs) == 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 sm.IsDesktopSession; } @@ -39,7 +76,7 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri) string url = uri.ToString(); - ProcessStartInfo psi = null; + ProcessStartInfo psi; if (PlatformUtils.IsLinux()) { // @@ -55,29 +92,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 { @@ -92,5 +117,23 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri) // is no need to add the extra overhead associated with ChildProcess here. Process.Start(psi); } + + private 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/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 Process CreateWindowsShellProcess(IFileSystem fs, 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)