mirror of
https://github.com/actions/runner.git
synced 2026-07-06 12:50:52 +08:00
Compare commits
4 Commits
main
...
fix/normal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0e5b75ee | ||
|
|
fffded93ac | ||
|
|
8307b8fe33 | ||
|
|
7585eb30aa |
@@ -64,6 +64,7 @@ namespace GitHub.Runner.Common
|
|||||||
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
|
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
|
||||||
private CancellationTokenSource _runnerShutdownTokenSource = new();
|
private CancellationTokenSource _runnerShutdownTokenSource = new();
|
||||||
private object _perfLock = new();
|
private object _perfLock = new();
|
||||||
|
private string _canonicalRootDirectory;
|
||||||
private Tracing _trace;
|
private Tracing _trace;
|
||||||
private Tracing _actionsHttpTrace;
|
private Tracing _actionsHttpTrace;
|
||||||
private Tracing _netcoreHttpTrace;
|
private Tracing _netcoreHttpTrace;
|
||||||
@@ -391,7 +392,12 @@ namespace GitHub.Runner.Common
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownDirectory.Root:
|
case WellKnownDirectory.Root:
|
||||||
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
|
if (_canonicalRootDirectory == null)
|
||||||
|
{
|
||||||
|
_canonicalRootDirectory = PathUtil.GetCanonicalPath(
|
||||||
|
new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName);
|
||||||
|
}
|
||||||
|
path = _canonicalRootDirectory;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownDirectory.Temp:
|
case WellKnownDirectory.Temp:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace GitHub.Runner.Sdk
|
namespace GitHub.Runner.Sdk
|
||||||
{
|
{
|
||||||
@@ -6,8 +9,98 @@ namespace GitHub.Runner.Sdk
|
|||||||
{
|
{
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
public static readonly string PathVariable = "Path";
|
public static readonly string PathVariable = "Path";
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
private static extern SafeFileHandle CreateFile(
|
||||||
|
string lpFileName,
|
||||||
|
uint dwDesiredAccess,
|
||||||
|
uint dwShareMode,
|
||||||
|
System.IntPtr lpSecurityAttributes,
|
||||||
|
uint dwCreationDisposition,
|
||||||
|
uint dwFlagsAndAttributes,
|
||||||
|
System.IntPtr hTemplateFile);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
private static extern uint GetFinalPathNameByHandle(
|
||||||
|
SafeFileHandle hFile,
|
||||||
|
[Out] StringBuilder lpszFilePath,
|
||||||
|
uint cchFilePath,
|
||||||
|
uint dwFlags);
|
||||||
|
|
||||||
|
private const uint FILE_READ_ATTRIBUTES = 0x80;
|
||||||
|
private const uint FILE_SHARE_READ = 0x1;
|
||||||
|
private const uint FILE_SHARE_WRITE = 0x2;
|
||||||
|
private const uint FILE_SHARE_DELETE = 0x4;
|
||||||
|
private const uint OPEN_EXISTING = 3;
|
||||||
|
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
||||||
|
private const uint VOLUME_NAME_DOS = 0x0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the NTFS canonical path for a directory, resolving drive letter
|
||||||
|
/// and folder name casing to match what is stored on disk.
|
||||||
|
/// On non-Windows platforms, returns the path unchanged.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetCanonicalPath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var handle = CreateFile(
|
||||||
|
path,
|
||||||
|
FILE_READ_ATTRIBUTES,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
System.IntPtr.Zero,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_FLAG_BACKUP_SEMANTICS,
|
||||||
|
System.IntPtr.Zero);
|
||||||
|
|
||||||
|
if (handle.IsInvalid)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new StringBuilder(1024);
|
||||||
|
var result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry with a larger buffer if the path was longer than expected
|
||||||
|
if (result >= buffer.Capacity)
|
||||||
|
{
|
||||||
|
buffer = new StringBuilder((int)result + 1);
|
||||||
|
result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
|
||||||
|
if (result == 0 || result >= buffer.Capacity)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonicalPath = buffer.ToString();
|
||||||
|
|
||||||
|
// Strip the \\?\UNC\ prefix and convert to standard UNC path
|
||||||
|
if (canonicalPath.StartsWith(@"\\?\UNC\", System.StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
canonicalPath = @"\\" + canonicalPath.Substring(8);
|
||||||
|
}
|
||||||
|
// Strip the \\?\ prefix for local paths
|
||||||
|
else if (canonicalPath.StartsWith(@"\\?\", System.StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
canonicalPath = canonicalPath.Substring(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canonicalPath;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
public static readonly string PathVariable = "PATH";
|
public static readonly string PathVariable = "PATH";
|
||||||
|
|
||||||
|
public static string GetCanonicalPath(string path)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public static string PrependPath(string path, string currentPath)
|
public static string PrependPath(string path, string currentPath)
|
||||||
|
|||||||
@@ -299,6 +299,52 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetDirectoryRootReturnsCachedValue()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
// Call GetDirectory(Root) twice — should return the same reference
|
||||||
|
var root1 = _hc.GetDirectory(WellKnownDirectory.Root);
|
||||||
|
var root2 = _hc.GetDirectory(WellKnownDirectory.Root);
|
||||||
|
|
||||||
|
Assert.NotNull(root1);
|
||||||
|
Assert.Equal(root1, root2);
|
||||||
|
Assert.True(Directory.Exists(root1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetDirectoryDerivedPathsUseRootCasing()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
var root = _hc.GetDirectory(WellKnownDirectory.Root);
|
||||||
|
var diag = _hc.GetDirectory(WellKnownDirectory.Diag);
|
||||||
|
var externals = _hc.GetDirectory(WellKnownDirectory.Externals);
|
||||||
|
|
||||||
|
// Diag and Externals should start with the same Root prefix
|
||||||
|
Assert.StartsWith(root, diag);
|
||||||
|
Assert.StartsWith(root, externals);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Setup([CallerMemberName] string testName = "")
|
private void Setup([CallerMemberName] string testName = "")
|
||||||
{
|
{
|
||||||
_tokenSource = new CancellationTokenSource();
|
_tokenSource = new CancellationTokenSource();
|
||||||
|
|||||||
151
src/Test/L0/Util/PathUtilL0.cs
Normal file
151
src/Test/L0/Util/PathUtilL0.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Tests.Util
|
||||||
|
{
|
||||||
|
public sealed class PathUtilL0
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsPath_WhenDirectoryDoesNotExist()
|
||||||
|
{
|
||||||
|
var fakePath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Path.GetRandomFileName());
|
||||||
|
var result = PathUtil.GetCanonicalPath(fakePath);
|
||||||
|
Assert.Equal(fakePath, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsPath_WhenNull()
|
||||||
|
{
|
||||||
|
Assert.Null(PathUtil.GetCanonicalPath(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsEmpty_WhenEmpty()
|
||||||
|
{
|
||||||
|
Assert.Equal(string.Empty, PathUtil.GetCanonicalPath(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsValidPath_ForExistingDirectory()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "pathutil_test_" + Path.GetRandomFileName());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
var result = PathUtil.GetCanonicalPath(tempDir);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(Directory.Exists(result));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OS_WINDOWS
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_NormalizesDriveLetter_OnWindows()
|
||||||
|
{
|
||||||
|
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
// Skip if temp is a UNC path (no drive letter to normalize)
|
||||||
|
if (tempDir.StartsWith(@"\\"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force lowercase drive letter
|
||||||
|
var lowerCased = char.ToLower(tempDir[0]) + tempDir.Substring(1);
|
||||||
|
|
||||||
|
var result = PathUtil.GetCanonicalPath(lowerCased);
|
||||||
|
|
||||||
|
// The canonical path should have an uppercase drive letter
|
||||||
|
Assert.True(char.IsUpper(result[0]),
|
||||||
|
$"Expected uppercase drive letter but got: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_NormalizesFolderCasing_OnWindows()
|
||||||
|
{
|
||||||
|
// Create a directory with known casing, then query with wrong casing
|
||||||
|
var basePath = Path.GetTempPath();
|
||||||
|
if (basePath.StartsWith(@"\\"))
|
||||||
|
{
|
||||||
|
return; // Skip UNC
|
||||||
|
}
|
||||||
|
|
||||||
|
var realName = "PathUtilTest_MiXeDcAsE_" + Path.GetRandomFileName();
|
||||||
|
var realDir = Path.Combine(basePath, realName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(realDir);
|
||||||
|
|
||||||
|
// Query with all-lowercase version
|
||||||
|
var wrongCased = Path.Combine(basePath, realName.ToLowerInvariant());
|
||||||
|
|
||||||
|
var result = PathUtil.GetCanonicalPath(wrongCased);
|
||||||
|
|
||||||
|
// The canonical result should contain the original mixed-case name
|
||||||
|
Assert.Contains(realName, result);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(realDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(realDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_IsIdempotent_OnWindows()
|
||||||
|
{
|
||||||
|
// Calling GetCanonicalPath twice should return the same result
|
||||||
|
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
var first = PathUtil.GetCanonicalPath(tempDir);
|
||||||
|
var second = PathUtil.GetCanonicalPath(first);
|
||||||
|
Assert.Equal(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsSameResult_RegardlessOfInputCasing_OnWindows()
|
||||||
|
{
|
||||||
|
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
if (tempDir.StartsWith(@"\\"))
|
||||||
|
{
|
||||||
|
return; // Skip UNC
|
||||||
|
}
|
||||||
|
|
||||||
|
var upper = tempDir.ToUpperInvariant();
|
||||||
|
var lower = tempDir.ToLowerInvariant();
|
||||||
|
|
||||||
|
var resultUpper = PathUtil.GetCanonicalPath(upper);
|
||||||
|
var resultLower = PathUtil.GetCanonicalPath(lower);
|
||||||
|
|
||||||
|
// Both should resolve to the same canonical path
|
||||||
|
Assert.Equal(resultUpper, resultLower);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user