From a130fec718edbc83e8067f0cde58b1fd720ce087 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 14 Mar 2023 15:07:47 -0700 Subject: [PATCH 1/8] env: avoid virtual member call in ctor; all refresh We had a virtual member call in the constructor of the various IEnvironment implementations, which is a code smell. Instead, lazily populate the `_variables` cache dictionary, and add an explict `Refresh` method to refresh the cache. We call `Refresh` in the `SetEnvironmentVariable` method for convenience. For testing we simply pipe through the pre-computed variables in the `internal` constructor. --- src/shared/Core/EnvironmentBase.cs | 48 +++++++++++++++++-- .../Core/Interop/MacOS/MacOSEnvironment.cs | 8 +--- .../Core/Interop/Posix/PosixEnvironment.cs | 7 +-- .../Interop/Windows/WindowsEnvironment.cs | 11 ++--- .../Objects/TestEnvironment.cs | 6 ++- 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/shared/Core/EnvironmentBase.cs b/src/shared/Core/EnvironmentBase.cs index b83a1b063..02d553f3a 100644 --- a/src/shared/Core/EnvironmentBase.cs +++ b/src/shared/Core/EnvironmentBase.cs @@ -53,18 +53,45 @@ public interface IEnvironment /// Target level of environment variable to set (Machine, Process, or User). void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process); + + /// + /// Refresh the current process environment variables. See . + /// + /// This is automatically called after . + void Refresh(); } public abstract class EnvironmentBase : IEnvironment { + private IReadOnlyDictionary _variables; + protected EnvironmentBase(IFileSystem fileSystem) { EnsureArgument.NotNull(fileSystem, nameof(fileSystem)); - FileSystem = fileSystem; } - public IReadOnlyDictionary Variables { get; protected set; } + internal EnvironmentBase(IFileSystem fileSystem, IReadOnlyDictionary variables) + : this(fileSystem) + { + EnsureArgument.NotNull(variables, nameof(variables)); + _variables = variables; + } + + public IReadOnlyDictionary Variables + { + get + { + // Variables are lazily loaded + if (_variables is null) + { + Refresh(); + } + + Debug.Assert(_variables != null); + return _variables; + } + } protected IFileSystem FileSystem { get; } @@ -126,9 +153,22 @@ internal virtual bool TryLocateExecutable(string program, ICollection pa public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) { - if (Variables.Keys.Contains(variable)) return; + // Don't bother setting the variable if it already has the same value + if (Variables.TryGetValue(variable, out var currentValue) && + StringComparer.Ordinal.Equals(currentValue, value)) + { + return; + } + Environment.SetEnvironmentVariable(variable, value, target); - Variables = GetCurrentVariables(); + + // Immediately refresh the variables so that the new value is available to callers using IEnvironment + Refresh(); + } + + public void Refresh() + { + _variables = GetCurrentVariables(); } protected abstract IReadOnlyDictionary GetCurrentVariables(); diff --git a/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs b/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs index a29f1f4df..256e81cb9 100644 --- a/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs +++ b/src/shared/Core/Interop/MacOS/MacOSEnvironment.cs @@ -15,11 +15,7 @@ public MacOSEnvironment(IFileSystem fileSystem) : base(fileSystem) { } internal MacOSEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) - : base(fileSystem) - { - EnsureArgument.NotNull(variables, nameof(variables)); - Variables = variables; - } + : base(fileSystem, variables) { } public override bool TryLocateExecutable(string program, out string path) { @@ -35,4 +31,4 @@ public override bool TryLocateExecutable(string program, out string path) return TryLocateExecutable(program, _pathsToIgnore, out path); } } -} \ No newline at end of file +} diff --git a/src/shared/Core/Interop/Posix/PosixEnvironment.cs b/src/shared/Core/Interop/Posix/PosixEnvironment.cs index c725c18e1..ec3d91c92 100644 --- a/src/shared/Core/Interop/Posix/PosixEnvironment.cs +++ b/src/shared/Core/Interop/Posix/PosixEnvironment.cs @@ -7,13 +7,10 @@ namespace GitCredentialManager.Interop.Posix public class PosixEnvironment : EnvironmentBase { public PosixEnvironment(IFileSystem fileSystem) - : this(fileSystem, null) { } + : base(fileSystem) { } internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) - : base(fileSystem) - { - Variables = variables ?? GetCurrentVariables(); - } + : base(fileSystem, variables) { } #region EnvironmentBase diff --git a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs index 707e70734..6c3450a38 100644 --- a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs +++ b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs @@ -10,13 +10,10 @@ namespace GitCredentialManager.Interop.Windows public class WindowsEnvironment : EnvironmentBase { public WindowsEnvironment(IFileSystem fileSystem) - : this(fileSystem, null) { } + : base(fileSystem) { } internal WindowsEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) - : base(fileSystem) - { - Variables = variables ?? GetCurrentVariables(); - } + : base(fileSystem, variables) { } #region EnvironmentBase @@ -48,7 +45,7 @@ public override void AddDirectoryToPath(string directoryPath, EnvironmentVariabl Environment.SetEnvironmentVariable("PATH", newValue, target); // Update the cached PATH variable to the latest value (as well as all other variables) - Variables = GetCurrentVariables(); + Refresh(); } public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVariableTarget target) @@ -66,7 +63,7 @@ public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVa Environment.SetEnvironmentVariable("PATH", newValue, target); // Update the cached PATH variable to the latest value (as well as all other variables) - Variables = GetCurrentVariables(); + Refresh(); } } diff --git a/src/shared/TestInfrastructure/Objects/TestEnvironment.cs b/src/shared/TestInfrastructure/Objects/TestEnvironment.cs index 19f1c355f..23ff9d60b 100644 --- a/src/shared/TestInfrastructure/Objects/TestEnvironment.cs +++ b/src/shared/TestInfrastructure/Objects/TestEnvironment.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Linq; namespace GitCredentialManager.Tests.Objects @@ -106,6 +105,11 @@ public bool TryLocateExecutable(string program, out string path) Variables.Add(variable, value); } + public void Refresh() + { + // Nothing to do! + } + #endregion } } From 44627709296dd2e0b62fe6659a9d5f4cfcf8083b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 13:51:03 -0700 Subject: [PATCH 2/8] posix-session: check for Wayland display envars Also check for the WAYLAND_DISPLAY environment variable when determining if a graphical session/display exists for POSIX systems. --- src/shared/Core/CommandContext.cs | 1 - src/shared/Core/Interop/Posix/PosixSessionManager.cs | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index 3346aad30..d051b2ef2 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -134,7 +134,6 @@ public CommandContext() else if (PlatformUtils.IsLinux()) { FileSystem = new LinuxFileSystem(); - // TODO: support more than just 'Posix' or X11 SessionManager = new PosixSessionManager(); Environment = new PosixEnvironment(FileSystem); ProcessManager = new ProcessManager(Trace2); diff --git a/src/shared/Core/Interop/Posix/PosixSessionManager.cs b/src/shared/Core/Interop/Posix/PosixSessionManager.cs index e4ff05cbc..1bfa1c479 100644 --- a/src/shared/Core/Interop/Posix/PosixSessionManager.cs +++ b/src/shared/Core/Interop/Posix/PosixSessionManager.cs @@ -9,7 +9,9 @@ public PosixSessionManager() PlatformUtils.EnsurePosix(); } - // Check if we have an X11 environment available - public virtual bool IsDesktopSession => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY")); + // Check if we have an X11 or Wayland display environment available + public virtual bool IsDesktopSession => + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY")) || + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")); } } From d37c201c3dc9a61078777ccaf770b707579d7722 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 13:52:34 -0700 Subject: [PATCH 3/8] fs: add abstractions for reading all text and enum dir Add abstractions to the IFileSystem interface for enumerating directories and reading the entire contents of a file to a string. --- src/shared/Core/FileSystem.cs | 29 +++++++++++++++ .../Objects/TestFileSystem.cs | 36 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/shared/Core/FileSystem.cs b/src/shared/Core/FileSystem.cs index c1cdcfd13..aeacfd51d 100644 --- a/src/shared/Core/FileSystem.cs +++ b/src/shared/Core/FileSystem.cs @@ -84,6 +84,29 @@ public interface IFileSystem /// IEnumerable EnumerateFiles(string path, string searchPattern); + /// + /// Returns an enumerable collection of directory full names in a specified path. + /// + /// The relative or absolute path to the directory to search. This string is not case-sensitive. + /// + /// An enumerable collection of the full names (including paths) for the directories + /// in the directory specified by path. + /// + IEnumerable EnumerateDirectories(string path); + + /// + /// Opens a text file, reads all the text in the file, and then closes the file + /// + /// The file to open for reading. + /// A string containing all the text in the file. + string ReadAllText(string path); + + /// + /// Opens a text file, reads all lines of the file, and then closes the file. + /// + /// The file to open for reading. + /// A string array containing all lines of the file. + string[] ReadAllLines(string path); } /// @@ -111,5 +134,11 @@ public Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAcce public void DeleteFile(string path) => File.Delete(path); public IEnumerable EnumerateFiles(string path, string searchPattern) => Directory.EnumerateFiles(path, searchPattern); + + public IEnumerable EnumerateDirectories(string path) => Directory.EnumerateDirectories(path); + + public string ReadAllText(string path) => File.ReadAllText(path); + + public string[] ReadAllLines(string path) => File.ReadAllLines(path); } } diff --git a/src/shared/TestInfrastructure/Objects/TestFileSystem.cs b/src/shared/TestInfrastructure/Objects/TestFileSystem.cs index 23918b284..11dff8f1f 100644 --- a/src/shared/TestInfrastructure/Objects/TestFileSystem.cs +++ b/src/shared/TestInfrastructure/Objects/TestFileSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Text.RegularExpressions; namespace GitCredentialManager.Tests.Objects @@ -92,6 +93,41 @@ bool IsPatternMatch(string s, string p) } } + IEnumerable IFileSystem.EnumerateDirectories(string path) + { + StringComparison comparer = IsCaseSensitive + ? StringComparison.Ordinal + : StringComparison.OrdinalIgnoreCase; + + foreach (var dirPath in Directories) + { + if (dirPath.StartsWith(path, comparer)) + { + yield return dirPath; + } + } + } + + string IFileSystem.ReadAllText(string path) + { + if (Files.TryGetValue(path, out byte[] data)) + { + return Encoding.UTF8.GetString(data); + } + + throw new IOException("File not found"); + } + + string[] IFileSystem.ReadAllLines(string path) + { + if (Files.TryGetValue(path, out byte[] data)) + { + return Encoding.UTF8.GetString(data).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + } + + throw new IOException("File not found"); + } + #endregion /// From f25667261e9df8d96b8fce25b7c39f48a800b554 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 13:55:24 -0700 Subject: [PATCH 4/8] browser: split browser-check from desktop session check Split the web browser check from the desktop session checks. In future commits we will enhance the browser-check to take in to account the WSL special case. --- .../Core/Authentication/MicrosoftAuthentication.cs | 6 +++--- src/shared/Core/BrowserUtils.cs | 1 + src/shared/Core/EnvironmentBase.cs | 13 +++++++++++++ src/shared/Core/ISessionManager.cs | 13 +++++++++++++ .../Core/Interop/Posix/PosixSessionManager.cs | 4 ++-- .../Core/Interop/Windows/WindowsSessionManager.cs | 4 ++-- src/shared/GitHub/GitHubAuthentication.cs | 8 ++++---- src/shared/GitLab/GitLabAuthentication.cs | 4 ++-- .../Objects/TestSessionManager.cs | 4 ++++ 9 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index d12e187fa..491fffd0f 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -508,14 +508,14 @@ private void EnsureCanUseEmbeddedWebView() private bool CanUseSystemWebView(IPublicClientApplication app, Uri redirectUri) { // MSAL requires the application redirect URI is a loopback address to use the System WebView - return Context.SessionManager.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback; + return Context.SessionManager.IsWebBrowserAvailable && app.IsSystemWebViewAvailable && redirectUri.IsLoopback; } private void EnsureCanUseSystemWebView(IPublicClientApplication app, Uri redirectUri) { - if (!Context.SessionManager.IsDesktopSession) + if (!Context.SessionManager.IsWebBrowserAvailable) { - throw new InvalidOperationException("System web view is not available without a desktop session."); + throw new InvalidOperationException("System web view is not available without a way to start a browser."); } if (!app.IsSystemWebViewAvailable) diff --git a/src/shared/Core/BrowserUtils.cs b/src/shared/Core/BrowserUtils.cs index 35ddab6d2..c34164ef0 100644 --- a/src/shared/Core/BrowserUtils.cs +++ b/src/shared/Core/BrowserUtils.cs @@ -28,6 +28,7 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri) ProcessStartInfo psi = null; if (PlatformUtils.IsLinux()) { + // // On Linux, 'shell execute' utilities like xdg-open launch a process without // detaching from the standard in/out descriptors. Some applications (like // Chromium) write messages to stdout, which is currently hooked up and being diff --git a/src/shared/Core/EnvironmentBase.cs b/src/shared/Core/EnvironmentBase.cs index 02d553f3a..6a3967193 100644 --- a/src/shared/Core/EnvironmentBase.cs +++ b/src/shared/Core/EnvironmentBase.cs @@ -191,5 +191,18 @@ public static string LocateExecutable(this IEnvironment environment, string prog throw new Exception($"Failed to locate '{program}' executable on the path."); } + + /// + /// Retrieves the value of an environment variable from the current process. + /// + /// The . + /// The name of the environment variable. + /// + /// The value of the environment variable specified by variable, or null if the environment variable is not found. + /// + public static string GetEnvironmentVariable(this IEnvironment environment, string variable) + { + return environment.Variables.TryGetValue(variable, out string value) ? value : null; + } } } diff --git a/src/shared/Core/ISessionManager.cs b/src/shared/Core/ISessionManager.cs index 1f707c046..165393153 100644 --- a/src/shared/Core/ISessionManager.cs +++ b/src/shared/Core/ISessionManager.cs @@ -7,5 +7,18 @@ public interface ISessionManager /// /// True if the session can display UI, false otherwise. bool IsDesktopSession { get; } + + /// + /// Determine if the current session has access to a web browser. + /// + /// True if the session can display a web browser, false otherwise. + bool IsWebBrowserAvailable { get; } + } + + public abstract class SessionManager : ISessionManager + { + public abstract bool IsDesktopSession { get; } + + public virtual bool IsWebBrowserAvailable => IsDesktopSession; } } diff --git a/src/shared/Core/Interop/Posix/PosixSessionManager.cs b/src/shared/Core/Interop/Posix/PosixSessionManager.cs index 1bfa1c479..b9624e69a 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 class PosixSessionManager : ISessionManager + public class PosixSessionManager : SessionManager { public PosixSessionManager() { @@ -10,7 +10,7 @@ public PosixSessionManager() } // Check if we have an X11 or Wayland display environment available - public virtual bool IsDesktopSession => + public override bool IsDesktopSession => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY")) || !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")); } diff --git a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs index 2296a9455..464f2c3fd 100644 --- a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs +++ b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs @@ -3,14 +3,14 @@ namespace GitCredentialManager.Interop.Windows { - public class WindowsSessionManager : ISessionManager + public class WindowsSessionManager : SessionManager { public WindowsSessionManager() { PlatformUtils.EnsureWindows(); } - public unsafe bool IsDesktopSession + public override unsafe bool IsDesktopSession { get { diff --git a/src/shared/GitHub/GitHubAuthentication.cs b/src/shared/GitHub/GitHubAuthentication.cs index bb6c6593e..95b62a5d2 100644 --- a/src/shared/GitHub/GitHubAuthentication.cs +++ b/src/shared/GitHub/GitHubAuthentication.cs @@ -64,8 +64,8 @@ public GitHubAuthentication(ICommandContext context) public async Task GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes) { - // If we don't have a desktop session/GUI then we cannot offer browser - if (!Context.SessionManager.IsDesktopSession) + // If we cannot start a browser then don't offer the option + if (!Context.SessionManager.IsWebBrowserAvailable) { modes = modes & ~AuthenticationModes.Browser; } @@ -257,8 +257,8 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri); - // We require a desktop session to launch the user's default web browser - if (!Context.SessionManager.IsDesktopSession) + // Can we launch the user's default web browser? + if (!Context.SessionManager.IsWebBrowserAvailable) { throw new InvalidOperationException("Browser authentication requires a desktop session"); } diff --git a/src/shared/GitLab/GitLabAuthentication.cs b/src/shared/GitLab/GitLabAuthentication.cs index 2eba8a4f1..40983491e 100644 --- a/src/shared/GitLab/GitLabAuthentication.cs +++ b/src/shared/GitLab/GitLabAuthentication.cs @@ -55,8 +55,8 @@ public GitLabAuthentication(ICommandContext context) public async Task GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes) { - // If we don't have a desktop session/GUI then we cannot offer browser - if (!Context.SessionManager.IsDesktopSession) + // If we cannot start a browser then don't offer the option + if (!Context.SessionManager.IsWebBrowserAvailable) { modes = modes & ~AuthenticationModes.Browser; } diff --git a/src/shared/TestInfrastructure/Objects/TestSessionManager.cs b/src/shared/TestInfrastructure/Objects/TestSessionManager.cs index 0ac775d42..8ac49ebca 100644 --- a/src/shared/TestInfrastructure/Objects/TestSessionManager.cs +++ b/src/shared/TestInfrastructure/Objects/TestSessionManager.cs @@ -3,6 +3,10 @@ namespace GitCredentialManager.Tests.Objects { public class TestSessionManager : ISessionManager { + public bool? IsWebBrowserAvailableOverride { get; set; } + public bool IsDesktopSession { get; set; } + + bool ISessionManager.IsWebBrowserAvailable => IsWebBrowserAvailableOverride ?? IsDesktopSession; } } From afe938c135c568e91031c3ed8f44fd6cec37b24f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 14:04:54 -0700 Subject: [PATCH 5/8] wsl: add ability to detect WSL distro and version Add methods to the WslUtils class to enable detection of a WSL distribution, and also determine which WSL version is being used. Version 1 uses the Windows NT kernel and runs the distribution in the same user-mode space as Windows programs. Version 2 uses a light-weight VM to host a real Linux kernel, and runs the distribution also inside the VM; interop with Windows is achieved using other mechanisms. --- src/shared/Core/WslUtils.cs | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/shared/Core/WslUtils.cs b/src/shared/Core/WslUtils.cs index 87d379622..d7a1e3331 100644 --- a/src/shared/Core/WslUtils.cs +++ b/src/shared/Core/WslUtils.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; +using System.Text.RegularExpressions; namespace GitCredentialManager { @@ -10,6 +12,60 @@ public static class WslUtils private const string WslUncPrefix = @"\\wsl$\"; private const string WslLocalHostUncPrefix = @"\\wsl.localhost\"; private const string WslCommandName = "wsl.exe"; + private const string WslInteropEnvar = "WSL_INTEROP"; + + /// + /// Cached WSL version. + /// + /// A value of 0 represents "not WSL", and a value less than 0 represents "unknown". + private static int _wslVersion = -1; + + public static bool IsWslDistribution(IEnvironment env, IFileSystem fs, out int wslVersion) + { + if (_wslVersion < 0) + { + _wslVersion = GetWslVersion(env, fs); + } + + wslVersion = _wslVersion; + return _wslVersion > 0; + } + + private static int GetWslVersion(IEnvironment env, IFileSystem fs) + { + // All WSL distributions are Linux.. obviously! + if (!PlatformUtils.IsLinux()) + { + return 0; + } + + // The WSL_INTEROP variable is set in WSL2 distributions + if (env.Variables.TryGetValue(WslInteropEnvar, out _)) + { + return 2; + } + + const string procVersionPath = "/proc/version"; + if (fs.FileExists(procVersionPath)) + { + // Both WSL1 and WSL2 distributions include "[Mm]icrosoft" in the version string + string procVersion = fs.ReadAllText(procVersionPath); + if (!Regex.IsMatch(procVersion, "[Mm]icrosoft")) + { + return 0; + } + + // WSL2 distributions return "WSL2" in the version string + if (Regex.IsMatch(procVersion, "wsl2", RegexOptions.IgnoreCase)) + { + return 2; + } + + return 1; + } + + return 0; + } /// /// Test if a file path points to a location in a Windows Subsystem for Linux distribution. From 6b887e1a28f8fcc876168a9f5b0f39b9ead3879d Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 15:54:21 -0700 Subject: [PATCH 6/8] ini: add basic INI file deserialiser Add a basic INI file deserialiser --- src/shared/Core.Tests/IniFileTests.cs | 135 +++++++++++++++++ src/shared/Core/IniFile.cs | 207 ++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 src/shared/Core.Tests/IniFileTests.cs create mode 100644 src/shared/Core/IniFile.cs diff --git a/src/shared/Core.Tests/IniFileTests.cs b/src/shared/Core.Tests/IniFileTests.cs new file mode 100644 index 000000000..a661547d4 --- /dev/null +++ b/src/shared/Core.Tests/IniFileTests.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Text; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests +{ + public class IniFileTests + { + [Fact] + public void IniSectionName_Equality() + { + var a1 = new IniSectionName("foo"); + var b1 = new IniSectionName("foo"); + Assert.Equal(a1,b1); + Assert.Equal(a1.GetHashCode(),b1.GetHashCode()); + + var a2 = new IniSectionName("foo"); + var b2 = new IniSectionName("FOO"); + Assert.Equal(a2,b2); + Assert.Equal(a2.GetHashCode(),b2.GetHashCode()); + + var a3 = new IniSectionName("foo", "bar"); + var b3 = new IniSectionName("foo", "BAR"); + Assert.NotEqual(a3,b3); + Assert.NotEqual(a3.GetHashCode(),b3.GetHashCode()); + + var a4 = new IniSectionName("foo", "bar"); + var b4 = new IniSectionName("FOO", "bar"); + Assert.Equal(a4,b4); + Assert.Equal(a4.GetHashCode(),b4.GetHashCode()); + } + + [Fact] + public void IniSerializer_Deserialize() + { + const string path = "/tmp/test.ini"; + string iniText = @" +[one] + foo = 123 + [two] + foo = abc +# comment +[two ""subsection name""] # comment [section] + foo = this is different # comment prop = val + +#[notasection] + + [ +[bad #section] +recovery tests] +[] + ] + + [three] + bar = a + bar = b + # comment + bar = c + empty = +[TWO] + foo = hello + widget = ""Hello, World!"" +[four] +[five] + prop1 = ""this hash # is inside quotes"" + prop2 = ""this hash # is inside quotes"" # this line has two hashes + prop3 = "" this dquoted string has three spaces around "" + #prop4 = this property has been commented-out +"; + + var fs = new TestFileSystem + { + Files = { [path] = Encoding.UTF8.GetBytes(iniText) } + }; + + IniFile ini = IniSerializer.Deserialize(fs, path); + + Assert.Equal(6, ini.Sections.Count); + + AssertSection(ini, "one", out IniSection one); + Assert.Equal(1, one.Properties.Count); + AssertProperty(one, "foo", "123"); + + AssertSection(ini, "two", out IniSection twoA); + Assert.Equal(3, twoA.Properties.Count); + AssertProperty(twoA, "foo", "hello"); + AssertProperty(twoA, "widget", "Hello, World!"); + + AssertSection(ini, "two", "subsection name", out IniSection twoB); + Assert.Equal(1, twoB.Properties.Count); + AssertProperty(twoB, "foo", "this is different"); + + AssertSection(ini, "three", out IniSection three); + Assert.Equal(4, three.Properties.Count); + AssertMultiProperty(three, "bar", "a", "b", "c"); + AssertProperty(three, "empty", ""); + + AssertSection(ini, "four", out IniSection four); + Assert.Equal(0, four.Properties.Count); + + AssertSection(ini, "five", out IniSection five); + Assert.Equal(3, five.Properties.Count); + AssertProperty(five, "prop1", "this hash # is inside quotes"); + AssertProperty(five, "prop2", "this hash # is inside quotes"); + AssertProperty(five, "prop3", " this dquoted string has three spaces around "); + } + + private static void AssertSection(IniFile file, string name, out IniSection section) + { + Assert.True(file.TryGetSection(name, out section)); + Assert.Equal(name, section.Name.Name); + Assert.Null(section.Name.SubName); + } + + private static void AssertSection(IniFile file, string name, string subName, out IniSection section) + { + Assert.True(file.TryGetSection(name, subName, out section)); + Assert.Equal(name, section.Name.Name); + Assert.Equal(subName, section.Name.SubName); + } + + private static void AssertProperty(IniSection section, string name, string value) + { + Assert.True(section.TryGetProperty(name, out var actualValue)); + Assert.Equal(value, actualValue); + } + + private static void AssertMultiProperty(IniSection section, string name, params string[] values) + { + Assert.True(section.TryGetMultiProperty(name, out IEnumerable actualValues)); + Assert.Equal(values, actualValues); + } + } +} diff --git a/src/shared/Core/IniFile.cs b/src/shared/Core/IniFile.cs new file mode 100644 index 000000000..cf438d49d --- /dev/null +++ b/src/shared/Core/IniFile.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; + +namespace GitCredentialManager +{ + public class IniFile + { + public IniFile() + { + Sections = new Dictionary(); + } + + public IDictionary Sections { get; } + + public bool TryGetSection(string name, string subName, out IniSection section) + { + return Sections.TryGetValue(new IniSectionName(name, subName), out section); + } + + public bool TryGetSection(string name, out IniSection section) + { + return Sections.TryGetValue(new IniSectionName(name), out section); + } + } + + [DebuggerDisplay("{DebuggerDisplay}")] + public readonly struct IniSectionName : IEquatable + { + public IniSectionName(string name, string subName = null) + { + Name = name; + SubName = string.IsNullOrEmpty(subName) ? null : subName; + } + + public string Name { get; } + + public string SubName { get; } + + public bool Equals(IniSectionName other) + { + // Main section name is case-insensitive, but subsection name IS case-sensitive! + return StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name) && + StringComparer.Ordinal.Equals(SubName, other.SubName); + } + + public override bool Equals(object obj) + { + return obj is IniSectionName other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return ((Name != null ? Name.ToLowerInvariant().GetHashCode() : 0) * 397) ^ + (SubName != null ? SubName.GetHashCode() : 0); + } + } + + private string DebuggerDisplay => SubName is null ? Name : $"{Name} \"{SubName}\""; + } + + [DebuggerDisplay("{DebuggerDisplay}")] + public class IniSection + { + public IniSection(IniSectionName name) + { + Name = name; + Properties = new List(); + } + + public IniSectionName Name { get; } + + public IList Properties { get; } + + public bool TryGetProperty(string name, out string value) + { + if (TryGetMultiProperty(name, out IEnumerable values)) + { + value = values.Last(); + return true; + } + + value = null; + return false; + } + + public bool TryGetMultiProperty(string name, out IEnumerable values) + { + IniProperty[] props = Properties + .Where(x => StringComparer.OrdinalIgnoreCase.Equals(x.Name, name)) + .ToArray(); + + if (props.Length == 0) + { + values = Array.Empty(); + return false; + } + + values = props.Select(x => x.Value); + return true; + } + + private string DebuggerDisplay => Name.SubName is null + ? $"{Name.Name} [Properties: {Properties.Count}]" + : $"{Name.Name} \"{Name.SubName}\" [Properties: {Properties.Count}]"; + } + + [DebuggerDisplay("{DebuggerDisplay}")] + public class IniProperty + { + public IniProperty(string name, string value) + { + Name = name; + Value = value; + } + + public string Name { get; } + public string Value { get; } + + private string DebuggerDisplay => $"{Name}={Value}"; + } + + public static class IniSerializer + { + private static readonly Regex SectionRegex = + new Regex(@"^\[[^\S#]*(?'name'[^\s#\]]*?)(?:\s+""(?'sub'.+)"")?\s*\]", RegexOptions.Compiled); + + private static readonly Regex PropertyRegex = + new Regex(@"^[^\S#]*?(?'name'[^\s#]+)\s*=(?'value'.*)?$", RegexOptions.Compiled); + + public static IniFile Deserialize(IFileSystem fs, string path) + { + IEnumerable lines = fs.ReadAllLines(path).Select(x => x.Trim()); + + var iniFile = new IniFile(); + IniSection section = null; + + foreach (string line in lines) + { + Match match = SectionRegex.Match(line); + if (match.Success) + { + string mainName = match.Groups["name"].Value; + string subName = match.Groups["sub"].Value; + + // Skip empty-named sections + if (string.IsNullOrWhiteSpace(mainName)) + { + continue; + } + + if (!iniFile.TryGetSection(mainName, subName, out section)) + { + var sectionName = new IniSectionName(mainName, subName); + section = new IniSection(sectionName); + iniFile.Sections[sectionName] = section; + } + + continue; + } + + match = PropertyRegex.Match(line); + if (match.Success) + { + if (section is null) + { + throw new Exception("Missing section header"); + } + + string propName = match.Groups["name"].Value; + string propValue = match.Groups["value"].Value.Trim(); + + // Trim trailing comments + int firstDQuote = propValue.IndexOf('"'); + int lastDQuote = propValue.LastIndexOf('"'); + int commentIdx = propValue.LastIndexOf('#'); + if (commentIdx > -1) + { + bool insideDQuotes = firstDQuote > -1 && lastDQuote > -1 && + (firstDQuote < commentIdx && commentIdx < lastDQuote); + + if (!insideDQuotes) + { + propValue = propValue.Substring(0, commentIdx).Trim(); + } + } + + // Trim book-ending double quotes: "foo" => foo + if (propValue.Length > 1 && propValue[0] == '"' && + propValue[propValue.Length - 1] == '"') + { + propValue = propValue.Substring(1, propValue.Length - 2); + } + + var property = new IniProperty(propName, propValue); + section.Properties.Add(property); + } + } + + return iniFile; + } + } +} From a0e2b9c9dbc51fec25a81c8864e1402995d28992 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 15:56:20 -0700 Subject: [PATCH 7/8] wsl: add ability to run CMD/PowerShell scripts from WSL Add ability to launch cmd.exe or PowerShell.exe scripts from inside a WSL distribution. In order to discover the location of cmd.exe/powershell.exe we need search the Windows file system that's mounted by default /mnt/c. We inspect the /etc/wsl.conf file to respect users who have changed the default mount location for Windows drives. --- src/shared/Core/WslUtils.cs | 101 ++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/shared/Core/WslUtils.cs b/src/shared/Core/WslUtils.cs index d7a1e3331..3e062a6fa 100644 --- a/src/shared/Core/WslUtils.cs +++ b/src/shared/Core/WslUtils.cs @@ -7,12 +7,21 @@ namespace GitCredentialManager { + public enum WindowsShell + { + Cmd, + PowerShell, + } + public static class WslUtils { private const string WslUncPrefix = @"\\wsl$\"; private const string WslLocalHostUncPrefix = @"\\wsl.localhost\"; private const string WslCommandName = "wsl.exe"; private const string WslInteropEnvar = "WSL_INTEROP"; + private const string WslConfFilePath = "/etc/wsl.conf"; + private const string DefaultWslMountPrefix = "/mnt"; + private const string DefaultWslSysDriveMountName = "c"; /// /// Cached WSL version. @@ -20,6 +29,11 @@ public static class WslUtils /// A value of 0 represents "not WSL", and a value less than 0 represents "unknown". private static int _wslVersion = -1; + /// + /// Cached Windows system drive mount path. + /// + private static string _sysDriveMountPath = null; + public static bool IsWslDistribution(IEnvironment env, IFileSystem fs, out int wslVersion) { if (_wslVersion < 0) @@ -113,6 +127,93 @@ public static bool IsWslPath(string path) return new ChildProcess(trace2, psi); } + /// + /// Create a command to be executed in a shell in the host Windows operating system. + /// + /// File system. + /// Shell used to execute the command in Windows. + /// Command to execute. + /// Optional working directory. + /// object ready to start. + public static Process CreateWindowsShellProcess(IFileSystem fs, + WindowsShell shell, string command, string workingDirectory = null) + { + string sysDrive = GetSystemDriveMountPath(fs); + + string launcher; + var args = new StringBuilder(); + + switch (shell) + { + case WindowsShell.Cmd: + launcher = Path.Combine(sysDrive, "Windows/cmd.exe"); + args.AppendFormat("/C {0}", command); + break; + + case WindowsShell.PowerShell: + const string psStreamSetup = + "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; " + + "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; "; + + launcher = Path.Combine(sysDrive, "Windows/System32/WindowsPowerShell/v1.0/powershell.exe"); + args.Append(" -NoProfile -NonInteractive -ExecutionPolicy Bypass"); + args.AppendFormat(" -Command \"{0} {1}\"", psStreamSetup, command); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(shell)); + } + + var psi = new ProcessStartInfo(launcher, args.ToString()) + { + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory ?? string.Empty + }; + + return new Process { StartInfo = psi }; + } + + private static string GetSystemDriveMountPath(IFileSystem fs) + { + if (_sysDriveMountPath is null) + { + string mountPrefix = DefaultWslMountPrefix; + + // If the wsl.conf file exists in this distribution the user may + // have changed the Windows volume mount point prefix. Use it! + if (fs.FileExists(WslConfFilePath)) + { + // Read wsl.conf for [automount] root = + IniFile wslConf = IniSerializer.Deserialize(fs, WslConfFilePath); + if (wslConf.TryGetSection("automount", out IniSection automountSection) && + automountSection.TryGetProperty("root", out string value)) + { + mountPrefix = value; + } + } + + // Try to locate the system volume by looking for the Windows\System32 directory + IEnumerable mountPoints = fs.EnumerateDirectories(mountPrefix); + foreach (string mountPoint in mountPoints) + { + string sys32Path = Path.Combine(mountPoint, "Windows", "System32"); + + if (fs.DirectoryExists(sys32Path)) + { + _sysDriveMountPath = mountPoint; + return _sysDriveMountPath; + } + } + + _sysDriveMountPath = Path.Combine(mountPrefix, DefaultWslSysDriveMountName); + } + + return _sysDriveMountPath; + } + public static string ConvertToDistroPath(string path, out string distribution) { if (!IsWslPath(path)) throw new ArgumentException("Must provide a WSL path", nameof(path)); From d0b767b37d6e6fa42b7c738a3729cdfc41e1c0d1 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 15:58:50 -0700 Subject: [PATCH 8/8] 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. --- src/shared/Core/BrowserUtils.cs | 46 +++++++------ src/shared/Core/CommandContext.cs | 6 +- src/shared/Core/ISessionManager.cs | 9 +++ .../Core/Interop/Linux/LinuxSessionManager.cs | 64 +++++++++++++++++++ .../Core/Interop/MacOS/MacOSSessionManager.cs | 2 +- .../Core/Interop/Posix/PosixSessionManager.cs | 10 ++- .../Interop/Windows/WindowsSessionManager.cs | 2 +- src/shared/Core/ProcessManager.cs | 1 - src/shared/Core/WslUtils.cs | 36 +++++++++++ 9 files changed, 144 insertions(+), 32 deletions(-) create mode 100644 src/shared/Core/Interop/Linux/LinuxSessionManager.cs diff --git a/src/shared/Core/BrowserUtils.cs b/src/shared/Core/BrowserUtils.cs index c34164ef0..6e908d1fc 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 d051b2ef2..304bdb683 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 WindowsProcessManager(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 ProcessManager(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 165393153..2569833f4 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 000000000..2147289ac --- /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 febcd20c2..584965ca1 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 b9624e69a..8709e12e7 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 464f2c3fd..d87d76347 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 78b0e2c9c..2e1544397 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 3e062a6fa..1db63d329 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)