diff --git a/src/Velopack.Build/MSBuildAsyncTask.cs b/src/Velopack.Build/MSBuildAsyncTask.cs index 31432887..13596c8f 100644 --- a/src/Velopack.Build/MSBuildAsyncTask.cs +++ b/src/Velopack.Build/MSBuildAsyncTask.cs @@ -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 ExecuteAsync(); + protected abstract Task ExecuteAsync(CancellationToken cancellationToken); + + public void Cancel() => CancellationTokenSource.Cancel(); } diff --git a/src/Velopack.Build/PackTask.cs b/src/Velopack.Build/PackTask.cs index 966512b2..d544defd 100644 --- a/src/Velopack.Build/PackTask.cs +++ b/src/Velopack.Build/PackTask.cs @@ -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 ExecuteAsync() + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { //System.Diagnostics.Debugger.Launch(); try { diff --git a/src/Velopack.Build/PublishTask.cs b/src/Velopack.Build/PublishTask.cs index 85cadcbe..a4bf0dc3 100644 --- a/src/Velopack.Build/PublishTask.cs +++ b/src/Velopack.Build/PublishTask.cs @@ -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 ExecuteAsync() + protected override async Task 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; } } diff --git a/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs b/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs index f83365a6..03ca6c4e 100644 --- a/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs +++ b/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs @@ -14,12 +14,12 @@ namespace Velopack.Packaging.Flow; public interface IVelopackFlowServiceClient { - Task LoginAsync(VelopackLoginOptions? options = null); - Task LogoutAsync(VelopackServiceOptions? options = null); + Task LoginAsync(VelopackLoginOptions? options, CancellationToken cancellationToken); + Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken); - Task GetProfileAsync(VelopackServiceOptions? options = null); + Task 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 LoginAsync(VelopackLoginOptions? options = null) + public async Task 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 GetProfileAsync(VelopackServiceOptions? options = null) + public async Task GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken) { AssertAuthenticated(); var endpoint = GetEndpoint("v1/user/profile", options); - return await HttpClient.GetFromJsonAsync(endpoint); + return await HttpClient.GetFromJsonAsync(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 GetAuthConfigurationAsync(VelopackServiceOptions? options) + private async Task GetAuthConfigurationAsync(VelopackServiceOptions? options, CancellationToken cancellationToken) { if (AuthConfiguration is not null) return AuthConfiguration; var endpoint = GetEndpoint("v1/auth/config", options); - var authConfig = await HttpClient.GetFromJsonAsync(endpoint); + var authConfig = await HttpClient.GetFromJsonAsync(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 AcquireSilentlyAsync(IPublicClientApplication pca) + private static async Task 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 AcquireInteractiveAsync(IPublicClientApplication pca, AuthConfiguration authConfiguration) + private static async Task 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 AcquireByDeviceCodeAsync(IPublicClientApplication pca) + private async Task 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; diff --git a/src/Velopack.Vpk/Commands/Flow/LoginCommandRunner.cs b/src/Velopack.Vpk/Commands/Flow/LoginCommandRunner.cs index 4b8ccc90..f6ad3d27 100644 --- a/src/Velopack.Vpk/Commands/Flow/LoginCommandRunner.cs +++ b/src/Velopack.Vpk/Commands/Flow/LoginCommandRunner.cs @@ -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