using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using CliWrap; using CliWrap.Buffered; namespace Centron.Scripts { public static class RunHelper { private static string _msBuildPath; private static string MsBuildPath { get { if (_msBuildPath == null) FillPaths(); return _msBuildPath; } } private static void FillPaths() { var vsWhere = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft Visual Studio", "Installer", "vswhere.exe"); var output = Read(vsWhere, "-products * -requires Microsoft.Component.MSBuild -property installationPath -prerelease"); var paths = output.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (paths.Length == 0) throw new Exception("Visual Studio with MSBuild component not found. Please complete the Visual Studio installation and ensure the 'Desktop development with C++' or '.NET desktop development' workload is installed."); var path = paths.First(); var msbuildPath = Path.Combine(path, "MSBuild", "Current", "Bin", "MSBuild.exe"); if (File.Exists(msbuildPath) == false) throw new Exception($"msbuild.exe not found at {msbuildPath}. Is Visual Studio properly installed?"); _msBuildPath = msbuildPath; } public static void RunDotNet(string arguments, TimeSpan? timeout = null) { Run("dotnet", arguments, timeout: timeout); } public static void RunDotNetTest(string arguments, TimeSpan? timeout = null) { try { Run("dotnet", "test " + arguments, timeout: timeout); } catch (Exception e) when (e.Data.Contains("ExitCode") && (int)e.Data["ExitCode"] == 1) { //The test run failed, but we don't want to error //Instead return and allow the rest of the process to read the TestResults file } } public static void RunMsBuild(string arguments, TimeSpan? timeout = null) { try { Run(MsBuildPath, arguments, timeout: timeout); } catch (Exception e) when (e.Data.Contains("ExitCode") && EnvironmentHelper.RunningInAzurePipelines()) { ParseAndOutputDiagnosticsForAzurePipelines(e.Message); throw new Exception($"MSBuild.exe failed with command: {arguments}.", e); } } private static void ParseAndOutputDiagnosticsForAzurePipelines(string message) { string GetValue(Match match, string name) => match.Groups.Cast().First(f => f.Name == name).Value.Replace("[", string.Empty).Replace("]", string.Empty); var diagnosticsRegex = new Regex(@"^\s*[\d>]*(?.*)\((?\d*),(?\d*)\): (?error|warning) (?.*): (?.*)\[.*$", RegexOptions.Multiline); var diagnostics = diagnosticsRegex .Matches(message) .Cast() .Select(f => new { Type = GetValue(f, "Type"), FileName = GetValue(f, "FileName"), LineNumber = GetValue(f, "LineNumber"), ColumnNumber = GetValue(f, "ColumnNumber"), Code = GetValue(f, "Code"), Message = GetValue(f, "Message"), }) .Where(f => f.Type == "error") //For now, only output errors .Distinct() .ToList(); foreach (var diagnostic in diagnostics) { Console.WriteLine($"##vso[task.logissue type=error;sourcepath={diagnostic.FileName};linenumber={diagnostic.LineNumber};columnnumber={diagnostic.ColumnNumber};code={diagnostic.Code};]{diagnostic.Message}"); } } public static void RunSignTool(/*string certificate, string password, */string timeServer, string description, params string[] files) { var possibleSignToolPaths = new[] { @"C:\Program Files (x86)\Windows Kits\8.1\bin\x86\signtool.exe", @"C:\Program Files (x86)\Windows Kits\10\bin\x86\signtool.exe", @"C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\signtool.exe", }; var signToolToUse = possibleSignToolPaths.FirstOrDefault(File.Exists); if (signToolToUse == null) throw new Exception("signtool.exe not found"); string arguments = $"sign /fd sha384 /tr \"{timeServer}\" /td sha384 /d \"{description}\" {string.Join(" ", files.Select(f => "\"" + f + "\""))} "; // string arguments = $"sign /f \"{certificate}\" /p \"{password}\" /fd sha256 /tr \"{timeServer}\" /td sha256 /d \"{description}\" {string.Join(" ", files.Select(f => "\"" + f + "\""))} "; Run(signToolToUse, arguments); } public static string ReadDotNetTool(string tool, string arguments, string workingDirectory = null, TimeSpan? timeout = null) { return Read(Path.Combine(CentronPaths.DotNetToolsDirectory, tool), arguments, workingDirectory, timeout); } public static void Run(string name, string args, string workingDirectory = null, TimeSpan? timeout = null) { Read(name, args, workingDirectory, timeout); } public static string Read(string name, string args, string workingDirectory = null, TimeSpan? timeout = null) { var tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(timeout ?? GetDefaultTimeout()); var result = Cli.Wrap(name) .WithArguments(args) .WithWorkingDirectory(workingDirectory ?? Directory.GetCurrentDirectory()) .WithValidation(CommandResultValidation.None) .ExecuteBufferedAsync(tokenSource.Token) .Task.Result; if (result.ExitCode != 0) throw new Exception($"The command \"{name} {args}\" failed!{Environment.NewLine}{result.StandardOutput}{Environment.NewLine}{result.StandardError}") { Data = { ["ExitCode"] = result.ExitCode } }; return result.StandardOutput + Environment.NewLine + result.StandardError; } private static TimeSpan GetDefaultTimeout() { return EnvironmentHelper.UseLongTimeout() ? TimeSpan.FromHours(2) // Long-Timeout is typically used when running a security scan during build (like GitHub Advanced Security for Azure DevOps) : TimeSpan.FromMinutes(20); } } }