// 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.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using OpenTap.Cli;
namespace OpenTap.Package
{
///
/// Base class for ICliActions that use a mutex to lock the Target directory for the duration of the command.
///
public abstract class LockingPackageAction : PackageAction
{
internal const string CommandLineArgumentRepositoryDescription =
"Override the package repository.\n" +
"The default is https://packages.opentap.io.\n" +
"An authentication token can be optionally supplied by appending ';token=' to the url.\n" +
"Example: https://packages.opentap.io;token=1234";
internal const string CommandLineArgumentNoCacheDescription =
"A package cache is used by default.\n" +
"This can be controlled from ./Settings/Package Manager.xml\n" +
"or disabled using this option.";
internal const string CommandLineArgumentVersionDescription =
"Semantic version (semver) of the package.\n" +
"The default is to select only non pre-release packages for the current OS and CPU architecture.\n" +
"'x' select all (non pre-release) versions >= x.0.0 and < (x+1).0.0,\n" +
"'x.y' select all (non pre-release) versions >= x.y.0 and < x.(y+1).0,\n" +
"'^x.y' select all (non pre-release) versions >= x.y.0 and < (x+1).0.0,\n" +
"'x.y.z' only match the exact version.\n" +
"Use 'any', 'beta', or 'rc' to match 'any', 'beta', or 'rc' pre-release versions and above.";
internal const string CommandLineArgumentOsDescription =
"Override the OS (Linux, Windows, MacOS) to target.\n" +
"The default is the current OS.";
internal const string CommandLineArgumentArchitectureDescription =
"Override the CPU architecture (x86, x64, AnyCPU) to target.\n" +
"The default is the current CPU architecture.";
///
/// Unlockes the package action to allow multiple running at the same time.
///
public bool Unlocked { get; set; }
///
/// The location to apply the command to. The default is the location of OpenTap.PackageManager.exe
///
[CommandLineArgument("target", Description = "Override the location where the command is applied.\nThe default is the OpenTAP installation directory.", ShortName = "t")]
public string Target { get; set; }
internal static string GetLocalInstallationDir()
{
return Installation.Current.Directory;
}
internal static string GuessHostOS()
{
return OperatingSystem.Current.Name;
}
///
/// Get the named mutex used to lock the specified OpenTAP installation directory while it is being changed.
///
/// The OpenTAP installation directory
///
public static Mutex GetMutex(string target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
var fullDir = Path.GetFullPath(target).Replace('\\', '/');
var hasher = SHA256.Create();
var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(fullDir));
return new Mutex(false, "Keysight.Tap.Package InstallLock " + BitConverter.ToString(hash).Replace("-", ""));
}
///
/// Executes this the action. Derived types should override LockedExecute instead of this.
///
/// Return 0 to indicate success. Otherwise return a custom errorcode that will be set as the exitcode from the CLI.
public override int Execute(CancellationToken cancellationToken)
{
if (String.IsNullOrEmpty(Target))
Target = GetLocalInstallationDir();
else
Target = Path.GetFullPath(Target.Trim());
if (!Directory.Exists(Target))
{
if (File.Exists(Target))
{
log.Error("Destination directory \"{0}\" is a file.", Target);
return (int)ExitCodes.ArgumentError;
}
FileSystemHelper.EnsureDirectoryOf(Target);
}
var lockfile = Path.Combine(Target, ".lock");
FileSystemHelper.EnsureDirectoryOf(lockfile);
using var fileLock = FileLock.Create(lockfile);
bool useLocking = Unlocked == false;
if (useLocking && !fileLock.WaitOne(0))
{
log.Info("Waiting for other package manager operation to complete.");
try
{
switch (WaitHandle.WaitAny(new WaitHandle[] { fileLock.WaitHandle, cancellationToken.WaitHandle }, TimeSpan.FromMinutes(2)))
{
case 0: // we got the mutex
break;
case 1: // user cancelled
throw new ExitCodeException(5, "User aborted while waiting for other package manager operation to complete.");
case WaitHandle.WaitTimeout:
throw new ExitCodeException(6, "Timeout after 2 minutes while waiting for other package manager operation to complete.");
}
}
catch (AbandonedMutexException)
{
// Another package manager exited without releasing the mutex. We should be able to take it now.
if (!fileLock.WaitOne(0))
throw new ExitCodeException(7, "Unable to run while another package manager operation is running.");
}
}
return LockedExecute(cancellationToken);
}
///
/// The code to be executed by the action while the Target directory is locked.
///
/// Return 0 to indicate success. Otherwise return a custom errorcode that will be set as the exitcode from the CLI.
protected abstract int LockedExecute(CancellationToken cancellationToken);
///
/// Only here for compatibility. Use IsolatedPackageAction instead of calling this.
///
[Obsolete("Inherit from IsolatedPackageAction instead.")]
public static bool RunIsolated(string application = null, string target = null)
{
try
{
IsolatedPackageAction.RunIsolated(application, target);
return true;
}
catch(InvalidOperationException)
{
return false;
}
}
}
}