Skip to content

Commit

Permalink
wsl: support calling Git inside of WSL
Browse files Browse the repository at this point in the history
Add support for calling back to the Git executable in a Windows
Subsystem for Linux (WSL).
  • Loading branch information
mjcheetham committed Sep 21, 2021
1 parent 98551cd commit 77ca2d4
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 2 deletions.
111 changes: 111 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager.Tests/WslUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System;
using System.Diagnostics;
using Xunit;

namespace Microsoft.Git.CredentialManager.Tests
{
public class WslUtilsTests
{
[PlatformTheory(Platforms.Windows)]
[InlineData(null, false)]
[InlineData(@"", false)]
[InlineData(@" ", false)]
[InlineData(@"\", false)]
[InlineData(@"wsl", false)]
[InlineData(@"\wsl\ubuntu\home", false)]
[InlineData(@"\\wsl\ubuntu\home", false)]
[InlineData(@"\wsl$\ubuntu\home", false)]
[InlineData(@"wsl$\ubuntu\home", false)]
[InlineData(@"//wsl$/ubuntu/home", false)]
[InlineData(@"\\wsl", false)]
[InlineData(@"\\wsl$", false)]
[InlineData(@"\\wsl$\", false)]
[InlineData(@"\\wsl$\ubuntu", true)]
[InlineData(@"\\wsl$\ubuntu\", true)]
[InlineData(@"\\wsl$\ubuntu\home", true)]
[InlineData(@"\\WSL$\UBUNTU\home", true)]
[InlineData(@"\\wsl$\ubuntu\home\", true)]
[InlineData(@"\\wsl$\openSUSE-42\home", true)]
public void WslUtils_IsWslPath(string path, bool expected)
{
bool actual = WslUtils.IsWslPath(path);
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(@"\\wsl$\ubuntu", "ubuntu", "/")]
[InlineData(@"\\wsl$\ubuntu\", "ubuntu", "/")]
[InlineData(@"\\wsl$\ubuntu\home", "ubuntu", "/home")]
[InlineData(@"\\wsl$\ubuntu\HOME", "ubuntu", "/HOME")]
[InlineData(@"\\wsl$\UBUNTU\home", "UBUNTU", "/home")]
[InlineData(@"\\wsl$\ubuntu\home\", "ubuntu", "/home/")]
[InlineData(@"\\wsl$\openSUSE-42\home", "openSUSE-42", "/home")]
public void WslUtils_ConvertToDistroPath(string path, string expectedDistro, string expectedPath)
{
string actualPath = WslUtils.ConvertToDistroPath(path, out string actualDistro);
Assert.Equal(expectedPath, actualPath);
Assert.Equal(expectedDistro, actualDistro);
}

[PlatformTheory(Platforms.Windows)]
[InlineData(null)]
[InlineData(@"")]
[InlineData(@" ")]
[InlineData(@"\")]
[InlineData(@"wsl")]
[InlineData(@"\wsl\ubuntu\home")]
[InlineData(@"\\wsl\ubuntu\home")]
[InlineData(@"\wsl$\ubuntu\home")]
[InlineData(@"wsl$\ubuntu\home")]
[InlineData(@"//wsl$/ubuntu/home")]
[InlineData(@"\\wsl")]
[InlineData(@"\\wsl$")]
[InlineData(@"\\wsl$\")]
public void WslUtils_ConvertToDistroPath_Invalid_ThrowsException(string path)
{
Assert.Throws<ArgumentException>(() => WslUtils.ConvertToDistroPath(path, out _));
}

[PlatformFact(Platforms.Windows)]
public void WslUtils_CreateWslProcess()
{
const string distribution = "ubuntu";
const string command = "/usr/lib/git-core/git version";

string expectedFileName = WslUtils.GetWslPath();
string expectedArgs = $"--distribution {distribution} --exec {command}";

Process process = WslUtils.CreateWslProcess(distribution, command);

Assert.NotNull(process);
Assert.Equal(expectedArgs, process.StartInfo.Arguments);
Assert.Equal(expectedFileName, process.StartInfo.FileName);
Assert.True(process.StartInfo.RedirectStandardInput);
Assert.True(process.StartInfo.RedirectStandardOutput);
Assert.True(process.StartInfo.RedirectStandardError);
Assert.False(process.StartInfo.UseShellExecute);
}

[PlatformFact(Platforms.Windows)]
public void WslUtils_CreateWslProcess_WorkingDirectory()
{
const string distribution = "ubuntu";
const string command = "/usr/lib/git-core/git version";
const string expectedWorkingDirectory = @"C:\Projects\";

string expectedFileName = WslUtils.GetWslPath();
string expectedArgs = $"--distribution {distribution} --exec {command}";

Process process = WslUtils.CreateWslProcess(distribution, command, expectedWorkingDirectory);

Assert.NotNull(process);
Assert.Equal(expectedArgs, process.StartInfo.Arguments);
Assert.Equal(expectedFileName, process.StartInfo.FileName);
Assert.True(process.StartInfo.RedirectStandardInput);
Assert.True(process.StartInfo.RedirectStandardOutput);
Assert.True(process.StartInfo.RedirectStandardError);
Assert.False(process.StartInfo.UseShellExecute);
Assert.Equal(expectedWorkingDirectory, process.StartInfo.WorkingDirectory);
}
}
}
11 changes: 10 additions & 1 deletion src/shared/Microsoft.Git.CredentialManager/CommandContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,22 @@ public CommandContext(string appPath)

private static string GetGitPath(IEnvironment environment, IFileSystem fileSystem, ITrace trace)
{
const string unixGitName = "git";
const string winGitName = "git.exe";

string gitExecPath;
string programName = PlatformUtils.IsWindows() ? "git.exe" : "git";
string programName = PlatformUtils.IsWindows() ? winGitName : unixGitName;

// Use the GIT_EXEC_PATH environment variable if set
if (environment.Variables.TryGetValue(Constants.EnvironmentVariables.GitExecutablePath,
out gitExecPath))
{
// If we're invoked from WSL we must locate the UNIX Git executable
if (PlatformUtils.IsWindows() && WslUtils.IsWslPath(gitExecPath))
{
programName = unixGitName;
}

string candidatePath = Path.Combine(gitExecPath, programName);
if (fileSystem.FileExists(candidatePath))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace Microsoft.Git.CredentialManager.Interop.Windows
Expand Down Expand Up @@ -90,6 +89,18 @@ public override bool TryLocateExecutable(string program, out string path)
return false;
}

public override Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
{
// If we're asked to start a WSL executable we must launch via the wsl.exe command tool
if (!useShellExecute && WslUtils.IsWslPath(path))
{
string wslPath = WslUtils.ConvertToDistroPath(path, out string distro);
return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", workingDirectory);
}

return base.CreateProcess(path, args, useShellExecute, workingDirectory);
}

#endregion

private static IReadOnlyDictionary<string, string> GetCurrentVariables()
Expand Down
101 changes: 101 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager/WslUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Microsoft.Git.CredentialManager
{
public static class WslUtils
{
private const string WslUncPrefix = @"\\wsl$\";
private const string WslCommandName = "wsl.exe";

/// <summary>
/// Test if a file path points to a location in a Windows Subsystem for Linux distribution.
/// </summary>
/// <param name="path">Path to test.</param>
/// <returns>True if <paramref name="path"/> is a WSL path, false otherwise.</returns>
public static bool IsWslPath(string path)
{
if (string.IsNullOrWhiteSpace(path)) return false;

return path.StartsWith(WslUncPrefix, StringComparison.OrdinalIgnoreCase) &&
path.Length > WslUncPrefix.Length;
}

/// <summary>
/// Create a command to be executed in a Windows Subsystem for Linux distribution.
/// </summary>
/// <param name="distribution">WSL distribution name.</param>
/// <param name="command">Command to execute.</param>
/// <param name="workingDirectory">Optional working directory.</param>
/// <returns><see cref="Process"/> object ready to start.</returns>
public static Process CreateWslProcess(string distribution, string command, string workingDirectory = null)
{
var args = new StringBuilder();
args.AppendFormat("--distribution {0} ", distribution);
args.AppendFormat("--exec {0}", command);

string wslExePath = GetWslPath();

var psi = new ProcessStartInfo(wslExePath, args.ToString())
{
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
WorkingDirectory = workingDirectory ?? string.Empty
};

return new Process { StartInfo = psi };
}

public static string ConvertToDistroPath(string path, out string distribution)
{
if (!IsWslPath(path)) throw new ArgumentException("Must provide a WSL path", nameof(path));

int distroStart = WslUncPrefix.Length;
int distroEnd = path.IndexOf('\\', distroStart);

if (distroEnd < 0) distroEnd = path.Length;

distribution = path.Substring(distroStart, distroEnd - distroStart);

if (path.Length > distroEnd)
{
return path.Substring(distroEnd).Replace('\\', '/');
}

return "/";
}

internal /*for testing purposes*/ static string GetWslPath()
{
// WSL is only supported on 64-bit operating systems
if (!Environment.Is64BitOperatingSystem)
{
throw new Exception("WSL is not supported on 32-bit operating systems");
}

//
// When running as a 32-bit application on a 64-bit operating system, we cannot access the real
// C:\Windows\System32 directory because the OS will redirect us transparently to the
// C:\Windows\SysWOW64 directory (containing 32-bit executables).
//
// In order to access the real 64-bit System32 directory, we must access via the pseudo directory
// C:\Windows\SysNative that does **not** experience any redirection for 32-bit applications.
//
// HOWEVER, if we're running as a 64-bit application on a 64-bit operating system, the SysNative
// directory does not exist! This means if running as a 32-bit application on a 64-bit OS we must
// use the System32 directory name directly.
//
var sysDir = Environment.ExpandEnvironmentVariables(
Environment.Is64BitProcess
? @"%WINDIR%\System32"
: @"%WINDIR%\SysNative"
);

return Path.Combine(sysDir, WslCommandName);
}
}
}

0 comments on commit 77ca2d4

Please sign in to comment.