Skip to content

Commit d0b767b

Browse files
committed
wsl: detect host Windows session 0 and disable browser
In order to detect if we have an interactive Windows desktop session from inside WSL we need to 'punch out' from WSL to determine the session ID and window station. Strictly speaking, except for session 0 (from Vista onwards), any Windows session can have exactly one interactive window station (always called WinSta0). However, because we cannot easily determine the window station name from a simple cmd/powershell script, we take a simplified approach which isn't 100% accurate. Instead, we only permit browser auth methods if we are NOT in Windows session 0; any other Windows session we assume we are in WinSta0. The default OpenSSH Server configuration (Windows 10+) has `sshd` running as the built-in NT user NETWORK_SERVICE, which means it runs in session 0 (the services session). This is most common scenario, other than using WSL from a 'real', interactive Windows session that we're likely to face.
1 parent a0e2b9c commit d0b767b

9 files changed

+144
-32
lines changed

src/shared/Core/BrowserUtils.cs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
2525

2626
string url = uri.ToString();
2727

28-
ProcessStartInfo psi = null;
28+
ProcessStartInfo psi;
2929
if (PlatformUtils.IsLinux())
3030
{
3131
//
@@ -41,29 +41,17 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
4141
// We try and use the same 'shell execute' utilities as the Framework does,
4242
// searching for them in the same order until we find one.
4343
//
44-
// One additional 'shell execute' utility we also attempt to use is `wslview`
45-
// that is commonly found on WSL (Windows Subsystem for Linux) distributions that
46-
// opens the browser on the Windows host.
47-
foreach (string shellExec in new[] {"xdg-open", "gnome-open", "kfmclient", "wslview"})
44+
if (!TryGetLinuxShellExecuteHandler(environment, out string shellExecPath))
4845
{
49-
if (environment.TryLocateExecutable(shellExec, out string shellExecPath))
50-
{
51-
psi = new ProcessStartInfo(shellExecPath, url)
52-
{
53-
RedirectStandardOutput = true,
54-
// Ok to redirect stderr for non-git-related processes
55-
RedirectStandardError = true
56-
};
57-
58-
// We found a way to open the URI; stop searching!
59-
break;
60-
}
46+
throw new Exception("Failed to locate a utility to launch the default web browser.");
6147
}
6248

63-
if (psi is null)
49+
psi = new ProcessStartInfo(shellExecPath, url)
6450
{
65-
throw new Exception("Failed to locate a utility to launch the default web browser.");
66-
}
51+
RedirectStandardOutput = true,
52+
// Ok to redirect stderr for non-git-related processes
53+
RedirectStandardError = true
54+
};
6755
}
6856
else
6957
{
@@ -78,5 +66,23 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
7866
// is no need to add the extra overhead associated with ChildProcess here.
7967
Process.Start(psi);
8068
}
69+
70+
public static bool TryGetLinuxShellExecuteHandler(IEnvironment env, out string shellExecPath)
71+
{
72+
// One additional 'shell execute' utility we also attempt to use over the Framework
73+
// is `wslview` that is commonly found on WSL (Windows Subsystem for Linux) distributions
74+
// that opens the browser on the Windows host.
75+
string[] shellHandlers = { "xdg-open", "gnome-open", "kfmclient", WslUtils.WslViewShellHandlerName };
76+
foreach (string shellExec in shellHandlers)
77+
{
78+
if (env.TryLocateExecutable(shellExec, out shellExecPath))
79+
{
80+
return true;
81+
}
82+
}
83+
84+
shellExecPath = null;
85+
return false;
86+
}
8187
}
8288
}

