diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index e374f36740e..fb4d79951a7 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -69,6 +69,8 @@ public static class Runner public static readonly OSPlatform Platform = OSPlatform.OSX; #elif OS_WINDOWS public static readonly OSPlatform Platform = OSPlatform.Windows; +#else + public static readonly OSPlatform Platform = OSPlatform.Linux; #endif #if X86 @@ -79,6 +81,8 @@ public static class Runner public static readonly Architecture PlatformArchitecture = Architecture.Arm; #elif ARM64 public static readonly Architecture PlatformArchitecture = Architecture.Arm64; +#else + public static readonly Architecture PlatformArchitecture = Architecture.X64; #endif public static readonly TimeSpan ExitOnUnloadTimeout = TimeSpan.FromSeconds(30); diff --git a/src/Runner.Sdk/Util/WhichUtil.cs b/src/Runner.Sdk/Util/WhichUtil.cs index 3c1ab3268b8..fde4fa2f6a9 100644 --- a/src/Runner.Sdk/Util/WhichUtil.cs +++ b/src/Runner.Sdk/Util/WhichUtil.cs @@ -114,6 +114,128 @@ public static string Which(string command, bool require = false, ITraceWriter tr } } +#if OS_WINDOWS + trace?.Info($"{command}: command not found. Make sure '{command}' is installed and its location included in the 'Path' environment variable."); +#else + trace?.Info($"{command}: command not found. Make sure '{command}' is installed and its location included in the 'PATH' environment variable."); +#endif + if (require) + { + throw new FileNotFoundException( + message: $"{command}: command not found", + fileName: command); + } + + return null; + } + + public static string Which2(string command, bool require = false, ITraceWriter trace = null, string prependPath = null) + { + ArgUtil.NotNullOrEmpty(command, nameof(command)); + trace?.Info($"Which2: '{command}'"); + if (Path.IsPathFullyQualified(command) && File.Exists(command)) + { + trace?.Info($"Fully qualified path: '{command}'"); + return command; + } + string path = Environment.GetEnvironmentVariable(PathUtil.PathVariable); + if (string.IsNullOrEmpty(path)) + { + trace?.Info("PATH environment variable not defined."); + path = path ?? string.Empty; + } + if (!string.IsNullOrEmpty(prependPath)) + { + path = PathUtil.PrependPath(prependPath, path); + } + + string[] pathSegments = path.Split(new Char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < pathSegments.Length; i++) + { + pathSegments[i] = Environment.ExpandEnvironmentVariables(pathSegments[i]); + } + + foreach (string pathSegment in pathSegments) + { + if (!string.IsNullOrEmpty(pathSegment) && Directory.Exists(pathSegment)) + { +#if OS_WINDOWS + string pathExt = Environment.GetEnvironmentVariable("PATHEXT"); + if (string.IsNullOrEmpty(pathExt)) + { + // XP's system default value for PATHEXT system variable + pathExt = ".com;.exe;.bat;.cmd;.vbs;.vbe;.js;.jse;.wsf;.wsh"; + } + + string[] pathExtSegments = pathExt.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries); + + // if command already has an extension. + if (pathExtSegments.Any(ext => command.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) + { + try + { + foreach (var file in Directory.EnumerateFiles(pathSegment, command)) + { + if (IsPathValid(file, trace)) + { + trace?.Info($"Location: '{file}'"); + return file; + } + } + } + catch (UnauthorizedAccessException ex) + { + trace?.Info("Ignore UnauthorizedAccess exception during Which."); + trace?.Verbose(ex.ToString()); + } + } + else + { + string searchPattern; + searchPattern = StringUtil.Format($"{command}.*"); + try + { + foreach (var file in Directory.EnumerateFiles(pathSegment, searchPattern)) + { + // add extension. + for (int i = 0; i < pathExtSegments.Length; i++) + { + string fullPath = Path.Combine(pathSegment, $"{command}{pathExtSegments[i]}"); + if (string.Equals(file, fullPath, StringComparison.OrdinalIgnoreCase) && IsPathValid(fullPath, trace)) + { + trace?.Info($"Location: '{fullPath}'"); + return fullPath; + } + } + } + } + catch (UnauthorizedAccessException ex) + { + trace?.Info("Ignore UnauthorizedAccess exception during Which."); + trace?.Verbose(ex.ToString()); + } + } +#else + try + { + foreach (var file in Directory.EnumerateFiles(pathSegment, command)) + { + if (IsPathValid(file, trace)) + { + trace?.Info($"Location: '{file}'"); + return file; + } + } + } + catch (UnauthorizedAccessException ex) + { + trace?.Info("Ignore UnauthorizedAccess exception during Which."); + trace?.Verbose(ex.ToString()); + } +#endif + } + } + #if OS_WINDOWS trace?.Info($"{command}: command not found. Make sure '{command}' is installed and its location included in the 'Path' environment variable."); #else @@ -134,7 +256,12 @@ private static bool IsPathValid(string path, ITraceWriter trace = null) { var fileInfo = new FileInfo(path); var linkTargetFullPath = fileInfo.Directory?.FullName + Path.DirectorySeparatorChar + fileInfo.LinkTarget; - if (fileInfo.LinkTarget == null || File.Exists(linkTargetFullPath) || File.Exists(fileInfo.LinkTarget)) return true; + if (fileInfo.LinkTarget == null || + File.Exists(linkTargetFullPath) || + File.Exists(fileInfo.LinkTarget)) + { + return true; + } trace?.Info($"the target '{fileInfo.LinkTarget}' of the symbolic link '{path}', does not exist"); return false; } diff --git a/src/Runner.Worker/Handlers/ScriptHandler.cs b/src/Runner.Worker/Handlers/ScriptHandler.cs index 57f61e9c4be..30114f27c33 100644 --- a/src/Runner.Worker/Handlers/ScriptHandler.cs +++ b/src/Runner.Worker/Handlers/ScriptHandler.cs @@ -83,19 +83,40 @@ protected override void PrintActionDetails(ActionRunStage stage) shellCommand = "pwsh"; if (validateShellOnHost) { - shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + shellCommandPath = WhichUtil.Which2(shellCommand, require: false, Trace, prependPath); + } + else + { + shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); + } if (string.IsNullOrEmpty(shellCommandPath)) { shellCommand = "powershell"; Trace.Info($"Defaulting to {shellCommand}"); - shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + shellCommandPath = WhichUtil.Which2(shellCommand, require: true, Trace, prependPath); + } + else + { + shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath); + } } } #else shellCommand = "sh"; if (validateShellOnHost) { - shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + shellCommandPath = WhichUtil.Which2("bash", false, Trace, prependPath) ?? WhichUtil.Which2("sh", true, Trace, prependPath); + } + else + { + shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath); + } } #endif argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand); @@ -106,7 +127,14 @@ protected override void PrintActionDetails(ActionRunStage stage) shellCommand = parsed.shellCommand; if (validateShellOnHost) { - shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + shellCommandPath = WhichUtil.Which2(parsed.shellCommand, true, Trace, prependPath); + } + else + { + shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath); + } } argFormat = $"{parsed.shellArgs}".TrimStart(); @@ -188,17 +216,38 @@ public async Task RunAsync(ActionRunStage stage) { #if OS_WINDOWS shellCommand = "pwsh"; - commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + commandPath = WhichUtil.Which2(shellCommand, require: false, Trace, prependPath); + } + else + { + commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); + } if (string.IsNullOrEmpty(commandPath)) { shellCommand = "powershell"; Trace.Info($"Defaulting to {shellCommand}"); - commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + commandPath = WhichUtil.Which2(shellCommand, require: true, Trace, prependPath); + } + else + { + commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath); + } } ArgUtil.NotNullOrEmpty(commandPath, "Default Shell"); #else shellCommand = "sh"; - commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + commandPath = WhichUtil.Which2("bash", false, Trace, prependPath) ?? WhichUtil.Which2("sh", true, Trace, prependPath); + } + else + { + commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath); + } #endif argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand); } @@ -209,7 +258,14 @@ public async Task RunAsync(ActionRunStage stage) if (!IsActionStep && systemShells.Contains(shell)) { shellCommand = shell; - commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + commandPath = WhichUtil.Which2(shell, !isContainerStepHost, Trace, prependPath); + } + else + { + commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath); + } if (shell == "bash") { argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat("sh"); @@ -224,7 +280,14 @@ public async Task RunAsync(ActionRunStage stage) var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell); shellCommand = parsed.shellCommand; // For non-ContainerStepHost, the command must be located on the host by Which - commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath); + if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true) + { + commandPath = WhichUtil.Which2(parsed.shellCommand, !isContainerStepHost, Trace, prependPath); + } + else + { + commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath); + } argFormat = $"{parsed.shellArgs}".TrimStart(); if (string.IsNullOrEmpty(argFormat)) { diff --git a/src/Test/L0/Util/WhichUtilL0.cs b/src/Test/L0/Util/WhichUtilL0.cs index 9a6443d1fff..90d32c466a6 100644 --- a/src/Test/L0/Util/WhichUtilL0.cs +++ b/src/Test/L0/Util/WhichUtilL0.cs @@ -212,5 +212,210 @@ public void WhichThrowsWhenSymlinkBroken() File.Delete(brokenSymlink); Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue); } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void UseWhich2FindGit() + { + using (TestHostContext hc = new(this)) + { + //Arrange + Tracing trace = hc.GetTrace(); + + // Act. + string gitPath = WhichUtil.Which2("git", trace: trace); + + trace.Info($"Which(\"git\") returns: {gitPath ?? string.Empty}"); + + // Assert. + Assert.True(!string.IsNullOrEmpty(gitPath) && File.Exists(gitPath), $"Unable to find Git through: {nameof(WhichUtil.Which)}"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Which2ReturnsNullWhenNotFound() + { + using (TestHostContext hc = new(this)) + { + //Arrange + Tracing trace = hc.GetTrace(); + + // Act. + string nosuch = WhichUtil.Which2("no-such-file-cf7e351f", trace: trace); + + trace.Info($"result: {nosuch ?? string.Empty}"); + + // Assert. + Assert.True(string.IsNullOrEmpty(nosuch), "Path should not be resolved"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Which2ThrowsWhenRequireAndNotFound() + { + using (TestHostContext hc = new(this)) + { + //Arrange + Tracing trace = hc.GetTrace(); + + // Act. + try + { + WhichUtil.Which2("no-such-file-cf7e351f", require: true, trace: trace); + throw new Exception("which should have thrown"); + } + catch (FileNotFoundException ex) + { + Assert.Equal("no-such-file-cf7e351f", ex.FileName); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Which2HandleFullyQualifiedPath() + { + using (TestHostContext hc = new(this)) + { + //Arrange + Tracing trace = hc.GetTrace(); + + // Act. + var gitPath = WhichUtil.Which2("git", require: true, trace: trace); + var gitPath2 = WhichUtil.Which2(gitPath, require: true, trace: trace); + + // Assert. + Assert.Equal(gitPath, gitPath2); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Which2HandlesSymlinkToTargetFullPath() + { + // Arrange + using TestHostContext hc = new TestHostContext(this); + Tracing trace = hc.GetTrace(); + string oldValue = Environment.GetEnvironmentVariable(PathUtil.PathVariable); +#if OS_WINDOWS + string newValue = oldValue + @$";{Path.GetTempPath()}"; + string symlinkName = $"symlink-{Guid.NewGuid()}"; + string symlink = Path.GetTempPath() + $"{symlinkName}.exe"; + string target = Path.GetTempPath() + $"target-{Guid.NewGuid()}.exe"; +#else + string newValue = oldValue + @$":{Path.GetTempPath()}"; + string symlinkName = $"symlink-{Guid.NewGuid()}"; + string symlink = Path.GetTempPath() + $"{symlinkName}"; + string target = Path.GetTempPath() + $"target-{Guid.NewGuid()}"; +#endif + + Environment.SetEnvironmentVariable(PathUtil.PathVariable, newValue); + + + using (File.Create(target)) + { + File.CreateSymbolicLink(symlink, target); + + // Act. + var result = WhichUtil.Which2(symlinkName, require: true, trace: trace); + + // Assert + Assert.True(!string.IsNullOrEmpty(result) && File.Exists(result), $"Unable to find symlink through: {nameof(WhichUtil.Which)}"); + + } + + + // Cleanup + File.Delete(symlink); + File.Delete(target); + Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue); + + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Which2HandlesSymlinkToTargetRelativePath() + { + // Arrange + using TestHostContext hc = new TestHostContext(this); + Tracing trace = hc.GetTrace(); + string oldValue = Environment.GetEnvironmentVariable(PathUtil.PathVariable); +#if OS_WINDOWS + string newValue = oldValue + @$";{Path.GetTempPath()}"; + string symlinkName = $"symlink-{Guid.NewGuid()}"; + string symlink = Path.GetTempPath() + $"{symlinkName}.exe"; + string targetName = $"target-{Guid.NewGuid()}.exe"; + string target = Path.GetTempPath() + targetName; +#else + string newValue = oldValue + @$":{Path.GetTempPath()}"; + string symlinkName = $"symlink-{Guid.NewGuid()}"; + string symlink = Path.GetTempPath() + $"{symlinkName}"; + string targetName = $"target-{Guid.NewGuid()}"; + string target = Path.GetTempPath() + targetName; +#endif + Environment.SetEnvironmentVariable(PathUtil.PathVariable, newValue); + + + using (File.Create(target)) + { + File.CreateSymbolicLink(symlink, targetName); + + // Act. + var result = WhichUtil.Which2(symlinkName, require: true, trace: trace); + + // Assert + Assert.True(!string.IsNullOrEmpty(result) && File.Exists(result), $"Unable to find {symlinkName} through: {nameof(WhichUtil.Which)}"); + } + + // Cleanup + File.Delete(symlink); + File.Delete(target); + Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue); + + } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void Which2ThrowsWhenSymlinkBroken() + { + // Arrange + using TestHostContext hc = new TestHostContext(this); + Tracing trace = hc.GetTrace(); + string oldValue = Environment.GetEnvironmentVariable(PathUtil.PathVariable); + +#if OS_WINDOWS + string newValue = oldValue + @$";{Path.GetTempPath()}"; + string brokenSymlinkName = $"broken-symlink-{Guid.NewGuid()}"; + string brokenSymlink = Path.GetTempPath() + $"{brokenSymlinkName}.exe"; +#else + string newValue = oldValue + @$":{Path.GetTempPath()}"; + string brokenSymlinkName = $"broken-symlink-{Guid.NewGuid()}"; + string brokenSymlink = Path.GetTempPath() + $"{brokenSymlinkName}"; +#endif + + + string target = "no-such-file-cf7e351f"; + Environment.SetEnvironmentVariable(PathUtil.PathVariable, newValue); + + File.CreateSymbolicLink(brokenSymlink, target); + + // Act. + var exception = Assert.Throws(() => WhichUtil.Which2(brokenSymlinkName, require: true, trace: trace)); + + // Assert + Assert.Equal(brokenSymlinkName, exception.FileName); + + // Cleanup + File.Delete(brokenSymlink); + Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue); + } } }