diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index b5bade471..225860a03 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -103,7 +103,7 @@ public CommandContext() { FileSystem = new WindowsFileSystem(); Environment = new WindowsEnvironment(FileSystem); - SessionManager = new WindowsSessionManager(Environment, FileSystem); + SessionManager = new WindowsSessionManager(Trace, Environment, FileSystem); ProcessManager = new WindowsProcessManager(Trace2); Terminal = new WindowsTerminal(Trace, Trace2); string gitPath = GetGitPath(Environment, FileSystem, Trace); @@ -120,7 +120,7 @@ public CommandContext() { FileSystem = new MacOSFileSystem(); Environment = new MacOSEnvironment(FileSystem); - SessionManager = new MacOSSessionManager(Environment, FileSystem); + SessionManager = new MacOSSessionManager(Trace, Environment, FileSystem); ProcessManager = new ProcessManager(Trace2); Terminal = new MacOSTerminal(Trace, Trace2); string gitPath = GetGitPath(Environment, FileSystem, Trace); @@ -137,7 +137,7 @@ public CommandContext() { FileSystem = new LinuxFileSystem(); Environment = new PosixEnvironment(FileSystem); - SessionManager = new LinuxSessionManager(Environment, FileSystem); + SessionManager = new LinuxSessionManager(Trace, Environment, FileSystem); ProcessManager = new ProcessManager(Trace2); Terminal = new LinuxTerminal(Trace, Trace2); string gitPath = GetGitPath(Environment, FileSystem, Trace); diff --git a/src/shared/Core/ISessionManager.cs b/src/shared/Core/ISessionManager.cs index b578950af..0ad0204c4 100644 --- a/src/shared/Core/ISessionManager.cs +++ b/src/shared/Core/ISessionManager.cs @@ -40,14 +40,17 @@ public static void OpenBrowser(this ISessionManager sm, string url) public abstract class SessionManager : ISessionManager { + protected ITrace Trace { get; } protected IEnvironment Environment { get; } protected IFileSystem FileSystem { get; } - protected SessionManager(IEnvironment env, IFileSystem fs) + protected SessionManager(ITrace trace, IEnvironment env, IFileSystem fs) { + EnsureArgument.NotNull(trace, nameof(trace)); EnsureArgument.NotNull(env, nameof(env)); EnsureArgument.NotNull(fs, nameof(fs)); + Trace = trace; Environment = env; FileSystem = fs; } @@ -69,6 +72,7 @@ public void OpenBrowser(Uri uri) protected virtual void OpenBrowserInternal(string url) { + Trace.WriteLine("Opening browser using framework shell-execute: " + url); var psi = new ProcessStartInfo(url) { UseShellExecute = true }; Process.Start(psi); } diff --git a/src/shared/Core/Interop/Linux/LinuxSessionManager.cs b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs index 66105e426..9033b4cf2 100644 --- a/src/shared/Core/Interop/Linux/LinuxSessionManager.cs +++ b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs @@ -8,7 +8,7 @@ public class LinuxSessionManager : PosixSessionManager { private bool? _isWebBrowserAvailable; - public LinuxSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs) + public LinuxSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs) { PlatformUtils.EnsureLinux(); } @@ -41,6 +41,8 @@ protected override void OpenBrowserInternal(string url) throw new Exception("Failed to locate a utility to launch the default web browser."); } + Trace.WriteLine($"Opening browser using '{shellExecPath}: {url}"); + var psi = new ProcessStartInfo(shellExecPath, url) { RedirectStandardOutput = true, @@ -53,44 +55,94 @@ protected override void OpenBrowserInternal(string url) private bool GetWebBrowserAvailable() { + // We need a shell execute handler to be able to launch to browser + if (!TryGetShellExecuteHandler(Environment, out _)) + { + Trace.WriteLine("Could not locate a shell execute handler for Linux - browser is not available."); + return false; + } + // If this is a Windows Subsystem for Linux distribution we may - // be able to launch the web browser of the host Windows OS. + // be able to launch the web browser of the host Windows OS, but + // there are further checks to do on the Windows host's session. + // + // 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.IsWslDistribution(Environment, FileSystem, out _)) { - // We need a shell execute handler to be able to launch to browser - if (!TryGetShellExecuteHandler(Environment, out _)) + if (WslUtils.GetWindowsSessionId(FileSystem) == 0) { + Trace.WriteLine("This is a WSL distribution, but Windows session 0 was detected - browser is not available."); 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) + // Not on session 0 - we assume the user can interact with browser on Windows. + Trace.WriteLine("This is a WSL distribution - browser is available."); + return true; + } + + // + // We may also be able to launch a browser if we're inside a Visual Studio Code remote session. + // VSCode overrides the BROWSER environment variable to a script that allows the user to open + // the browser on their client machine. + // + // Even though we can start a browser, one piece of critical functionality we need is the ability + // to have that browser be able to connect back to GCM over localhost. There are several types + // of VSCode remote session, and only some of them automatically forward ports in such a way that + // the client browser can automatically connect back to GCM over localhost. + // + // * SSH [OK] + // Connection over SSH to a remote machine. + // + // * Dev Containers [OK] + // Connection to a container. + // + // * Dev Tunnels [Not OK - forwarded ports not accessible on the client via localhost] + // Connection to a remote machine over the Internet using Microsoft Dev Tunnels. + // + // * WSL [Ignored - already handled above] + // + if (Environment.Variables.ContainsKey("VSCODE_IPC_HOOK_CLI") && + Environment.Variables.ContainsKey("BROWSER")) + { + // Looking for SSH_CONNECTION tells us we're connected via SSH. + // HOWEVER, we may also see SSH_CONNECTION in a Dev Tunnel session if the tunnel server + // process was started within an SSH session (and the SSH_CONNECTION environment variable + // was inherited). + // We therefore check for the absence of the SSH_TTY variable, which gets unset + // in Dev Tunnel sessions but is always still set in regular SSH sessions. + if (Environment.Variables.ContainsKey("SSH_CONNECTION") && + !Environment.Variables.ContainsKey("SSH_TTY")) { - return false; + Trace.WriteLine("VSCode (Remote SSH) detected - browser is available."); + return true; } - // 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; + if (Environment.Variables.ContainsKey("REMOTE_CONTAINERS")) + { + Trace.WriteLine("VSCode (Dev Containers) detected - browser is available."); + return true; + } + + Trace.WriteLine("VSCode (Remote Tunnel) detected - browser is not available."); + return false; } - // We require an interactive desktop session to be able to launch a browser + // We need a desktop session to be able to launch the browser in the general case return IsDesktopSession; } diff --git a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs index 584965ca1..047a4dd96 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(IEnvironment env, IFileSystem fs) : base(env, fs) + public MacOSSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs) { PlatformUtils.EnsureMacOS(); } diff --git a/src/shared/Core/Interop/Posix/PosixSessionManager.cs b/src/shared/Core/Interop/Posix/PosixSessionManager.cs index 8709e12e7..4aee424ed 100644 --- a/src/shared/Core/Interop/Posix/PosixSessionManager.cs +++ b/src/shared/Core/Interop/Posix/PosixSessionManager.cs @@ -2,7 +2,7 @@ namespace GitCredentialManager.Interop.Posix { public abstract class PosixSessionManager : SessionManager { - protected PosixSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs) + protected PosixSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs) { PlatformUtils.EnsurePosix(); } diff --git a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs index d87d76347..2200ae6a5 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(IEnvironment env, IFileSystem fs) : base(env, fs) + public WindowsSessionManager(ITrace trace, IEnvironment env, IFileSystem fs) : base(trace, env, fs) { PlatformUtils.EnsureWindows(); }