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;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Build.Framework;
using MSBuildTask = Microsoft.Build.Utilities.Task; using MSBuildTask = Microsoft.Build.Utilities.Task;
namespace Velopack.Build; namespace Velopack.Build;
public abstract class MSBuildAsyncTask : MSBuildTask public abstract class MSBuildAsyncTask : MSBuildTask, ICancelableTask
{ {
protected MSBuildLogger Logger { get; } protected MSBuildLogger Logger { get; }
private CancellationTokenSource CancellationTokenSource { get; } = new();
protected MSBuildAsyncTask() protected MSBuildAsyncTask()
{ {
Logger = new MSBuildLogger(Log); Logger = new MSBuildLogger(Log);
@@ -15,8 +19,18 @@ public abstract class MSBuildAsyncTask : MSBuildTask
public sealed override bool Execute() public sealed override bool Execute()
{ {
CancellationToken token = CancellationTokenSource.Token;
try { 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) { } catch (AggregateException ex) {
ex.Flatten().Handle((x) => { ex.Flatten().Handle((x) => {
Log.LogError(x.Message); 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;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
using Velopack.Packaging; using Velopack.Packaging;
@@ -90,7 +91,7 @@ public class PackTask : MSBuildAsyncTask
public string? Categories { get; set; } public string? Categories { get; set; }
protected override async Task<bool> ExecuteAsync() protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{ {
//System.Diagnostics.Debugger.Launch(); //System.Diagnostics.Debugger.Launch();
try { try {

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -9,9 +10,8 @@ namespace Velopack.Build;
public class PublishTask : MSBuildAsyncTask public class PublishTask : MSBuildAsyncTask
{ {
private static HttpClient HttpClient { get; } = new(new HmacAuthHttpClientHandler private static HttpClient HttpClient { get; } = new(new HmacAuthHttpClientHandler {
{ InnerHandler = new HttpClientHandler()
InnerHandler = new HttpClientHandler()
}) { }) {
Timeout = TimeSpan.FromMinutes(10) Timeout = TimeSpan.FromMinutes(10)
}; };
@@ -25,29 +25,23 @@ public class PublishTask : MSBuildAsyncTask
public string? ApiKey { get; set; } public string? ApiKey { get; set; }
protected override async Task<bool> ExecuteAsync() protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{ {
//System.Diagnostics.Debugger.Launch(); //System.Diagnostics.Debugger.Launch();
try { IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger);
IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger); if (!await client.LoginAsync(new() {
if (!await client.LoginAsync(new() { AllowDeviceCodeFlow = false,
AllowDeviceCodeFlow = false, AllowInteractiveLogin = false,
AllowInteractiveLogin = false, VelopackBaseUrl = ServiceUrl,
VelopackBaseUrl = ServiceUrl, ApiKey = ApiKey
ApiKey = ApiKey }, cancellationToken).ConfigureAwait(false)) {
}).ConfigureAwait(false)) { Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
return true;
}
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl)
.ConfigureAwait(false);
return true; 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 public interface IVelopackFlowServiceClient
{ {
Task<bool> LoginAsync(VelopackLoginOptions? options = null); Task<bool> LoginAsync(VelopackLoginOptions? options, CancellationToken cancellationToken);
Task LogoutAsync(VelopackServiceOptions? options = null); 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 public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : IVelopackFlowServiceClient
@@ -30,35 +30,35 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
private AuthConfiguration? AuthConfiguration { get; set; } 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(); options ??= new VelopackLoginOptions();
Logger.LogInformation("Preparing to login to Velopack ({ServiceUrl})", options.VelopackBaseUrl); 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); var pca = await BuildPublicApplicationAsync(authConfiguration);
if (!string.IsNullOrWhiteSpace(options.ApiKey)) { if (!string.IsNullOrWhiteSpace(options.ApiKey)) {
HttpClient.DefaultRequestHeaders.Authorization = new(HmacHelper.HmacScheme, 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()); Logger.LogInformation("{UserName} logged into Velopack with API key", profile?.GetDisplayName());
return true; return true;
} else { } else {
AuthenticationResult? rv = null; AuthenticationResult? rv = null;
if (options.AllowCacheCredentials) { if (options.AllowCacheCredentials) {
rv = await AcquireSilentlyAsync(pca); rv = await AcquireSilentlyAsync(pca, cancellationToken);
} }
if (rv is null && options.AllowInteractiveLogin) { if (rv is null && options.AllowInteractiveLogin) {
rv = await AcquireInteractiveAsync(pca, authConfiguration); rv = await AcquireInteractiveAsync(pca, authConfiguration, cancellationToken);
} }
if (rv is null && options.AllowDeviceCodeFlow) { if (rv is null && options.AllowDeviceCodeFlow) {
rv = await AcquireByDeviceCodeAsync(pca); rv = await AcquireByDeviceCodeAsync(pca, cancellationToken);
} }
if (rv != null) { if (rv != null) {
HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken); 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()); Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName());
return true; 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); var pca = await BuildPublicApplicationAsync(authConfiguration);
@@ -83,15 +83,15 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
Logger.LogInformation("Cleared saved login(s) for Velopack"); 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(); AssertAuthenticated();
var endpoint = GetEndpoint("v1/user/profile", options); 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); channel ??= ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs);
ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger); ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger);
@@ -130,7 +130,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
VelopackBaseUrl = serviceUrl VelopackBaseUrl = serviceUrl
}; };
await UploadReleaseAssetAsync(options).ConfigureAwait(false); await UploadReleaseAssetAsync(options, cancellationToken).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName); Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName);
} }
@@ -143,13 +143,13 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
VelopackBaseUrl = serviceUrl VelopackBaseUrl = serviceUrl
}; };
await UploadInstallerAssetAsync(options).ConfigureAwait(false); await UploadInstallerAssetAsync(options, cancellationToken).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile); Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile);
} }
} }
private async Task UploadReleaseAssetAsync(UploadOptions options) private async Task UploadReleaseAssetAsync(UploadOptions options, CancellationToken cancellationToken)
{ {
AssertAuthenticated(); AssertAuthenticated();
@@ -163,12 +163,12 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
var endpoint = GetEndpoint("v1/upload-release", options); var endpoint = GetEndpoint("v1/upload-release", options);
var response = await HttpClient.PostAsync(endpoint, formData); var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
} }
private async Task UploadInstallerAssetAsync(UploadInstallerOptions options) private async Task UploadInstallerAssetAsync(UploadInstallerOptions options, CancellationToken cancellationToken)
{ {
AssertAuthenticated(); AssertAuthenticated();
@@ -184,19 +184,19 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
var endpoint = GetEndpoint("v1/upload-installer", options); var endpoint = GetEndpoint("v1/upload-installer", options);
var response = await HttpClient.PostAsync(endpoint, formData); var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
} }
private async Task<AuthConfiguration> GetAuthConfigurationAsync(VelopackServiceOptions? options) private async Task<AuthConfiguration> GetAuthConfigurationAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
{ {
if (AuthConfiguration is not null) if (AuthConfiguration is not null)
return AuthConfiguration; return AuthConfiguration;
var endpoint = GetEndpoint("v1/auth/config", options); 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) if (authConfig is null)
throw new Exception("Failed to get auth configuration."); throw new Exception("Failed to get auth configuration.");
if (authConfig.B2CAuthority is null) 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()) { foreach (var account in await pca.GetAccountsAsync()) {
try { try {
if (account is not null) { if (account is not null) {
return await pca.AcquireTokenSilent(Scopes, account) return await pca.AcquireTokenSilent(Scopes, account)
.ExecuteAsync(); .ExecuteAsync(cancellationToken);
} }
} catch (MsalException) { } catch (MsalException) {
await pca.RemoveAsync(account); await pca.RemoveAsync(account);
@@ -239,18 +239,18 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) :
return null; 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 { try {
return await pca.AcquireTokenInteractive(Scopes) return await pca.AcquireTokenInteractive(Scopes)
.WithB2CAuthority(authConfiguration.B2CAuthority) .WithB2CAuthority(authConfiguration.B2CAuthority)
.ExecuteAsync(); .ExecuteAsync(cancellationToken);
} catch (MsalException) { } catch (MsalException) {
} }
return null; return null;
} }
private async Task<AuthenticationResult?> AcquireByDeviceCodeAsync(IPublicClientApplication pca) private async Task<AuthenticationResult?> AcquireByDeviceCodeAsync(IPublicClientApplication pca, CancellationToken cancellationToken)
{ {
try { try {
var result = await pca.AcquireTokenWithDeviceCode(Scopes, 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). // If this occurs, an OperationCanceledException will be thrown (see catch below for more details).
Logger.LogInformation(deviceCodeResult.Message); Logger.LogInformation(deviceCodeResult.Message);
return Task.FromResult(0); return Task.FromResult(0);
}).ExecuteAsync(); }).ExecuteAsync(cancellationToken);
Logger.LogInformation(result.Account.Username); Logger.LogInformation(result.Account.Username);
return result; return result;

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging.Abstractions; using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow; using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow; namespace Velopack.Vpk.Commands.Flow;
@@ -11,6 +12,6 @@ public class LoginCommandRunner(IVelopackFlowServiceClient Client) : ICommand<Lo
await Client.LoginAsync(new() { await Client.LoginAsync(new() {
VelopackBaseUrl = options.VelopackBaseUrl, VelopackBaseUrl = options.VelopackBaseUrl,
ApiKey = options.ApiKey, 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; using Velopack.Packaging.Flow;
#nullable enable #nullable enable
@@ -8,6 +9,6 @@ internal class LogoutCommandRunner(IVelopackFlowServiceClient Client) : ICommand
{ {
public async Task Run(LogoutOptions options) 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; using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow; namespace Velopack.Vpk.Commands.Flow;
@@ -7,16 +8,17 @@ public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand<
{ {
public async Task Run(PublishOptions options) public async Task Run(PublishOptions options)
{ {
CancellationToken token = CancellationToken.None;
if (!await Client.LoginAsync(new VelopackLoginOptions() { if (!await Client.LoginAsync(new VelopackLoginOptions() {
AllowCacheCredentials = true, AllowCacheCredentials = true,
AllowDeviceCodeFlow = false, AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false, AllowInteractiveLogin = false,
ApiKey = options.ApiKey, ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl VelopackBaseUrl = options.VelopackBaseUrl
})) { }, token)) {
return; return;
} }
await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, options.VelopackBaseUrl); await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, options.VelopackBaseUrl, token);
} }
} }