using Semmle.Util; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Xml; using System.Net.Http; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using System.Runtime.InteropServices; namespace Semmle.Autobuild.Shared { public delegate void BuildOutputHandler(string? data); /// /// Wrapper around system calls so that the build scripts can be unit-tested. /// public interface IBuildActions { /// /// Runs a process, captures its output, and provides it asynchronously. /// /// The exe to run. /// The other command line arguments. /// The working directory (null for current directory). /// Additional environment variables. /// A handler for stdout output. /// A handler for stderr output. /// The process exit code. int RunProcess(string exe, string args, string? workingDirectory, IDictionary? env, BuildOutputHandler onOutput, BuildOutputHandler onError); /// /// Runs a process and captures its output. /// /// The exe to run. /// The other command line arguments. /// The working directory (null for current directory). /// Additional environment variables. /// The lines of stdout. /// The process exit code. int RunProcess(string exe, string args, string? workingDirectory, IDictionary? env, out IList stdOut); /// /// Runs a process but does not capture its output. /// /// The exe to run. /// The other command line arguments. /// The working directory (null for current directory). /// Additional environment variables. /// The process exit code. int RunProcess(string exe, string args, string? workingDirectory, IDictionary? env); /// /// Tests whether a file exists, File.Exists(). /// /// The filename. /// True iff the file exists. bool FileExists(string file); /// /// Tests whether a directory exists, Directory.Exists(). /// /// The directory name. /// True iff the directory exists. bool DirectoryExists(string dir); /// /// Deletes a file, File.Delete(). /// /// The filename. void FileDelete(string file); /// /// Deletes a directory, Directory.Delete(). /// void DirectoryDelete(string dir, bool recursive); /// /// Creates all directories and subdirectories in the specified path unless they already exist. /// void CreateDirectory(string path); /// /// Gets an environment variable, Environment.GetEnvironmentVariable(). /// /// The name of the variable. /// The string value, or null if the variable is not defined. string? GetEnvironmentVariable(string name); /// /// Gets the current directory, Directory.GetCurrentDirectory(). /// /// The current directory. string GetCurrentDirectory(); /// /// Enumerates files in a directory, Directory.EnumerateFiles(). /// /// The directory to enumerate. /// A list of filenames, or an empty list. IEnumerable EnumerateFiles(string dir); /// /// Enumerates the directories in a directory, Directory.EnumerateDirectories(). /// /// The directory to enumerate. /// List of subdirectories, or empty list. IEnumerable EnumerateDirectories(string dir); /// /// True if we are running on Windows. /// bool IsWindows(); /// /// Gets a value indicating whether we are running on macOS. /// /// True if we are running on macOS. bool IsMacOs(); /// /// Gets a value indicating whether we are running on arm. /// /// True if we are running on arm. bool IsArm(); /// /// Combine path segments, Path.Combine(). /// /// The parts of the path. /// The combined path. string PathCombine(params string[] parts); /// /// Gets the full path for , Path.GetFullPath(). /// string GetFullPath(string path); /// /// Returns the file name and extension of the specified path string. /// [return: NotNullIfNotNull("path")] string? GetFileName(string? path); /// /// Returns the directory information for the specified path string. /// string? GetDirectoryName(string? path); /// /// Writes contents to file, File.WriteAllText(). /// /// The filename. /// The text. void WriteAllText(string filename, string contents); /// /// Loads the XML document from . /// XmlDocument LoadXml(string filename); string EnvironmentExpandEnvironmentVariables(string s); /// /// Downloads the resource with the specified URI to a local file. /// void DownloadFile(string address, string fileName); /// /// Creates an for the given . /// /// /// The path suggesting where the diagnostics should be written to. /// /// /// A to which diagnostic entries can be added. /// IDiagnosticsWriter CreateDiagnosticsWriter(string filename); } /// /// An implementation of IBuildActions that actually performs the requested operations. /// public class SystemBuildActions : IBuildActions { void IBuildActions.FileDelete(string file) => File.Delete(file); bool IBuildActions.FileExists(string file) => File.Exists(file); private static ProcessStartInfo GetProcessStartInfo(string exe, string arguments, string? workingDirectory, IDictionary? environment, bool redirectStandardOutput) { var pi = new ProcessStartInfo(exe, arguments) { UseShellExecute = false, RedirectStandardOutput = redirectStandardOutput }; if (workingDirectory is not null) pi.WorkingDirectory = workingDirectory; // Environment variables can only be used when not redirecting stdout if (!redirectStandardOutput) { if (environment is not null) environment.ForEach(kvp => pi.Environment[kvp.Key] = kvp.Value); } return pi; } int IBuildActions.RunProcess(string exe, string args, string? workingDirectory, System.Collections.Generic.IDictionary? env, BuildOutputHandler onOutput, BuildOutputHandler onError) { var pi = GetProcessStartInfo(exe, args, workingDirectory, env, true); using var p = new Process { StartInfo = pi }; p.StartInfo.RedirectStandardError = true; p.OutputDataReceived += new DataReceivedEventHandler((sender, e) => onOutput(e.Data)); p.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => onError(e.Data)); p.Start(); p.BeginErrorReadLine(); p.BeginOutputReadLine(); p.WaitForExit(); return p.ExitCode; } int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? environment) { var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment, false); using var p = Process.Start(pi); if (p is null) { return -1; } p.WaitForExit(); return p.ExitCode; } int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary? environment, out IList stdOut) { var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment, true); return pi.ReadOutput(out stdOut); } void IBuildActions.DirectoryDelete(string dir, bool recursive) => Directory.Delete(dir, recursive); bool IBuildActions.DirectoryExists(string dir) => Directory.Exists(dir); void IBuildActions.CreateDirectory(string path) => Directory.CreateDirectory(path); string? IBuildActions.GetEnvironmentVariable(string name) => Environment.GetEnvironmentVariable(name); string IBuildActions.GetCurrentDirectory() => Directory.GetCurrentDirectory(); IEnumerable IBuildActions.EnumerateFiles(string dir) => Directory.EnumerateFiles(dir); IEnumerable IBuildActions.EnumerateDirectories(string dir) => Directory.EnumerateDirectories(dir); bool IBuildActions.IsWindows() => Win32.IsWindows(); bool IBuildActions.IsMacOs() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); bool IBuildActions.IsArm() => RuntimeInformation.ProcessArchitecture == Architecture.Arm64 || RuntimeInformation.ProcessArchitecture == Architecture.Arm; string IBuildActions.PathCombine(params string[] parts) => Path.Combine(parts); void IBuildActions.WriteAllText(string filename, string contents) => File.WriteAllText(filename, contents); XmlDocument IBuildActions.LoadXml(string filename) { var ret = new XmlDocument(); ret.Load(filename); return ret; } string IBuildActions.GetFullPath(string path) => Path.GetFullPath(path); string? IBuildActions.GetFileName(string? path) => Path.GetFileName(path); string? IBuildActions.GetDirectoryName(string? path) => Path.GetDirectoryName(path); public string EnvironmentExpandEnvironmentVariables(string s) => Environment.ExpandEnvironmentVariables(s); private static async Task DownloadFileAsync(string address, string filename) { using var httpClient = new HttpClient(); using var request = new HttpRequestMessage(HttpMethod.Get, address); using var contentStream = await (await httpClient.SendAsync(request)).Content.ReadAsStreamAsync(); using var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); await contentStream.CopyToAsync(stream); } public void DownloadFile(string address, string fileName) => DownloadFileAsync(address, fileName).Wait(); public IDiagnosticsWriter CreateDiagnosticsWriter(string filename) => new DiagnosticsStream(filename); public static IBuildActions Instance { get; } = new SystemBuildActions(); } }