src/shared/Core/CommandContext.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ public CommandContext()
102102
if (PlatformUtils.IsWindows())
103103
{
104104
FileSystem = new WindowsFileSystem();
105-
SessionManager = new WindowsSessionManager();
106105
Environment = new WindowsEnvironment(FileSystem);
106+
SessionManager = new WindowsSessionManager(Environment, FileSystem);
107107
ProcessManager = new WindowsProcessManager(Trace2);
108108
Terminal = new WindowsTerminal(Trace);
109109
string gitPath = GetGitPath(Environment, FileSystem, Trace);
@@ -118,7 +118,7 @@ public CommandContext()
118118
else if (PlatformUtils.IsMacOS())
119119
{
120120
FileSystem = new MacOSFileSystem();
121-
SessionManager = new MacOSSessionManager();
121+
SessionManager = new MacOSSessionManager(Environment, FileSystem);
122122
Environment = new MacOSEnvironment(FileSystem);
123123
ProcessManager = new ProcessManager(Trace2);
124124
Terminal = new MacOSTerminal(Trace);
@@ -134,7 +134,7 @@ public CommandContext()
134134
else if (PlatformUtils.IsLinux())
135135
{
136136
FileSystem = new LinuxFileSystem();
137-
SessionManager = new PosixSessionManager();
137+
SessionManager = new LinuxSessionManager(Environment, FileSystem);
138138
Environment = new PosixEnvironment(FileSystem);
139139
ProcessManager = new ProcessManager(Trace2);
140140
Terminal = new LinuxTerminal(Trace);

src/shared/Core/ISessionManager.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ public interface ISessionManager
1717

1818
public abstract class SessionManager : ISessionManager
1919
{
20+
protected IEnvironment Environment { get; }
21+
protected IFileSystem FileSystem { get; }
22+
23+
protected SessionManager(IEnvironment env, IFileSystem fs)
24+
{
25+
Environment = env;
26+
FileSystem = fs;
27+
}
28+
2029
public abstract bool IsDesktopSession { get; }
2130

2231
public virtual bool IsWebBrowserAvailable => IsDesktopSession;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using GitCredentialManager.Interop.Posix;
2+
3+
namespace GitCredentialManager.Interop.Linux;
4+
5+
public class LinuxSessionManager : PosixSessionManager
6+
{
7+
private bool? _isWebBrowserAvailable;
8+
9+
public LinuxSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
10+
{
11+
PlatformUtils.EnsureLinux();
12+
}
13+
14+
public override bool IsWebBrowserAvailable
15+
{
16+
get
17+
{
18+
return _isWebBrowserAvailable ??= GetWebBrowserAvailable();
19+
}
20+
}
21+
22+
private bool GetWebBrowserAvailable()
23+
{
24+
// If this is a Windows Subsystem for Linux distribution we may
25+
// be able to launch the web browser of the host Windows OS.
26+
if (WslUtils.IsWslDistribution(Environment, FileSystem, out _))
27+
{
28+
// We need a shell execute handler to be able to launch to browser
29+
if (!BrowserUtils.TryGetLinuxShellExecuteHandler(Environment, out _))
30+
{
31+
return false;
32+
}
33+
34+
//
35+
// If we are in Windows logon session 0 then the user can never interact,
36+
// even in the WinSta0 window station. This is typical when SSH-ing into a
37+
// Windows 10+ machine using the default OpenSSH Server configuration,
38+
// which runs in the 'services' session 0.
39+
//
40+
// If we're in any other session, and in the WinSta0 window station then
41+
// the user can possibly interact. However, since it's hard to determine
42+
// the window station from PowerShell cmdlets (we'd need to write P/Invoke
43+
// code and that's just messy and too many levels of indirection quite
44+
// frankly!) we just assume any non session 0 is interactive.
45+
//
46+
// This assumption doesn't hold true if the user has changed the user that
47+
// the OpenSSH Server service runs as (not a built-in NT service) *AND*
48+
// they've SSH-ed into the Windows host (and then started a WSL shell).
49+
// This feels like a very small subset of users...
50+
//
51+
if (WslUtils.GetWindowsSessionId(FileSystem) == 0)
52+
{
53+
return false;
54+
}
55+
56+
// If we are not in session 0, or we cannot get the Windows session ID,
57+
// assume that we *CAN* launch the browser so that users are never blocked.
58+
return true;
59+
}
60+
61+
// We require an interactive desktop session to be able to launch a browser
62+
return IsDesktopSession;
63+
}
64+
}

src/shared/Core/Interop/MacOS/MacOSSessionManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.MacOS
55
{
66
public class MacOSSessionManager : PosixSessionManager
77
{
8-
public MacOSSessionManager()
8+
public MacOSSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
99
{
1010
PlatformUtils.EnsureMacOS();
1111
}
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
using System;
2-
31
namespace GitCredentialManager.Interop.Posix
42
{
5-
public class PosixSessionManager : SessionManager
3+
public abstract class PosixSessionManager : SessionManager
64
{
7-
public PosixSessionManager()
5+
protected PosixSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
86
{
97
PlatformUtils.EnsurePosix();
108
}
119

1210
// Check if we have an X11 or Wayland display environment available
1311
public override bool IsDesktopSession =>
14-
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY")) ||
15-
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
12+
!string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("DISPLAY")) ||
13+
!string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
1614
}
1715
}

src/shared/Core/Interop/Windows/WindowsSessionManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.Windows
55
{
66
public class WindowsSessionManager : SessionManager
77
{
8-
public WindowsSessionManager()
8+
public WindowsSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
99
{
1010
PlatformUtils.EnsureWindows();
1111
}

src/shared/Core/ProcessManager.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Diagnostics;
2-
using System.Threading.Tasks;
32

43
namespace GitCredentialManager;
54

src/shared/Core/WslUtils.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public static class WslUtils
2323
private const string DefaultWslMountPrefix = "/mnt";
2424
private const string DefaultWslSysDriveMountName = "c";
2525

26+
internal const string WslViewShellHandlerName = "wslview";
27+
28+
/// <summary>
29+
/// Cached Windows host session ID.
30+
/// </summary>
31+
/// <remarks>A value less than 0 represents "unknown".</remarks>
32+
private static int _windowsSessionId = -1;
33+
2634
/// <summary>
2735
/// Cached WSL version.
2836
/// </summary>
@@ -176,6 +184,34 @@ public static Process CreateWindowsShellProcess(IFileSystem fs,
176184
return new Process { StartInfo = psi };
177185
}
178186

187+
/// <summary>
188+
/// Get the host Windows session ID.
189+
/// </summary>
190+
/// <returns>Windows session ID, or a negative value if it is not known.</returns>
191+
public static int GetWindowsSessionId(IFileSystem fs)
192+
{
193+
if (_windowsSessionId < 0)
194+
{
195+
const string script = @"(Get-Process -ID $PID).SessionId";
196+
using (Process proc = CreateWindowsShellProcess(fs, WindowsShell.PowerShell, script))
197+
{
198+
proc.Start();
199+
proc.WaitForExit();
200+
201+
if (proc.ExitCode == 0)
202+
{
203+
string output = proc.StandardOutput.ReadToEnd().Trim();
204+
if (int.TryParse(output, out int sessionId))
205+
{
206+
_windowsSessionId = sessionId;
207+
}
208+
}
209+
}
210+
}
211+
212+
return _windowsSessionId;
213+
}
214+
179215
private static string GetSystemDriveMountPath(IFileSystem fs)
180216
{
181217
if (_sysDriveMountPath is null)

0 commit comments

Comments
 (0)