Skip to content

Instantly share code, notes, and snippets.

@peterhuene
Last active February 5, 2018 01:05
Show Gist options
  • Save peterhuene/a95b370f8bf84ee4b68be7b94de628fe to your computer and use it in GitHub Desktop.
Save peterhuene/a95b370f8bf84ee4b68be7b94de628fe to your computer and use it in GitHub Desktop.
Transactional tool install
using System;
using System.Collections.Generic;
using System.IO;
using System.Transactions;
using Microsoft.Extensions.EnvironmentAbstractions;
namespace Microsoft.DotNet.ToolPackage
{
internal interface IToolPackageManager
{
(ToolConfiguration Configuration, string ExecutablePath) InstallPackage(
string packageId,
string packageVersion = null,
string targetFramework = null,
FilePath? tempProjectPath = null,
DirectoryPath? offlineFeedPath = null,
FilePath? nugetConfig = null,
string source = null,
string verbosity = null);
ToolConfiguration UninstallPackage(string packageId);
}
internal interface IShellShimManager
{
void CreateShim(FilePath targetExecutablePath, string commandName);
void RemoveShim(string commandName);
}
internal class TransactionalOperation : IEnlistmentNotification
{
private Action _undo;
private Action _prepare;
public TransactionalOperation(Action undo, Action prepare)
{
_undo = undo;
_prepare = prepare;
}
public static void Enlist(Action undo = null, Action prepare = null)
{
if (Transaction.Current == null)
{
throw new InvalidOperationException();
}
if (undo == null && prepare == null)
{
return;
}
Transaction.Current.EnlistVolatile(new TransactionalOperation(undo, prepare), EnlistmentOptions.None);
}
public void Commit(Enlistment enlistment)
{
_undo = _prepare = null;
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
Rollback(enlistment);
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
try
{
Prepare();
}
catch (Exception ex)
{
_prepare = null;
Undo();
preparingEnlistment.ForceRollback(ex);
return;
}
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
_prepare = null;
Undo();
enlistment.Done();
}
private void Prepare()
{
if (_prepare != null)
{
_prepare();
}
_prepare = null;
}
private void Undo()
{
if (_undo != null)
{
_undo();
}
_undo = null;
}
}
internal class ToolPackageManager : IToolPackageManager
{
private readonly DirectoryPath _toolsPath;
private readonly DirectoryPath _stagingPath;
public ToolPackageManager(DirectoryPath toolsPath, DirectoryPath stagingPath)
{
_toolsPath = toolsPath;
_stagingPath = stagingPath;
}
public (ToolConfiguration Configuration, string ExecutablePath) InstallPackage(
string packageId,
string packageVersion = null,
string targetFramework = null,
FilePath? tempProjectPath = null,
DirectoryPath? offlineFeedPath = null,
FilePath? nugetConfig = null,
string source = null,
string verbosity = null)
{
var stagedPackageId = Path.GetRandomFileName();
var stagedPackageDirectory = GetStagedPackageDirectory(stagedPackageId);
var packageDirectory = GetPackageDirectory(packageId);
// Omitted: check to see if the package already exists
// Create staging directory
Directory.CreateDirectory(stagedPackageDirectory);
// Create a local transaction scope that inherits from any outter scope
// This will commit the transaction immediately if there is no outter scope
// If there is an outter scope, the completion of the inner scope will have no effect
using (var scope = new TransactionScope())
{
TransactionalOperation.Enlist(
undo: () => {
if (Directory.Exists(stagedPackageDirectory))
{
Directory.Delete(stagedPackageDirectory, true);
}
if (Directory.Exists(packageDirectory))
{
Directory.Delete(packageDirectory, true);
}
},
prepare: () => Directory.Move(stagedPackageDirectory, packageDirectory));
// Omitted: create the temp project and restore into the staged directory
// Omitted: read the tool configuration from the staged location
ToolConfiguration configuration = null;
// Omitted: calculate the executable path based upon the expected location in the non-staged package directory
string executablePath = null;
scope.Complete();
return (configuration, executablePath);
}
}
public ToolConfiguration UninstallPackage(string packageId)
{
throw new NotImplementedException();
}
private string GetStagedPackageDirectory(string stagedPackageId)
{
return Path.Combine(_stagingPath.Value, stagedPackageId);
}
private string GetPackageDirectory(string packageId)
{
return Path.Combine(_toolsPath.Value, packageId);
}
}
public class ShellShimManager : IShellShimManager
{
DirectoryPath _shimsDirectory;
public ShellShimManager(DirectoryPath shimsDirectory)
{
_shimsDirectory = shimsDirectory;
}
public void CreateShim(FilePath targetExecutablePath, string commandName)
{
// Omitted: checking if the shim already exists
// Create a local transaction scope that inherits from any outter scope
// This will commit the transaction immediately if there is no outter scope
// If there is an outter scope, the completion of the inner scope will have no effect
using (var scope = new TransactionScope())
{
// Omitted: create the shim files
string pathToShimFile = null;
TransactionalOperation.Enlist(undo: () => File.Delete(pathToShimFile));
// Omitted: adding an operation to delete the .exe.config on Windows
scope.Complete();
}
}
public void RemoveShim(string commandName)
{
throw new NotImplementedException();
}
}
public class Example
{
public static void Run()
{
bool somethingBadHappens = true;
using (var scope = new TransactionScope())
{
var packageManager = new ToolPackageManager(new DirectoryPath("tools"), new DirectoryPath("staging"));
var shimManager = new ShellShimManager(new DirectoryPath("shims"));
// Stages the install of the tool, waiting for the transaction to commit
var (configuration, executablePath) = packageManager.InstallPackage("some.tool");
// Create the shim
shimManager.CreateShim(new FilePath(executablePath), configuration.CommandName);
if (somethingBadHappens)
{
// This will roll back the package install and the creation of the shim
throw new Exception("whoops");
}
scope.Complete();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment