diff --git a/UnitTests/PInvoke/DOSvc/DOSvcTests.cs b/UnitTests/PInvoke/DOSvc/DOSvcTests.cs index 085a0c26..ba7af1fc 100644 --- a/UnitTests/PInvoke/DOSvc/DOSvcTests.cs +++ b/UnitTests/PInvoke/DOSvc/DOSvcTests.cs @@ -2,51 +2,110 @@ using NUnit.Framework.Internal; using System.IO; using System.Threading; +using System.Threading.Tasks; using Vanara.Utilities; using static Vanara.PInvoke.DOSvc; +using static Vanara.PInvoke.Ole32; namespace Vanara.PInvoke.Tests; [TestFixture] public class DOSvcTests { - [Test] - public async void Test() - { - const string name = "Test download"; - string id = Guid.NewGuid().ToString("N"); + private const string name = "Test download"; + private readonly string uri = "https://github.com/EWSoftware/SHFB/releases/download/2023.7.8.0/SHFBInstaller_2023.7.8.0.zip"; + [OneTimeSetUp] + public void _Setup() => CoInitializeSecurity(PSECURITY_DESCRIPTOR.NULL, -1, null, default, Rpc.RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, + Rpc.RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE, dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_STATIC_CLOAKING).ThrowIfFailed(); + + [Test] + public async Task TestAsync() + { using TemporaryDirectory tempRoot = new(); - string uri = "https://github.com/EWSoftware/SHFB/releases/download/2023.7.8.0/SHFBInstaller_2023.7.8.0.zip"; string dest = tempRoot.RandomTxtFileFullPath; + var tw = TestContext.Out; Progress progress = new(); - progress.ProgressChanged += (s, e) => TestContext.WriteLine(e.State); + progress.ProgressChanged += (s, e) => tw.WriteLine($"{e.State} - {100*e.BytesTransferred/e.BytesTotal}%"); CancellationTokenSource cancel = new(); - await Downloader.DownloadAsync(uri, dest, progress, cancel.Token, true, name, id); + var ret = await Downloader.DownloadAsync(uri, dest, true, name, true, null, progress, null, cancel.Token); Assert.That(File.Exists(dest), Is.True); + Assert.That(ret, Is.Not.Empty); + } - //IDOManager mgr = new(); - //IDODownload dnld = mgr.CreateDownload(); - //CoSetProxyBlanket(dnld, dwImpLevel: Rpc.RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE/*, dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_STATIC_CLOAKING*/).ThrowIfFailed(); + [Test] + public void TestAsyncCancel() + { + using TemporaryDirectory tempRoot = new(); + string dest = tempRoot.RandomTxtFileFullPath; - //Assert.That(() => dnld.SetProperty(DODownloadProperty.DODownloadProperty_Uri, uri), Throws.Nothing); - //Assert.That((string)dnld.GetProperty(DODownloadProperty.DODownloadProperty_Uri), Is.EqualTo(uri)); - //Assert.That(() => dnld.SetProperty(DODownloadProperty.DODownloadProperty_ForegroundPriority, true), Throws.Nothing); - //Assert.That((bool)dnld.GetProperty(DODownloadProperty.DODownloadProperty_ForegroundPriority), Is.True); - //Assert.That(() => dnld.SetProperty(DODownloadProperty.DODownloadProperty_LocalPath, dest), Throws.Nothing); - //Assert.That((string)dnld.GetProperty(DODownloadProperty.DODownloadProperty_LocalPath), Is.EqualTo(dest)); - //Assert.That(() => dnld.SetProperty(DODownloadProperty.DODownloadProperty_DisplayName, name), Throws.Nothing); - //Assert.That((string)dnld.GetProperty(DODownloadProperty.DODownloadProperty_DisplayName), Is.EqualTo(name)); + var tw = TestContext.Out; + Progress progress = new(); + progress.ProgressChanged += (s, e) => tw.WriteLine($"{e.State} - {100*e.BytesTransferred/e.BytesTotal}%"); + CancellationTokenSource cancel = new(); + var task = Downloader.DownloadAsync(uri, dest, false, name, true, null, progress, null, cancel.Token); + cancel.Cancel(); + Assert.That(task.IsCanceled, Is.True); + } - //AutoResetEvent done = new(false); - //Callback callback = new(); - //callback.StatusChange += (s) => { if (s.State == DODownloadState.DODownloadState_Finalized) done.Set(); else if (s.Error.Failed) throw s.Error.GetException()!; }; - //object wrp = new UnknownWrapper(callback); - //Assert.That(() => dnld.SetProperty(DODownloadProperty.DODownloadProperty_CallbackInterface, wrp), Throws.Nothing); + [Test] + public void Test() + { + SetupDownload(out var dnld, out var tempRoot, out var dest, out var done); - //dnld.Start(); - //done.WaitOne(); - //dnld.Finalize(); + dnld.Start(); + Assert.That(done.WaitOne(TimeSpan.FromMinutes(1)), Is.True); + dnld.Finalize(); + Thread.Sleep(250); + Assert.That(File.Exists(dest), Is.True); + } + + [Test] + public void TestAbort() + { + SetupDownload(out var dnld, out var tempRoot, out var dest, out var done); + + dnld.Start(); + dnld.Abort(); + Assert.That(dnld.GetStatus().State, Is.EqualTo(DODownloadState.DODownloadState_Aborted)); + Assert.That(() => dnld.Finalize(), Throws.Exception); + Assert.That(File.Exists(dest), Is.False); + } + + private void SetupDownload(out IDODownload dnld, out TemporaryDirectory tempRoot, out string dest, out AutoResetEvent doneEvent) + { + IDOManager mgr = new(); + IDODownload download = dnld = mgr.CreateDownload(); + CoSetProxyBlanket(download, dwImpLevel: Rpc.RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE/*, dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_STATIC_CLOAKING*/).ThrowIfFailed(); + + var done = doneEvent = new(false); + Callback callback = new(); + var tw = TestContext.Out; + callback.StatusChange += (s) => { tw.WriteLine("========"); tw.WriteLine(s.GetStringVal()); if (s.State is DODownloadState.DODownloadState_Transferred or DODownloadState.DODownloadState_Finalized) done.Set(); else if (s.Error.Failed) throw s.Error.GetException()!; }; + Assert.That(() => download.SetProperty(DODownloadProperty.DODownloadProperty_CallbackInterface, callback), Throws.Nothing); + + tempRoot = new(); + string destination = dest = tempRoot.RandomTxtFileFullPath; + Assert.That(() => download.SetProperty(DODownloadProperty.DODownloadProperty_Uri, uri), Throws.Nothing); + Assert.That((string)download.GetProperty(DODownloadProperty.DODownloadProperty_Uri), Is.EqualTo(uri)); + Assert.That(() => download.SetProperty(DODownloadProperty.DODownloadProperty_LocalPath, destination), Throws.Nothing); + Assert.That((string)download.GetProperty(DODownloadProperty.DODownloadProperty_LocalPath), Is.EqualTo(dest)); + Assert.That(() => download.SetProperty(DODownloadProperty.DODownloadProperty_DisplayName, name), Throws.Nothing); + Assert.That((string)download.GetProperty(DODownloadProperty.DODownloadProperty_DisplayName), Is.EqualTo(name)); + Assert.That(() => download.SetProperty(DODownloadProperty.DODownloadProperty_ForegroundPriority, true), Throws.Nothing); + Assert.That((bool)download.GetProperty(DODownloadProperty.DODownloadProperty_ForegroundPriority), Is.True); + } + + [ComVisible(true), Guid("90AFD61C-C21C-4627-8A9A-E3268BC89051")] + public class Callback : IDODownloadStatusCallback + { + public event Action? StatusChange; + + HRESULT IDODownloadStatusCallback.OnStatusChange(IDODownload download, in DO_DOWNLOAD_STATUS status) + { + StatusChange?.Invoke(status); + return HRESULT.S_OK; + } } } \ No newline at end of file diff --git a/UnitTests/PInvoke/DOSvc/Downloader.cs b/UnitTests/PInvoke/DOSvc/Downloader.cs index 99acd78f..62349d2e 100644 --- a/UnitTests/PInvoke/DOSvc/Downloader.cs +++ b/UnitTests/PInvoke/DOSvc/Downloader.cs @@ -7,127 +7,99 @@ using Vanara.PInvoke; using static Vanara.PInvoke.DOSvc; using static Vanara.PInvoke.Ole32; -#nullable enable namespace Vanara.Utilities { public static class Downloader { - public static async Task DownloadAsync(string url, string destPath, IProgress? progress = null, CancellationToken cancellationToken = default, bool computeHash = true, string? displayName = null, string? contentId = null) + /// Downloads the asynchronous. + /// The remote URI path of the resource to download. + /// The local path name to save the download file. If the path does not exist, Delivery Optimization will attempt to create it under + /// the caller's privileges. + /// if set to [compute hash]. + /// Optional. Use this property to set the download display name. + /// Optional. Use this property to set or get the current download priority. value will bring the download to + /// the foreground with higher priority. The default is background priority. + /// Optional. An array of DO_DOWNLOAD_RANGE structures (to download only specific ranges of the file). Pass to download the entire file. + /// Optional. A instance to receive status information. + /// Optional. A timeout for the download. If the timeout expires, the download will be aborted. + /// Optional. A cancellation token. + /// + /// If is , a array with a SHA256 hash of the resulting file. + /// + public static Task DownloadAsync(string url, string destPath, bool computeHash = true, string? displayName = null, + bool foregroundPriority = false, DO_DOWNLOAD_RANGE[]? ranges = null, IProgress? progress = null, + TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - File.Delete(destPath); - + // Get interfaces and set security + CoInitializeSecurity(PSECURITY_DESCRIPTOR.NULL, -1, null, default, Rpc.RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, + Rpc.RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE, dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_STATIC_CLOAKING); IDOManager mgr = new(); IDODownload dnld = mgr.CreateDownload(); CoSetProxyBlanket(dnld, dwImpLevel: Rpc.RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE).ThrowIfFailed(); - dnld.SetProperty(DODownloadProperty.DODownloadProperty_Uri, url); - dnld.SetProperty(DODownloadProperty.DODownloadProperty_LocalPath, destPath); - dnld.SetProperty(DODownloadProperty.DODownloadProperty_ForegroundPriority, true); - DownloadStatusCallback callback = new(progress, cancellationToken); + // Create a task completion source + TaskCompletionSource tcs = new(); + + // Set properties + DownloadStatusCallback callback = new(Callback_StatusChange); dnld.SetProperty(DODownloadProperty.DODownloadProperty_CallbackInterface, callback); if (!string.IsNullOrEmpty(displayName)) dnld.SetProperty(DODownloadProperty.DODownloadProperty_DisplayName, displayName!); - if (!string.IsNullOrEmpty(contentId)) - dnld.SetProperty(DODownloadProperty.DODownloadProperty_ContentId, contentId!); + if (foregroundPriority) + dnld.SetProperty(DODownloadProperty.DODownloadProperty_ForegroundPriority, true); + if (timeout.HasValue) + dnld.SetProperty(DODownloadProperty.DODownloadProperty_NoProgressTimeoutSeconds, (uint)timeout.Value.TotalSeconds); + dnld.SetProperty(DODownloadProperty.DODownloadProperty_Uri, url); + dnld.SetProperty(DODownloadProperty.DODownloadProperty_LocalPath, destPath); - return await Task.Factory.StartNew(() => + // Start download + cancellationToken.Register(() => { dnld.Abort(); tcs.TrySetCanceled(cancellationToken); }, false); + dnld.Start(ranges); + + // Wait for completion or failure + return tcs.Task; + + // Process status change + void Callback_StatusChange(DO_DOWNLOAD_STATUS currentStatus) { - dnld.Start(); - cancellationToken.Register(dnld.Abort); - if (cancellationToken.IsCancellationRequested) + if (currentStatus.Error.Failed) { - dnld.Abort(); - return new byte[0]; + tcs.TrySetException(currentStatus.Error.GetException()!); + return; } - if (callback.Wait()) + Report(); + switch (currentStatus.State) { - dnld.Finalize(); - if (computeHash) - return SHA256.Create().ComputeHash(File.ReadAllBytes(destPath)); - } - return new byte[0]; - }, cancellationToken); - } + case DODownloadState.DODownloadState_Transferred: + dnld.Finalize(); + tcs.TrySetResult(computeHash && File.Exists(destPath) ? SHA256.Create().ComputeHash(File.ReadAllBytes(destPath)) : []); + break; - public static IEnumerable EnumDownloads(DODownloadProperty? category = null) => new IDOManager().EnumDownloads(category); - } - - [ComVisible(true), Guid("90AFD61C-C21C-4627-8A9A-E3268BC89051"), ClassInterface(ClassInterfaceType.None)] - public class DownloadStatusCallback : IDODownloadStatusCallback - { - private readonly CancellationToken cancel; - private readonly object lockObj = new(); - private readonly IProgress? progress; - private DO_DOWNLOAD_STATUS currentStatus = default; - - public DownloadStatusCallback(IProgress? progress, CancellationToken cancellationToken) - { - this.progress = progress; - cancel = cancellationToken; - } - - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); - - public bool Wait() - { - lock (lockObj) - { - bool transferChange = false; - ulong? initialTransferAmount = null; - while (!cancel.IsCancellationRequested) - { - if (!transferChange) - { - if (cancel.WaitHandle.WaitOne(Timeout) == false) - throw new TimeoutException(); - } - else - { - cancel.WaitHandle.WaitOne(); - } - - if (cancel.IsCancellationRequested) - return false; - - if (currentStatus.Error.Failed) - throw currentStatus.Error.GetException()!; - - switch (currentStatus.State) - { - case DODownloadState.DODownloadState_Created: - case DODownloadState.DODownloadState_Paused: - break; - - case DODownloadState.DODownloadState_Transferring: - if (currentStatus.BytesTransferred > 0 || currentStatus.BytesTotal > 0) - progress?.Report(currentStatus); - if (!initialTransferAmount.HasValue) - initialTransferAmount = currentStatus.BytesTransferred; - else if (currentStatus.BytesTransferred != initialTransferAmount.Value) - transferChange = true; - break; - - case DODownloadState.DODownloadState_Transferred: - case DODownloadState.DODownloadState_Finalized: - if (currentStatus.BytesTransferred > 0 || currentStatus.BytesTotal > 0) - progress?.Report(currentStatus); - return true; - - case DODownloadState.DODownloadState_Aborted: - return false; - } + case DODownloadState.DODownloadState_Aborted: + case DODownloadState.DODownloadState_Transferring: + case DODownloadState.DODownloadState_Created: + case DODownloadState.DODownloadState_Paused: + case DODownloadState.DODownloadState_Finalized: + break; } - return false; + void Report() { if (currentStatus.BytesTransferred > 0 || currentStatus.BytesTotal > 0) progress?.Report(currentStatus); } } } - HRESULT IDODownloadStatusCallback.OnStatusChange(IDODownload download, in DO_DOWNLOAD_STATUS status) + /// Enumerates the existing downloads. + /// The property name to be used as a category to enumerate. Passing will retrieve all existing downloads + /// An enumeration of the existing downloads. + public static IEnumerable EnumDownloads(DODownloadProperty? category = null) => new IDOManager().EnumDownloads(category); + + [ComVisible(true), Guid("90AFD61C-C21C-4627-8A9A-E3268BC89051")] + internal class DownloadStatusCallback(Action? StatusChange) : IDODownloadStatusCallback { - lock (lockObj) - currentStatus = status; - progress?.Report(currentStatus); - return HRESULT.S_OK; + HRESULT IDODownloadStatusCallback.OnStatusChange(IDODownload download, in DO_DOWNLOAD_STATUS status) + { + StatusChange?.Invoke(status); + return HRESULT.S_OK; + } } } } \ No newline at end of file