Implementing cancellation for MSBuild tasks

This commit is contained in:
Kevin Bost
2024-05-22 22:29:24 -07:00
parent 1dbad49ae4
commit 4ae5bbf356
7 changed files with 79 additions and 64 deletions

View File

@@ -1,13 +1,17 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using MSBuildTask = Microsoft.Build.Utilities.Task;
namespace Velopack.Build;
public abstract class MSBuildAsyncTask : MSBuildTask
public abstract class MSBuildAsyncTask : MSBuildTask, ICancelableTask
{
protected MSBuildLogger Logger { get; }
private CancellationTokenSource CancellationTokenSource { get; } = new();
protected MSBuildAsyncTask()
{
Logger = new MSBuildLogger(Log);
@@ -15,8 +19,18 @@ public abstract class MSBuildAsyncTask : MSBuildTask
public sealed override bool Execute()
{
CancellationToken token = CancellationTokenSource.Token;
try {
return Task.Run(ExecuteAsync).Result;
return Task.Run(async () => {
try {
return await ExecuteAsync(token).ConfigureAwait(false);
} catch (OperationCanceledException) {
return false;
} catch (Exception ex) {
Log.LogErrorFromException(ex, true, true, null);
return false;
}
}, token).Result;
} catch (AggregateException ex) {
ex.Flatten().Handle((x) => {
Log.LogError(x.Message);
@@ -26,5 +40,7 @@ public abstract class MSBuildAsyncTask : MSBuildTask
}
}
protected abstract Task<bool> ExecuteAsync();
protected abstract Task<bool> ExecuteAsync(CancellationToken cancellationToken);
public void Cancel() => CancellationTokenSource.Cancel();
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Velopack.Packaging;
@@ -90,7 +91,7 @@ public class PackTask : MSBuildAsyncTask
public string? Categories { get; set; }
protected override async Task<bool> ExecuteAsync()
protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
//System.Diagnostics.Debugger.Launch();
try {

View File

@@ -1,5 +1,6 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
@@ -9,9 +10,8 @@ namespace Velopack.Build;
public class PublishTask : MSBuildAsyncTask
{
private static HttpClient HttpClient { get; } = new(new HmacAuthHttpClientHandler
{
InnerHandler = new HttpClientHandler()
private static HttpClient HttpClient { get; } = new(new HmacAuthHttpClientHandler {
InnerHandler = new HttpClientHandler()
}) {
Timeout = TimeSpan.FromMinutes(10)
};
@@ -25,29 +25,23 @@ public class PublishTask : MSBuildAsyncTask
public string? ApiKey { get; set; }
protected override async Task<bool> ExecuteAsync()
protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
//System.Diagnostics.Debugger.Launch();
try {
IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger);
if (!await client.LoginAsync(new() {
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
VelopackBaseUrl = ServiceUrl,
ApiKey = ApiKey
}).ConfigureAwait(false)) {
Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
return true;
}
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl)
.ConfigureAwait(false);
IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger);
if (!await client.LoginAsync(new() {
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
VelopackBaseUrl = ServiceUrl,
ApiKey = ApiKey
}, cancellationToken).ConfigureAwait(false)) {
Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
return true;
} catch (Exception ex) {
Log.LogErrorFromException(ex, true, true, null);
return false;
}
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl, cancellationToken)
.ConfigureAwait(false);
return true;
}
}

View File

@@ -14,12 +14,12 @@ namespace Velopack.Packaging.Flow;
public interface IVelopackFlowServiceClient
{
Task<bool> LoginAsync(VelopackLoginOptions? options = null);
Task LogoutAsync(VelopackServiceOptions? options = null);
Task<bool> LoginAsync(VelopackLoginOptions? options, CancellationToken cancellationToken);
Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task<Profile?> GetProfileAsync(VelopackServiceOptions? options = null);
Task<Profile?> GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl);
Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, CancellationToken cancellationToken);
}
public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : IVelopackFlowServiceClient
@@ -30,35 +30,35 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
private AuthConfiguration? AuthConfiguration { get; set; }
public async Task<bool> LoginAsync(VelopackLoginOptions? options = null)
public async Task<bool> LoginAsync(VelopackLoginOptions? options, CancellationToken cancellationToken)
{
options ??= new VelopackLoginOptions();
Logger.LogInformation("Preparing to login to Velopack ({ServiceUrl})", options.VelopackBaseUrl);
var authConfiguration = await GetAuthConfigurationAsync(options);
var authConfiguration = await GetAuthConfigurationAsync(options, cancellationToken);
var pca = await BuildPublicApplicationAsync(authConfiguration);
if (!string.IsNullOrWhiteSpace(options.ApiKey)) {
HttpClient.DefaultRequestHeaders.Authorization = new(HmacHelper.HmacScheme, options.ApiKey);
var profile = await GetProfileAsync(options);
var profile = await GetProfileAsync(options, cancellationToken);
Logger.LogInformation("{UserName} logged into Velopack with API key", profile?.GetDisplayName());
return true;
} else {
AuthenticationResult? rv = null;
if (options.AllowCacheCredentials) {
rv = await AcquireSilentlyAsync(pca);
rv = await AcquireSilentlyAsync(pca, cancellationToken);
}
if (rv is null && options.AllowInteractiveLogin) {
rv = await AcquireInteractiveAsync(pca, authConfiguration);
rv = await AcquireInteractiveAsync(pca, authConfiguration, cancellationToken);
}
if (rv is null && options.AllowDeviceCodeFlow) {
rv = await AcquireByDeviceCodeAsync(pca);
rv = await AcquireByDeviceCodeAsync(pca, cancellationToken);
}
if (rv != null) {
HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken);
var profile = await GetProfileAsync(options);
var profile = await GetProfileAsync(options, cancellationToken);
Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName());
return true;
@@ -69,9 +69,9 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
}
}
public async Task LogoutAsync(VelopackServiceOptions? options = null)
public async Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
{
var authConfiguration = await GetAuthConfigurationAsync(options);
var authConfiguration = await GetAuthConfigurationAsync(options, cancellationToken);
var pca = await BuildPublicApplicationAsync(authConfiguration);
@@ -83,15 +83,15 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
Logger.LogInformation("Cleared saved login(s) for Velopack");
}
public async Task<Profile?> GetProfileAsync(VelopackServiceOptions? options = null)
public async Task<Profile?> GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
{
AssertAuthenticated();
var endpoint = GetEndpoint("v1/user/profile", options);
return await HttpClient.GetFromJsonAsync<Profile>(endpoint);
return await HttpClient.GetFromJsonAsync<Profile>(endpoint, cancellationToken);
}
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl)
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, CancellationToken cancellationToken)
{
channel ??= ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs);
ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger);
@@ -130,7 +130,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
VelopackBaseUrl = serviceUrl
};
await UploadReleaseAssetAsync(options).ConfigureAwait(false);
await UploadReleaseAssetAsync(options, cancellationToken).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName);
}
@@ -143,13 +143,13 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
VelopackBaseUrl = serviceUrl
};
await UploadInstallerAssetAsync(options).ConfigureAwait(false);
await UploadInstallerAssetAsync(options, cancellationToken).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile);
}
}
private async Task UploadReleaseAssetAsync(UploadOptions options)
private async Task UploadReleaseAssetAsync(UploadOptions options, CancellationToken cancellationToken)
{
AssertAuthenticated();
@@ -163,12 +163,12 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
var endpoint = GetEndpoint("v1/upload-release", options);
var response = await HttpClient.PostAsync(endpoint, formData);
var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken);
response.EnsureSuccessStatusCode();
}
private async Task UploadInstallerAssetAsync(UploadInstallerOptions options)
private async Task UploadInstallerAssetAsync(UploadInstallerOptions options, CancellationToken cancellationToken)
{
AssertAuthenticated();
@@ -184,19 +184,19 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
var endpoint = GetEndpoint("v1/upload-installer", options);
var response = await HttpClient.PostAsync(endpoint, formData);
var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken);
response.EnsureSuccessStatusCode();
}
private async Task<AuthConfiguration> GetAuthConfigurationAsync(VelopackServiceOptions? options)
private async Task<AuthConfiguration> GetAuthConfigurationAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
{
if (AuthConfiguration is not null)
return AuthConfiguration;
var endpoint = GetEndpoint("v1/auth/config", options);
var authConfig = await HttpClient.GetFromJsonAsync<AuthConfiguration>(endpoint);
var authConfig = await HttpClient.GetFromJsonAsync<AuthConfiguration>(endpoint, cancellationToken);
if (authConfig is null)
throw new Exception("Failed to get auth configuration.");
if (authConfig.B2CAuthority is null)
@@ -223,13 +223,13 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
}
}
private static async Task<AuthenticationResult?> AcquireSilentlyAsync(IPublicClientApplication pca)
private static async Task<AuthenticationResult?> AcquireSilentlyAsync(IPublicClientApplication pca, CancellationToken cancellationToken)
{
foreach (var account in await pca.GetAccountsAsync()) {
try {
if (account is not null) {
return await pca.AcquireTokenSilent(Scopes, account)
.ExecuteAsync();
.ExecuteAsync(cancellationToken);
}
} catch (MsalException) {
await pca.RemoveAsync(account);
@@ -239,18 +239,18 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
return null;
}
private static async Task<AuthenticationResult?> AcquireInteractiveAsync(IPublicClientApplication pca, AuthConfiguration authConfiguration)
private static async Task<AuthenticationResult?> AcquireInteractiveAsync(IPublicClientApplication pca, AuthConfiguration authConfiguration, CancellationToken cancellationToken)
{
try {
return await pca.AcquireTokenInteractive(Scopes)
.WithB2CAuthority(authConfiguration.B2CAuthority)
.ExecuteAsync();
.ExecuteAsync(cancellationToken);
} catch (MsalException) {
}
return null;
}
private async Task<AuthenticationResult?> AcquireByDeviceCodeAsync(IPublicClientApplication pca)
private async Task<AuthenticationResult?> AcquireByDeviceCodeAsync(IPublicClientApplication pca, CancellationToken cancellationToken)
{
try {
var result = await pca.AcquireTokenWithDeviceCode(Scopes,
@@ -267,7 +267,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
// If this occurs, an OperationCanceledException will be thrown (see catch below for more details).
Logger.LogInformation(deviceCodeResult.Message);
return Task.FromResult(0);
}).ExecuteAsync();
}).ExecuteAsync(cancellationToken);
Logger.LogInformation(result.Account.Username);
return result;

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging.Abstractions;
using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
@@ -11,6 +12,6 @@ public class LoginCommandRunner(IVelopackFlowServiceClient Client) : ICommand<Lo
await Client.LoginAsync(new() {
VelopackBaseUrl = options.VelopackBaseUrl,
ApiKey = options.ApiKey,
});
}, CancellationToken.None);
}
}

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging.Abstractions;
using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
#nullable enable
@@ -8,6 +9,6 @@ internal class LogoutCommandRunner(IVelopackFlowServiceClient Client) : ICommand
{
public async Task Run(LogoutOptions options)
{
await Client.LogoutAsync(options);
await Client.LogoutAsync(options, CancellationToken.None);
}
}

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging.Abstractions;
using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
@@ -7,16 +8,17 @@ public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand<
{
public async Task Run(PublishOptions options)
{
CancellationToken token = CancellationToken.None;
if (!await Client.LoginAsync(new VelopackLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl
})) {
}, token)) {
return;
}
await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, options.VelopackBaseUrl);
await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, options.VelopackBaseUrl, token);
}
}