-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Redpoint.PackageManagement library (#68)
* Implement Redpoint.PackageManagement library * Add ProgressMonitor to test dependency injection
- Loading branch information
Showing
16 changed files
with
432 additions
and
99 deletions.
There are no files selected for viewing
159 changes: 159 additions & 0 deletions
159
UET/Redpoint.PackageManagement/HomebrewPackageManager.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
namespace Redpoint.PackageManagement | ||
{ | ||
using Microsoft.Extensions.Logging; | ||
using Redpoint.PathResolution; | ||
using Redpoint.ProcessExecution; | ||
using Redpoint.ProgressMonitor.Utils; | ||
using Redpoint.Reservation; | ||
using System.Runtime.Versioning; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
[SupportedOSPlatform("macos")] | ||
internal class HomebrewPackageManager : IPackageManager | ||
{ | ||
private readonly ILogger<HomebrewPackageManager> _logger; | ||
private readonly IPathResolver _pathResolver; | ||
private readonly IProcessExecutor _processExecutor; | ||
private readonly ISimpleDownloadProgress _simpleDownloadProgress; | ||
private readonly IGlobalMutexReservationManager _globalMutexReservationManager; | ||
|
||
public HomebrewPackageManager( | ||
ILogger<HomebrewPackageManager> logger, | ||
IPathResolver pathResolver, | ||
IProcessExecutor processExecutor, | ||
ISimpleDownloadProgress simpleDownloadProgress, | ||
IReservationManagerFactory reservationManagerFactory) | ||
{ | ||
_logger = logger; | ||
_pathResolver = pathResolver; | ||
_processExecutor = processExecutor; | ||
_simpleDownloadProgress = simpleDownloadProgress; | ||
_globalMutexReservationManager = reservationManagerFactory.CreateGlobalMutexReservationManager(); | ||
} | ||
|
||
private async Task<string> FindHomebrewOrInstallItAsync(CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
return await _pathResolver.ResolveBinaryPath("brew").ConfigureAwait(false); | ||
} | ||
catch (FileNotFoundException) | ||
{ | ||
} | ||
|
||
await using (await _globalMutexReservationManager.ReserveExactAsync("HomebrewInstall", cancellationToken)) | ||
{ | ||
try | ||
{ | ||
return await _pathResolver.ResolveBinaryPath("brew").ConfigureAwait(false); | ||
} | ||
catch (FileNotFoundException) | ||
{ | ||
} | ||
|
||
if (File.Exists("/opt/homebrew/bin/brew")) | ||
{ | ||
return "/opt/homebrew/bin/brew"; | ||
} | ||
|
||
_logger.LogInformation($"Downloading Homebrew..."); | ||
using var client = new HttpClient(); | ||
|
||
var targetPath = Path.Combine(Path.GetTempPath(), "install.sh"); | ||
using (var file = new FileStream(targetPath, FileMode.Create, FileAccess.Write)) | ||
{ | ||
await _simpleDownloadProgress.DownloadAndCopyToStreamAsync( | ||
client, | ||
new Uri("https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"), | ||
async stream => await stream.CopyToAsync(file, cancellationToken).ConfigureAwait(false), | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
File.SetUnixFileMode(targetPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); | ||
|
||
_logger.LogInformation($"Installing Homebrew..."); | ||
await _processExecutor.ExecuteAsync( | ||
new ProcessSpecification | ||
{ | ||
FilePath = @"/bin/bash", | ||
Arguments = new LogicalProcessArgument[] | ||
{ | ||
"-c", | ||
targetPath, | ||
}, | ||
WorkingDirectory = Path.GetTempPath(), | ||
EnvironmentVariables = new Dictionary<string, string> | ||
{ | ||
{ "NONINTERACTIVE", "1" } | ||
}, | ||
}, CaptureSpecification.Passthrough, cancellationToken).ConfigureAwait(false); | ||
|
||
return "/opt/homebrew/bin/brew"; | ||
} | ||
} | ||
|
||
public async Task InstallOrUpgradePackageToLatestAsync(string packageId, CancellationToken cancellationToken) | ||
{ | ||
var homebrew = await FindHomebrewOrInstallItAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
await using (await _globalMutexReservationManager.TryReserveExactAsync($"PackageInstall-{packageId}").ConfigureAwait(false)) | ||
{ | ||
_logger.LogInformation($"Checking if {packageId} is installed..."); | ||
var exitCode = await _processExecutor.ExecuteAsync( | ||
new ProcessSpecification | ||
{ | ||
FilePath = homebrew, | ||
Arguments = new LogicalProcessArgument[] | ||
{ | ||
"list", | ||
packageId, | ||
}, | ||
WorkingDirectory = Path.GetTempPath(), | ||
EnvironmentVariables = new Dictionary<string, string> | ||
{ | ||
{ "NONINTERACTIVE", "1" } | ||
}, | ||
}, CaptureSpecification.Silence, cancellationToken).ConfigureAwait(false); | ||
|
||
if (exitCode == 0) | ||
{ | ||
_logger.LogInformation($"Ensuring {packageId} is up-to-date..."); | ||
await _processExecutor.ExecuteAsync( | ||
new ProcessSpecification | ||
{ | ||
FilePath = homebrew, | ||
Arguments = [ | ||
"upgrade", | ||
"git", | ||
], | ||
EnvironmentVariables = new Dictionary<string, string> | ||
{ | ||
{ "NONINTERACTIVE", "1" } | ||
}, | ||
}, | ||
CaptureSpecification.Passthrough, | ||
CancellationToken.None).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
_logger.LogInformation($"Installing {packageId}..."); | ||
await _processExecutor.ExecuteAsync( | ||
new ProcessSpecification | ||
{ | ||
FilePath = homebrew, | ||
Arguments = [ | ||
"install", | ||
"git", | ||
], | ||
EnvironmentVariables = new Dictionary<string, string> | ||
{ | ||
{ "NONINTERACTIVE", "1" } | ||
}, | ||
}, | ||
CaptureSpecification.Passthrough, | ||
CancellationToken.None).ConfigureAwait(false); | ||
} | ||
}; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
namespace Redpoint.PackageManagement | ||
{ | ||
using System.Threading; | ||
|
||
public interface IPackageManager | ||
{ | ||
/// <summary> | ||
/// Installs or upgrades the target package to the latest version. | ||
/// </summary> | ||
/// <param name="packageId">The package ID (platform dependent).</param> | ||
/// <returns>An asynchronous task that can be awaited on.</returns> | ||
Task InstallOrUpgradePackageToLatestAsync( | ||
string packageId, | ||
CancellationToken cancellationToken); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
namespace Redpoint.PackageManagement | ||
{ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
internal class NullPackageManager : IPackageManager | ||
{ | ||
public Task InstallOrUpgradePackageToLatestAsync( | ||
string packageId, | ||
CancellationToken cancellationToken) | ||
{ | ||
throw new PlatformNotSupportedException("This platform does not support package management."); | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
UET/Redpoint.PackageManagement/PackageManagementServiceExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
namespace Redpoint.PackageManagement | ||
{ | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
/// <summary> | ||
/// Provides registration functions to register an implementation of <see cref="IPackageManager"/> into a <see cref="IServiceCollection"/>. | ||
/// </summary> | ||
public static class PathResolutionServiceExtensions | ||
{ | ||
/// <summary> | ||
/// Add package management services (the <see cref="IPackageManager"/> service) into the service collection. | ||
/// </summary> | ||
/// <param name="services">The service collection to register an implementation of <see cref="IPackageManager"/> to.</param> | ||
public static void AddPackageManagement(this IServiceCollection services) | ||
{ | ||
if (OperatingSystem.IsWindows()) | ||
{ | ||
services.AddSingleton<IPackageManager, WinGetPackageManager>(); | ||
} | ||
else if (OperatingSystem.IsMacOS()) | ||
{ | ||
services.AddSingleton<IPackageManager, HomebrewPackageManager>(); | ||
} | ||
else | ||
{ | ||
services.AddSingleton<IPackageManager, NullPackageManager>(); | ||
} | ||
} | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
UET/Redpoint.PackageManagement/Redpoint.PackageManagement.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<Import Project="$(MSBuildThisFileDirectory)../Lib/Common.Build.props" /> | ||
|
||
<Import Project="$(MSBuildThisFileDirectory)../Lib/LibraryPackaging.Build.props" /> | ||
<PropertyGroup> | ||
<Description>Provides APIs for installing, upgrading and uninstalling packages with WinGet and Homebrew.</Description> | ||
<PackageTags>winget, homebrew, package, management</PackageTags> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<ProjectReference Include="..\Redpoint.Concurrency\Redpoint.Concurrency.csproj" /> | ||
<ProjectReference Include="..\Redpoint.PathResolution\Redpoint.PathResolution.csproj" /> | ||
<ProjectReference Include="..\Redpoint.ProcessExecution\Redpoint.ProcessExecution.csproj" /> | ||
<ProjectReference Include="..\Redpoint.ProgressMonitor\Redpoint.ProgressMonitor.csproj" /> | ||
<ProjectReference Include="..\Redpoint.Reservation\Redpoint.Reservation.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
namespace Redpoint.PackageManagement | ||
{ | ||
using Microsoft.Extensions.Logging; | ||
using Redpoint.Concurrency; | ||
using Redpoint.PathResolution; | ||
using Redpoint.ProcessExecution; | ||
using Redpoint.ProgressMonitor.Utils; | ||
using Redpoint.Reservation; | ||
using System.Runtime.Versioning; | ||
using System.Text; | ||
using System.Threading; | ||
|
||
[SupportedOSPlatform("windows")] | ||
internal class WinGetPackageManager : IPackageManager | ||
{ | ||
private readonly ILogger<WinGetPackageManager> _logger; | ||
private readonly IPathResolver _pathResolver; | ||
private readonly IProcessExecutor _processExecutor; | ||
private readonly ISimpleDownloadProgress _simpleDownloadProgress; | ||
private readonly IGlobalMutexReservationManager _globalMutexReservationManager; | ||
|
||
public WinGetPackageManager( | ||
ILogger<WinGetPackageManager> logger, | ||
IPathResolver pathResolver, | ||
IProcessExecutor processExecutor, | ||
ISimpleDownloadProgress simpleDownloadProgress, | ||
IReservationManagerFactory reservationManagerFactory) | ||
{ | ||
_logger = logger; | ||
_pathResolver = pathResolver; | ||
_processExecutor = processExecutor; | ||
_simpleDownloadProgress = simpleDownloadProgress; | ||
_globalMutexReservationManager = reservationManagerFactory.CreateGlobalMutexReservationManager(); | ||
} | ||
|
||
private async Task<string> FindPwshOrInstallItAsync(CancellationToken cancellationToken) | ||
{ | ||
// Try to find PowerShell 7 via PATH. The WinGet CLI doesn't work under SYSTEM (even with absolute path) due to MSIX nonsense, but apparently the PowerShell scripts use a COM API that does? | ||
try | ||
{ | ||
return await _pathResolver.ResolveBinaryPath("pwsh").ConfigureAwait(false); | ||
} | ||
catch (FileNotFoundException) | ||
{ | ||
} | ||
|
||
await using (await _globalMutexReservationManager.ReserveExactAsync("PwshInstall", cancellationToken)) | ||
{ | ||
try | ||
{ | ||
return await _pathResolver.ResolveBinaryPath("pwsh").ConfigureAwait(false); | ||
} | ||
catch (FileNotFoundException) | ||
{ | ||
} | ||
|
||
_logger.LogInformation($"Downloading PowerShell Core..."); | ||
using var client = new HttpClient(); | ||
|
||
var targetPath = Path.Combine(Path.GetTempPath(), "PowerShell-7.4.6-win-x64.msi"); | ||
using (var file = new FileStream(targetPath, FileMode.Create, FileAccess.Write)) | ||
{ | ||
await _simpleDownloadProgress.DownloadAndCopyToStreamAsync( | ||
client, | ||
new Uri("https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/PowerShell-7.4.6-win-x64.msi"), | ||
async stream => await stream.CopyToAsync(file, cancellationToken).ConfigureAwait(false), | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
_logger.LogInformation($"Installing PowerShell Core..."); | ||
await _processExecutor.ExecuteAsync( | ||
new ProcessSpecification | ||
{ | ||
FilePath = @"C:\WINDOWS\system32\msiexec.exe", | ||
Arguments = new LogicalProcessArgument[] | ||
{ | ||
"/a", | ||
targetPath, | ||
"/quiet", | ||
"/qn", | ||
"ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1", | ||
"ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1", | ||
"ADD_PATH=1", | ||
"DISABLE_TELEMETRY=1", | ||
"USE_MU=1", | ||
"ENABLE_MU=1" | ||
}, | ||
WorkingDirectory = Path.GetTempPath() | ||
}, CaptureSpecification.Passthrough, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
return await _pathResolver.ResolveBinaryPath("pwsh").ConfigureAwait(false); | ||
} | ||
|
||
public async Task InstallOrUpgradePackageToLatestAsync(string packageId, CancellationToken cancellationToken) | ||
{ | ||
var pwsh = await FindPwshOrInstallItAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
await using (await _globalMutexReservationManager.TryReserveExactAsync($"PackageInstall-{packageId}").ConfigureAwait(false)) | ||
{ | ||
_logger.LogInformation($"Ensuring {packageId} is installed and is up-to-date..."); | ||
var script = | ||
$$""" | ||
if ($null -eq (Get-InstalledModule -ErrorAction SilentlyContinue -Name {{packageId}})) { | ||
Write-Host "Installing WinGet PowerShell module because it's not currently installed..."; | ||
Install-Module -Name Microsoft.WinGet.Client -Force; | ||
} | ||
$InstalledPackage = (Get-WinGetPackage -Id {{packageId}} -ErrorAction SilentlyContinue); | ||
if ($null -eq $InstalledPackage) { | ||
Write-Host "Installing {{packageId}} because it's not currently installed..."; | ||
Install-WinGetPackage -Id {{packageId}} -Mode Silent; | ||
exit 0; | ||
} elseif ($InstalledPackage.Version -ne (Find-WinGetPackage -Id {{packageId}}).Version) { | ||
Write-Host "Updating {{packageId}} because it's not the latest version..."; | ||
Update-WinGetPackage -Id {{packageId}} -Mode Silent; | ||
exit 0; | ||
} | ||
"""; | ||
var encodedScript = Convert.ToBase64String(Encoding.Unicode.GetBytes(script)); | ||
|
||
await _processExecutor.ExecuteAsync( | ||
new ProcessSpecification | ||
{ | ||
FilePath = pwsh, | ||
Arguments = [ | ||
"-NonInteractive", | ||
"-OutputFormat", | ||
"Text", | ||
"-EncodedCommand", | ||
encodedScript, | ||
] | ||
}, | ||
CaptureSpecification.Passthrough, | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace Redpoint.Reservation | ||
{ | ||
/// <summary> | ||
/// Represents a lock obtained on a global mutex. You must call <see cref="IAsyncDisposable.DisposeAsync"/> | ||
/// once you are finished with the reservation. | ||
/// </summary> | ||
public interface IGlobalMutexReservation : IAsyncDisposable | ||
{ | ||
} | ||
} |
Oops, something went wrong.