// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Text; using System.Threading; using OpenTap.Cli; using OpenTap.Package.PackageInstallHelpers; #pragma warning disable 1591 // TODO: Add XML Comments in this file, then remove this namespace OpenTap.Package { [Display("install", Group: "package", Description: "Install one or more packages.")] public class PackageInstallAction : IsolatedPackageAction { static readonly TraceSource Log = OpenTap.Log.CreateSource("Install"); [Obsolete("Use Force instead.")] public bool ForceInstall { get => Force; set => Force = value; } [CommandLineArgument("dependencies", Description = "Install dependencies without asking. This is always enabled when installing bundle packages.", ShortName = "y")] public bool InstallDependencies { get; set; } [Obsolete("It is no longer supported to ignore dependencies as it causes a broken installation when used.")] [CommandLineArgument("no-dependencies", Description = "Don't install dependencies. This is implied when using --force.")] public bool IgnoreDependencies { get; set; } [CommandLineArgument("overwrite", Description = "Overwrite files that already exist without asking. This is implied when using --force.")] public bool Overwrite { get; set; } [CommandLineArgument("repository", Description = CommandLineArgumentRepositoryDescription, ShortName = "r")] public string[] Repository { get; set; } [CommandLineArgument("no-cache", Description = CommandLineArgumentNoCacheDescription)] public bool NoCache { get; set; } [CommandLineArgument("version", Description = CommandLineArgumentVersionDescription)] public string Version { get; set; } [CommandLineArgument("os", Description = CommandLineArgumentOsDescription)] public string OS { get; set; } [CommandLineArgument("architecture", Description = CommandLineArgumentArchitectureDescription)] public CpuArchitecture Architecture { get; set; } /// /// This is used when specifying the install action through the CLI. If you need to specify multiple packages with different version numbers, use /// [UnnamedCommandLineArgument("package(s)", Required = true, Description = "One or more packages to install. A package can refer to a .TapPackage file, or a name to be resolved from the specified repositories.")] public string[] Packages { get; set; } [CommandLineArgument("check-only", Description = "Checks if the selected package(s) can be installed, but does not install or download them.")] public bool CheckOnly { get; set; } [Obsolete("Interactive is the default. Use --non-interactive to disable.")] [CommandLineArgument("interactive", Description = "More user responsive.")] [Browsable(false)] public bool Interactive { get; set; } /// /// Never prompt for user input. /// [CommandLineArgument("non-interactive", Description = "Never prompt for user input.")] public bool NonInteractive { get; set; } = false; [CommandLineArgument("no-downgrade", Description = "Don't install if the same or a newer version is already installed.")] public bool NoDowngrade { get; set; } [CommandLineArgument("unpack-only", Description = "Only unpack the package payload into the installation directory and\n" + "skip any additional install actions the package might have defined.\n" + "This can leave the installed package unusable.")] public bool UnpackOnly { get; set; } // This is set when system wide packages are being installed. public bool SystemWideOnly { get; set; } /// /// This is used when specifying multiple packages with different version numbers. In that case can be left null. /// public PackageSpecifier[] PackageReferences { get; set; } private string DefaultOs; /// /// This will be set only if the install action was started by /// This is supposed to solve an issue where OpenTAP fails to detect that the process was elevated. /// If one level of elevation was already attempted, it is unlikely that further attempts will cause the check to succeed. /// In this instance, it is better to just try the installation with the current privileges, and fail with whatever /// error if those privileges are not sufficient. /// internal bool AlreadyElevated { get; set; } public PackageInstallAction() { Architecture = ArchitectureHelper.GuessBaseArchitecture; OS = GuessHostOS(); DefaultOs = OS; } private int DoExecute(CancellationToken cancellationToken) { if (Target == null) Target = FileSystemHelper.GetCurrentInstallationDirectory(); if (TryFindParentInstallation(Target, out var parent)) { log.Error($"OpenTAP installation detected in directory '{parent}'. Nested installations are not supported."); return 1; } var targetInstallation = new Installation(Target); if (NoCache) PackageManagerSettings.Current.UseLocalPackageCache = false; Repository = ExtractRepositoryTokens(Repository, true); List repositories = PackageManagerSettings.Current.GetEnabledRepositories(Repository); if (!NonInteractive) Packages = AutoCorrectPackageNames.Correct(Packages, repositories); bool installError = false; var installer = new Installer(Target, cancellationToken) { DoSleep = false, ForceInstall = Force, UnpackOnly = UnpackOnly }; installer.ProgressUpdate += RaiseProgressUpdate; installer.Error += RaiseError; installer.Error += ex => installError = true; try { Log.Debug("Fetching package information..."); RaiseProgressUpdate(5, "Gathering packages."); // If exact version is specified, check if it's already installed if (Version != null && SemanticVersion.TryParse(Version, out var vs) && Force == false) { foreach (var pkg in Packages) { // We should always install when it's a file, this could be part of development if (File.Exists(pkg)) break; PackageIdentifier pid = new PackageIdentifier(pkg, vs, Architecture, OS); var installedPackage = targetInstallation.GetPackages(validOnly: true).FirstOrDefault(p => p.Name == pid.Name); if (installedPackage != null && pid.Version.Equals(installedPackage.Version)) { Log.Info($"Package '{pid.Name}' '{installedPackage.Version}' is already installed."); return (int)ExitCodes.Success; } } } // Get package information bool askToInstallDependencies = !NonInteractive; if (Force) askToInstallDependencies = false; List packagesToInstall; try { packagesToInstall = PackageActionHelpers.GatherPackagesAndDependencyDefs( targetInstallation, PackageReferences, Packages, Version, Architecture, OS, repositories, Force, NoDowngrade); } catch (ImageResolveException ex) when (ex.Result is FailedImageResolution fir) { // If the problem is exactly that we failed to resolve a compatible release version of a package, // ask the user to retry the resolution with a pre-release instead. // We could also ask for an RC / Any, but if there are no releases, then it is likely also the case that there are no RCs. // 'any' would be the most likely specifier to succeed, but it is probably a bit too extreme to suggest something so bleeding edge. // A beta version strikes a good middle ground. It is pretty likely to resolve, and probably won't be too unstable. if (NonInteractive) throw; if (Packages.Length != 1) throw; if (fir.resolveProblems.Count != 1) throw; var problem = fir.resolveProblems[0]; if (problem.Version != VersionSpecifier.AnyRelease) throw; // Only show the beta option if the resolution problem was with the package we are trying to install if (problem.Name != Packages[0]) throw; // Try to resolve a beta version instead, but only offer the beta version if the user wants it try { packagesToInstall = PackageActionHelpers.GatherPackagesAndDependencyDefs( targetInstallation, PackageReferences, Packages, "beta", Architecture, OS, repositories, Force, NoDowngrade); } catch { // throw original exception if beta resolution fails throw ex; } // If the beta resolution succeeded, ask the user if they want to install the beta version var betaVersion = packagesToInstall.First(p => p.Name == problem.Name).Version; var req = new AskAboutPrerelease($"Package '{problem.Name}' has no compatible release version, but the beta version '{betaVersion.ToString(4)}' is compatible.\nInstall this version instead?"); UserInput.Request(req); if (req.Response == UseBetaQuestion.No) throw; } if (SystemWideOnly) { // the current process is for installing system wide packages only. packagesToInstall = packagesToInstall.Where(x => x.IsSystemWide()).ToList(); } if (packagesToInstall?.Any() != true) { if (NoDowngrade) { Log.Info("No package(s) were upgraded."); return (int)ExitCodes.Success; } Log.Info("Could not find one or more packages."); return (int)PackageExitCodes.PackageDependencyError; } foreach (var pkg in packagesToInstall) { // print a warning if the selected package is incompatible with the host platform. // or return an error if the package does not match. var platformCompatible = pkg.IsPlatformCompatible(targetInstallation.Architecture, targetInstallation.OS); if (!Force) { // print a warning if necessary. // --os and --architecture are not really supported when --force is not enabled. // we only allow resolving to installable packages. var differentArch = ArchitectureHelper.GuessBaseArchitecture != Architecture; bool printedWarning = false; void maybePrintWarning() { if (printedWarning) return; printedWarning = true; Log.Warning("OS or Architecture were specified without --force. Only compatible packages will be selected."); } foreach (var package in packagesToInstall) { if (differentArch) { if (package.Architecture != CpuArchitecture.AnyCPU && Architecture != package.Architecture) { maybePrintWarning(); Log.Warning("Selected package {2} architecture {0} instead of {1}", package.Architecture, Architecture, package.Name); } } if (OS != DefaultOs) { if (!package.IsOsCompatible(OS)) { maybePrintWarning(); Log.Warning("Selected package {2} for {0} instead of {1}", package.Name, package.OS, OS); } } } } if (!platformCompatible) { var selectedPlatformCompatible = pkg.IsPlatformCompatible(Architecture, OS); var message = $"Selected package {pkg.Name} for {pkg.OS}, {pkg.Architecture} is incompatible with the host platform {targetInstallation.OS}, {targetInstallation.Architecture}."; if (selectedPlatformCompatible || Force) // --OS [arg] was used or --force is specified. Try to install it anyway. Log.Warning(message); else { Log.Error(message); return (int)ExitCodes.ArgumentError; } } } var installationPackages = targetInstallation.GetPackages(validOnly: true); var overWriteCheckExitCode = CheckForOverwrittenPackages(installationPackages, packagesToInstall, Force || Overwrite, !(NonInteractive || Overwrite)); if (overWriteCheckExitCode == InstallationQuestion.Cancel) { Log.Info("Install cancelled by user."); return (int)ExitCodes.UserCancelled; } if (overWriteCheckExitCode == InstallationQuestion.OverwriteFile) Log.Warning("Overwriting files. (--{0} option specified).", Overwrite ? "overwrite" : "force"); RaiseProgressUpdate(10, "Gathering dependencies."); if (!SystemWideOnly) { // Sometimes system wide packages depends on non-system wide packages. // when installing system wide packages in a sub-process we dont need to check the dependencies // because that has already been done. bool checkDependencies = !Force || CheckOnly; var installedToCheck = installationPackages.Where(p => p.IsSystemWide() == false); // don't use system-wide to check, as that would prevent installing older stuff, if a system-wide package depends on newer versions. var issue = DependencyChecker.CheckDependencies(installedToCheck, packagesToInstall, Force ? LogEventType.Information : checkDependencies ? LogEventType.Error : LogEventType.Warning); if (checkDependencies) { if (issue == DependencyChecker.Issue.BrokenPackages) { Log.Info("To fix the package conflict uninstall or update the conflicted packages."); Log.Info( "To install packages despite the conflicts, use the --force option. Note that this can break the installation."); return (int)PackageExitCodes.PackageDependencyError; } if (CheckOnly) { Log.Info("Check completed with no problems detected."); return (int)ExitCodes.Success; } } } // Download the packages // We divide the progress by 2 in the progress update because we assume downloading the packages // accounts for half the installation progress. So when all the packages have finished downloading, // we have finished 10 + (100/2)% of the installation process. packagesToInstall = packagesToInstall.OrderBy(p => p.IsSystemWide()).ToList(); var downloadedPackageFiles = PackageActionHelpers.DownloadPackages( PackageCacheHelper.PackageCacheDirectory, packagesToInstall, progressUpdate: (progress, msg) => RaiseProgressUpdate(10 + progress / 2, msg), ignoreCache: NoCache); // The downloaded package files will arrive in the same order as the packagesToInstall list. // We need to split the list into two parts, one for regular packages and one for systemwide packages. var cutoff = packagesToInstall.FindIndex(p => p.IsSystemWide()); if (cutoff == -1) cutoff = packagesToInstall.Count; var regularPackages = downloadedPackageFiles.Take(cutoff).ToList(); var systemwidePackages = downloadedPackageFiles.Skip(cutoff).ToArray(); // We need to elevate if // 1. Elevation was not already attempted, and // 2. we need to install systemWide packages, and // 3. We are not already running as admin bool needElevation = !AlreadyElevated && systemwidePackages.Any() && SubProcessHost.IsAdmin() == false; // Warn the user if elevation was already attempted, and we are not currently running as admin if (AlreadyElevated && SubProcessHost.IsAdmin() == false) { log.Warning($"Process elevation failed. Installation will continue without elevation."); } // If we need to install system-wide packages and we are not admin, we should install them in an elevated sub-process if (needElevation) { RaiseProgressUpdate(20, "Installing system-wide packages."); var installStep = new PackageInstallStep() { Packages = systemwidePackages, Repositories = Array.Empty(), Target = PackageDef.SystemWideInstallationDirectory, Force = Force, SystemWideOnly = true }; var processRunner = new SubProcessHost { ForwardLogs = true, MutedSources = { "CLI", "Session", "Resolver", "AssemblyFinder", "PluginManager", "TestPlan", "UpdateCheck", "Installation" }, // The current install action is a locking package action. // Setting this flag lets the child process bypass the lock on the installation. Unlocked = true, }; var result = processRunner.Run(installStep, true, cancellationToken); if (result != Verdict.Pass) { var ex = new Exception( $"Failed installing system-wide packages. Try running the command as administrator."); RaiseError(ex); throw ex; } var pct = ((double)systemwidePackages.Length / (systemwidePackages.Length + packagesToInstall.Count)) * 100; RaiseProgressUpdate((int)pct, "Installed system-wide packages."); } // Otherwise if we are admin and we need to install system-wide packages, we can install them in the current process else if (systemwidePackages.Any()) { installer.PackagePaths.AddRange(systemwidePackages); } installer.PackagePaths.AddRange(regularPackages); } catch (OperationCanceledException e) { Log.Info(e.Message); return (int)ExitCodes.UserCancelled; } catch (ImageResolveException ex) { if (Packages != null && Packages.Length > 0) { Log.Error("Could not install {0}{1}", string.Join(", ", Packages.Select(x => $"{x}")), string.IsNullOrWhiteSpace(Version) ? "" : $" v{Version}"); // If the requested package is a bundle, the easiest way to resolve the resolution error is to uninstall the bundle. if (Packages.Length == 1 && targetInstallation.GetPackages().FirstOrDefault(p => p.Name == Packages[0]) is PackageDef pkg && pkg.IsBundle()) { Log.Error( $"Please try manually uninstalling '{Packages[0]}' and re-installing it."); return (int)PackageExitCodes.PackageDependencyError; } } else { Log.Error("Could not resolve one or more packages."); } var unsatisfiedDependencies = ex.InstalledPackages.Where(x => false == x.Dependencies.All(dep => ex.InstalledPackages.Any(x2 => x2.Name == dep.Name && dep.Version.IsSatisfiedBy(x2.Version.AsExactSpecifier())))).ToArray(); if (unsatisfiedDependencies.Any()) { Log.Warning("This might be because of the following conflicts:"); var missingDeps = unsatisfiedDependencies.SelectMany(x => x.Dependencies.Where(dep => !ex.InstalledPackages.Any(x2 => x2.Name == dep.Name && dep.Version.IsSatisfiedBy(x2.Version.AsExactSpecifier())))) .ToArray(); foreach (var grouping in missingDeps.GroupBy(d => d.Name)) { var versions = grouping.ToArray(); var highest = versions.FindMax(dep => dep.Version); var dependers = unsatisfiedDependencies.Where(dep => dep.Dependencies.Any(d => d.Name == grouping.Key)); // Omit the warnings for packages that would have been satisfied by the specified version if (Packages[0] == grouping.Key && !string.IsNullOrWhiteSpace(Version) && VersionSpecifier.TryParse(Version, out var filter)) { dependers = dependers.Where(dep => false == dep.Dependencies.First(d => d.Name == grouping.Key).Version .IsSatisfiedBy(filter)); } var dependString = string.Join(", ", dependers.Select(d => d.Name)); Log.Info($"{grouping.Key} version {highest.Version} required by {dependString}"); } } // If the problem is not generic, then there are additional details about the resolution problem. if (ex.Result is FailedImageResolution f && f.resolveProblems is not GenericResolutionProblem) log.Info(f.resolveProblems.Description()); Log.Debug("{0}", ex.Message); return (int)ExitCodes.PackageResolutionError; } catch (Exception e) { Log.Info("Could not download one or more packages."); Log.Info(e.Message); Log.Debug(e); RaiseError(e); return (int)ExitCodes.NetworkError; } // This happens in cases where only system-wide packages were requested. if (installer.PackagePaths.Count == 0) return 0; Log.Info("Installing to {0}", Path.GetFullPath(Target)); // Uninstall old packages before var status = UninstallExisting(targetInstallation, installer.PackagePaths, cancellationToken); if (status != (int)ExitCodes.Success) return status; var toInstall = ReorderPackages(installer.PackagePaths); installer.PackagePaths.Clear(); installer.PackagePaths.AddRange(toInstall); // Install the package installer.InstallThread(); if (installError) return (int)PackageExitCodes.PackageInstallError; return 0; } protected override int LockedExecute(CancellationToken cancellationToken) { var currentInterface = UserInput.GetInterface(); if (NonInteractive) UserInput.SetInterface(new NonInteractiveUserInputInterface()); try { return DoExecute(cancellationToken); } finally { IncrementChangeId(Target); UserInput.SetInterface(currentInterface); } } private int UninstallExisting(Installation installation, List packagePaths, CancellationToken cancellationToken) { var installed = installation.GetPackages(); var packages = packagePaths.Select(PackageDef.FromPackage).Select(x => x.Name).ToHashSet(); var existingPackages = installed.Where(kvp => packages.Contains(kvp.Name)).Select(x => (x.PackageSource as XmlPackageDefSource)?.PackageDefFilePath).ToList(); if (existingPackages.Count == 0) return (int)ExitCodes.Success; var newInstaller = new Installer(Target, cancellationToken); //newInstaller.ProgressUpdate += RaiseProgressUpdate; newInstaller.Error += RaiseError; newInstaller.DoSleep = false; newInstaller.PackagePaths.AddRange(existingPackages); return newInstaller.UninstallThread(); } /// /// Reorder packages to ensure that dependencies are installed before a package needing it. /// /// /// private List ReorderPackages(List packagePaths) { var toInstall = new List(); var packages = packagePaths.ToDictionary(k => k, k => PackageDef.FromPackage(k)); while (packages.Count > 0) { var next = packages.FirstOrDefault(pkg => pkg.Value.Dependencies.All(dep => !packages.Values.Any(p => p.Name == dep.Name))); if (next.Value == null) next = packages.First(); // This doesn't matter at this point toInstall.Add(next.Key); packages.Remove(next.Key); } return toInstall; } public enum InstallationQuestion { //KeepExitingFiles = 3, [Display("Cancel", Order: 2)] Cancel = 2, [Display("Overwrite Files", Order: 1)] OverwriteFile = 1, [Browsable(false)] Success = 0, } class AskAboutInstallingAnyway { public string Name { get; } = "Overwrite Files?"; [Browsable(true)] [Layout(LayoutMode.FullRow)] public string Message { get; private set; } public AskAboutInstallingAnyway(string message) => Message = message; [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)] [Submit] public InstallationQuestion Response { get; set; } = InstallationQuestion.Cancel; } internal enum UseBetaQuestion { [Display("Yes")] Beta = 0, [Display("No")] No = 1, } class AskAboutPrerelease { [Browsable(true)] [Layout(LayoutMode.FullRow)] public string Message { get; private set; } public AskAboutPrerelease(string message) => Message = message; [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)] [Submit] public UseBetaQuestion Response { get; set; } = UseBetaQuestion.No; } internal static InstallationQuestion CheckForOverwrittenPackages(IEnumerable installedPackages, IEnumerable packagesToInstall, bool force, bool interactive = false) { var conflicts = VerifyPackageHashes.CalculatePackageInstallConflicts(installedPackages, packagesToInstall); void buildMessage(Action addLine) { foreach (var conflictGroup in conflicts.ToLookup(x => x.OffendingPackage)) { addLine($" These files will be overwritten if package '{conflictGroup.Key.Name}' is installed:"); foreach (var conflict in conflictGroup) { addLine($" {conflict.File.FileName} (from package '{conflict.Package.Name}')."); } } } if (conflicts.Any()) { if (interactive) { StringBuilder message = new StringBuilder(); buildMessage(line => message.AppendLine(line)); var question = new AskAboutInstallingAnyway(message.ToString()); UserInput.Request(question); return question.Response; } buildMessage(line => Log.Info(line)); if (force) { return InstallationQuestion.OverwriteFile; } Log.Error( "Installing these packages will overwrite existing files. " + "Use --overwrite to overwrite existing files, possibly breaking installed packages."); return InstallationQuestion.Cancel; } return InstallationQuestion.Success; } } }