using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using Microsoft.Build.Framework; using OpenTap; using OpenTap.Diagnostic; using OpenTap.Package; namespace Keysight.OpenTap.Sdk.MSBuild { internal interface IImageDeployer { void Install(ImageSpecifier spec, Installation install, CancellationToken cts); } internal class DefaultImageDeployer : IImageDeployer { public void Install(ImageSpecifier spec, Installation install, CancellationToken cts) { if (!spec.Repositories.Any(r => r.ToLower().Contains("https://packages.opentap.io") || r.ToLower().Contains("http://packages.opentap.io"))) spec.Repositories.Add("https://packages.opentap.io"); spec.MergeAndDeploy(install, cts); } } internal class OpenTapImageInstaller : IDisposable { public string TapDir { get; set; } public string RuntimeDir { get; set; } public CancellationToken CancellationToken { get; set; } public PackageDef OpenTapNugetPackage { get; set; } public IImageDeployer ImageDeployer { get; set; } /// /// This object represents a trace listener of the type EventTraceListener from the assembly /// private EventTraceListener traceListener; void onEvent(IEnumerable events) { // These sources are really loud and not relevant to the image install. // In addition, messages logged at the error level are treated as build errors by MSBuild. // Skip these log sources. var mutedSources = new HashSet() { "Searcher", "PluginManager", "TypeDataProvider", "Resolver", "Serializer" }; foreach (var evt in events) { if (mutedSources.Contains(evt.Source)) continue; var msg = $"{evt.Source} : {evt.Message}"; LogMessage(msg, evt.EventType, null); } } /// /// Instantiate an OpenTAP trace listener and create a delegate to handle log messages /// /// void attachTraceListener() { traceListener = new EventTraceListener(); traceListener.MessageLogged += onEvent; Log.AddListener(traceListener); } public OpenTapImageInstaller(string tapDir, string runtimeDir, CancellationToken cancellationToken) { CancellationToken = cancellationToken; TapDir = tapDir; RuntimeDir = runtimeDir; attachTraceListener(); } /// /// UserInputInterface implementation which returns immediately. Intended for non-interactive use. /// class NonInteractiveUserInputInterface : IUserInputInterface { void IUserInputInterface.RequestUserInput(object dataObject, TimeSpan timeout, bool modal) { } } /// /// Creates a merged image of currently installed packages and the packages contained in ITaskItem[] /// and deploys it to the output directory. /// /// /// /// public bool InstallImage(ITaskItem[] packagesToInstall, List repositories) { bool success = false; var mutexName = Path.GetFullPath(TapDir) + ".image.lock"; using var filelock = FileLock.Create(mutexName); var sw = Stopwatch.StartNew(); int count = 0; while (!filelock.WaitOne(TimeSpan.FromSeconds(5))) { if (sw.Elapsed > TimeSpan.FromMinutes(10)) { LogMessage($"Image installer timed out waiting for mutex.", (int)LogEventType.Error, null); return false; } count++; LogMessage($"Image Installer waiting for mutex... ({count})", (int)LogEventType.Warning, null); } try { // This installation is happening during a C# compilation context. The user will not be able to respond to any user input requests. // We should ensure a non-interactive user input is configured in case any user inputs are raised. UserInput.SetInterface(new NonInteractiveUserInputInterface()); var install = new Installation(TapDir); OpenTapNugetPackage = install.GetOpenTapPackage(); var imageSpecifier = ImageSpecifierFromTaskItems(packagesToInstall); imageSpecifier.Repositories.AddRange(repositories); var openTapSpec = new PackageSpecifier("OpenTAP", VersionSpecifier.Parse(OpenTapNugetPackage.Version.ToString())); imageSpecifier.Packages.Add(openTapSpec); ImageDeployer ??= new DefaultImageDeployer(); try { ImageDeployer.Install(imageSpecifier, install, CancellationToken); success = true; } catch (ImageResolveException ex) { LogMessage(ex.Message, (int)LogEventType.Error, null); LogMessage("Unable to resolve image.", (int)LogEventType.Error, null); } } catch (AggregateException aex) { LogMessage(aex.Message, (int)LogEventType.Error, null); foreach (var ex in aex.InnerExceptions) { LogMessage(ex.Message, (int)LogEventType.Error, null); } } catch (Exception ex) { LogMessage(ex.Message, (int)LogEventType.Error, null); } finally { filelock.Release(); if (success == false) { LogMessage($"Failed to install packages.", (int)LogEventType.Error, null); } } return success; } public Action LogMessage = (msg, evt, item) => { }; public void Dispose() { Log.RemoveListener(traceListener); } private ImageSpecifier ImageSpecifierFromTaskItems(ITaskItem[] items) { var spec = new ImageSpecifier(); foreach (var i in items) { var name = i.ItemSpec; var versionString = i.GetMetadata("Version"); // The version of OpenTAP installed through nuget is added manually and should not be considered from task items if (name == "OpenTAP") { // The version was omitted - this is fine if (string.IsNullOrWhiteSpace(versionString)) continue; if (VersionSpecifier.TryParse(versionString, out var versionSpecifier)) { // The requested version is compatible with the installed version -- this is fine if (versionSpecifier.IsCompatible(OpenTapNugetPackage.Version)) continue; } LogMessage( $"This project was restored using OpenTAP version '{OpenTapNugetPackage.Version}', but version " + $"'{versionString}' was requested. Changing the version of OpenTAP installed through nuget " + $"can have unpredictable results and is not supported. Please omit the version from this element.", (int)LogEventType.Warning, i); continue; } var archString = i.GetMetadata("Architecture"); var os = i.GetMetadata("OS"); if (Enum.TryParse(archString, out CpuArchitecture arch) == false) arch = CpuArchitecture.Unspecified; if (VersionSpecifier.TryParse(versionString, out var version) == false) { LogMessage($"String '{versionString}' is not a valid version specifier." + $" Falling back to latest release.", (int)LogEventType.Warning, i); version = VersionSpecifier.Parse(""); } spec.Packages.Add(new PackageSpecifier(name, version, arch, os)); } return spec; } } }