Corrected problem with test code for DoSvc (#465).

master
David Hall 2024-05-28 13:02:05 -06:00
parent 7837934295
commit f652be90ab
2 changed files with 155 additions and 124 deletions

View File

@ -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<DO_DOWNLOAD_STATUS> 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<DO_DOWNLOAD_STATUS> 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<DO_DOWNLOAD_STATUS>? StatusChange;
HRESULT IDODownloadStatusCallback.OnStatusChange(IDODownload download, in DO_DOWNLOAD_STATUS status)
{
StatusChange?.Invoke(status);
return HRESULT.S_OK;
}
}
}

View File

@ -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<byte[]> DownloadAsync(string url, string destPath, IProgress<DO_DOWNLOAD_STATUS>? progress = null, CancellationToken cancellationToken = default, bool computeHash = true, string? displayName = null, string? contentId = null)
/// <summary>Downloads the asynchronous.</summary>
/// <param name="url">The remote URI path of the resource to download.</param>
/// <param name="destPath">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.</param>
/// <param name="computeHash">if set to <see langword="true" /> [compute hash].</param>
/// <param name="displayName">Optional. Use this property to set the download display name.</param>
/// <param name="foregroundPriority">Optional. Use this property to set or get the current download priority. <see langword="true" /> value will bring the download to
/// the foreground with higher priority. The default is background priority.</param>
/// <param name="ranges">Optional. An array of <c>DO_DOWNLOAD_RANGE</c> structures (to download only specific ranges of the file). Pass <see langword="null" /> to download the entire file.</param>
/// <param name="progress">Optional. A <see cref="IProgress{T}" /> instance to receive status information.</param>
/// <param name="timeout">Optional. A timeout for the download. If the timeout expires, the download will be aborted.</param>
/// <param name="cancellationToken">Optional. A cancellation token.</param>
/// <returns>
/// If <paramref name="computeHash" /> is <see langword="true" />, a <see cref="byte" /> array with a SHA256 hash of the resulting file.
/// </returns>
public static Task<byte[]> DownloadAsync(string url, string destPath, bool computeHash = true, string? displayName = null,
bool foregroundPriority = false, DO_DOWNLOAD_RANGE[]? ranges = null, IProgress<DO_DOWNLOAD_STATUS>? 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<byte[]> 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<IDODownload> 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<DO_DOWNLOAD_STATUS>? progress;
private DO_DOWNLOAD_STATUS currentStatus = default;
public DownloadStatusCallback(IProgress<DO_DOWNLOAD_STATUS>? 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)
/// <summary>Enumerates the existing downloads.</summary>
/// <param name="category">The property name to be used as a category to enumerate. Passing <see langword="null"/> will retrieve all existing downloads</param>
/// <returns>An enumeration of the existing downloads.</returns>
public static IEnumerable<IDODownload> EnumDownloads(DODownloadProperty? category = null) => new IDOManager().EnumDownloads(category);
[ComVisible(true), Guid("90AFD61C-C21C-4627-8A9A-E3268BC89051")]
internal class DownloadStatusCallback(Action<DO_DOWNLOAD_STATUS>? 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;
}
}
}
}