CsWinRT icon indicating copy to clipboard operation
CsWinRT copied to clipboard

Investigate AddPackageAsync hangs

Open manodasanW opened this issue 1 year ago • 22 comments

Determine whether issue still repros with recent version and investigate:

https://github.com/dotnet/wpf/issues/4097

manodasanW avatar Aug 23 '24 20:08 manodasanW

This is easily reproducible. Just create an empty WPF app with .net 8.0 and call

await packageManager.AddPackageAsync(...)

it is not reproduced every time but usually on every 5 calls, hangs at least 1 time. It is only reproducible in Release

Calling the method synchrously packageManager.AddPackageAsync(...).GetWaiter().GetResult() is a workaround for this issue but we noticed the installation of the same package is noticeable slower using the synchronous call.

This is not reproduced in WinUI3 app (or it is possible to be much harder to be reproduced)

ekalchev avatar Aug 24 '24 09:08 ekalchev

We found that calling Task.Run(() => packageManager.AddPackageAsync(...).GetWaiter().GetResult()) from net48 application also causes AddPackageAsync task never to resolve.

The deadlocks are reproduced ONLY with Release builds

ekalchev avatar Sep 17 '24 13:09 ekalchev

net48 has a different WinRT runtime, not CsWinRT.

dongle-the-gadget avatar Sep 17 '24 14:09 dongle-the-gadget

net48 has a different WinRT runtime, not CsWinRT.

We are able to reproduce the deadlock with both .net8.0 and net48. I know that net core uses CsWinRT package and net48 uses Microsoft.Windows.SDK.Contracts but I am not sure where to submit a bug for Microsoft.Windows.SDK.Contracts

ekalchev avatar Sep 17 '24 14:09 ekalchev

We found that if we turn off optimization for (net48) in Release it works.

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
      <Optimize>False</Optimize>
</PropertyGroup>

ekalchev avatar Sep 17 '24 14:09 ekalchev

@ekalchev Are you using CsWinRT projections that you generate in net48 or are you using the built-in support?

manodasanW avatar Sep 18 '24 07:09 manodasanW

We are using this nuget version https://www.nuget.org/packages/Microsoft.Windows.SDK.Contracts. We tried older version and it is still reproducible.

As much as I know CsWinRT nuget package can be used only with .net core 5 or later. So for net48 we use the nuget package above. Keep in mind the issue is present in both .net48 apps and .net core apps

We also reproduce with with .net8 app and using <TargetFramework>net8-windows10.0.17763</TargetFramework>

If you can't reproduce it, we can provide a code that reproduce it.

ekalchev avatar Sep 19 '24 18:09 ekalchev

Would be great to have some progress here - same issue with my project!

krasidachev avatar Oct 08 '24 08:10 krasidachev

We have identified additional details regarding this issue. By passing a CancellationToken to the affected methods, the freezing problem is resolved. Here is an example of how to implement this:

using(CancellationTokenSource cts = new CancellationTokenSource ())
{
await packageManager.StagePackageAsync(uri, null).AsTask(cts .Token)
}

Using this approach, we conducted thousands of package installations with AddPackageAsync and StagePackageAsync, and none of these operations froze, unlike when they were executed without a CancellationToken. We hope this information helps in addressing the issue or provides a viable workaround.

ekalchev avatar Oct 25 '24 09:10 ekalchev

We have identified additional details regarding this issue. By passing a CancellationToken to the affected methods, the freezing problem is resolved. Here is an example of how to implement this:

using(CancellationTokenSource cts = new CancellationTokenSource ()) { await packageManager.StagePackageAsync(uri, null).AsTask(cts .Token) }

Using this approach, we conducted thousands of package installations with AddPackageAsync and StagePackageAsync, and none of these operations froze, unlike when they were executed without a CancellationToken. We hope this information helps in addressing the issue or provides a viable workaround.

Does the workaround still work if CancellationToken.None is passed around?

Aetopia avatar Feb 17 '25 19:02 Aetopia

Yes

ekalchev avatar Feb 18 '25 06:02 ekalchev

I have also stumbled upon the same issue and it is very annoying. I would love to see this being worked on.

Nevca avatar Mar 25 '25 15:03 Nevca

We have identified additional details regarding this issue. By passing a CancellationToken to the affected methods, the freezing problem is resolved. Here is an example of how to implement this: using(CancellationTokenSource cts = new CancellationTokenSource ()) { await packageManager.StagePackageAsync(uri, null).AsTask(cts .Token) } Using this approach, we conducted thousands of package installations with AddPackageAsync and StagePackageAsync, and none of these operations froze, unlike when they were executed without a CancellationToken. We hope this information helps in addressing the issue or provides a viable workaround.

Does the workaround still work if CancellationToken.None is passed around?

Actually, we found another PackageManager method AddPackageByAppInstallerFileAsync. We tried with to call AsTask with new CancelationToken and CancellationToken.None but it the problem was still present.

The workaround with the cancellationToken works only if it is constructed by CancellationTokenSource like that.

using(CancellationTokenSource cts = new CancellationTokenSource ())
{
await packageManager.AddPackageByAppInstallerFileAsync(uri, null).AsTask(cts .Token)
}

ekalchev avatar Mar 25 '25 16:03 ekalchev

I'm encountering the same problem. Could you please resolve it?

yordans avatar Mar 25 '25 16:03 yordans

It is really frustrating that such an issue hasn't been addressed yet, especially since it only occurs in Release and not consistently. This behavior is extremely inconvenient and disruptive to the workflow. Why is this problem still unresolved? It's clearly an important issue, and it’s time it was properly addressed.

I'm encountering the same issue as well, so I kindly ask for it to be resolved.

nssimeonov avatar Mar 28 '25 12:03 nssimeonov

This is still an issue, and for me even in a WinUI 3 app.

whiskhub avatar May 10 '25 15:05 whiskhub

It’s concerning that this issue has persisted for years without an official response from Microsoft. The API is currently not functioning as expected, and we would appreciate timely communication and a resolution from the Microsoft team.

ekalchev avatar May 12 '25 07:05 ekalchev

It seems the issue is with WindowsRuntimeSystemExtensions.AsTask() or specifically with how it wraps asynchronous Windows Runtime operations & awaits them.

That makes me ask has anyone tried to wrap asynchronous Windows Runtime operations themselves using TaskCompletionSource<T>/TaskCompletionSource & see if the issue still occurs? Because if it does then something is clearly wrong with WindowsRuntimeSystemExtensions.AsTask().

Here is an implementation for wrapping IAsyncOperationWithProgress<TResult, TProgress> as Task<TResult>which supports:

  • Attaching a handler for progress via callbacks.

  • Ability to cancel the asynchronous operation by passing a cancellation token.

  • Handler error propagation to underlying wrapping task.

public static async Task<TResult> Task<TResult, TProgress>(
    this IAsyncOperationWithProgress<TResult, TProgress> info,
    AsyncOperationProgressHandler<TResult, TProgress> handler = default,
    CancellationToken token = default
)
{
    // Check if the caller has passed a handler.

    bool flag = handler is not null;

    // Only asynchronously await if the operation hasn't completed.

    if (info.Status is AsyncStatus.Started)
    {
        TaskCompletionSource<object> source = new();

        // Propagate the errors from the caller's handler by wrapping it. 

        void Handler(IAsyncOperationWithProgress<TResult, TProgress> info, TProgress status)
        {
            try { handler(info, status); }
            catch (Exception exception) { source.TrySetException(exception); }
        }

        // Attach the handler only if the caller passed one.

        if (flag) info.Progress += Handler;

        info.Completed += (info, status) =>
        {
            // Detach the handler only if the caller passed one.

            if (flag) info.Progress -= Handler;
            source.TrySetResult(null);
        };

        using var registration = token.Register(info.Cancel);
        await source.Task.ConfigureAwait(false);
    }

    // Check if the operation was canceled, errored or completed.
        
    return info.Status switch
    {
        AsyncStatus.Error => throw info.ErrorCode,
        AsyncStatus.Canceled => throw new TaskCanceledException(),
        _ => info.GetResults(),
    };
}

Consuming the extension method that wraps the operation:

var manager = new PackageManager();
using CancellationTokenSource source = new();

await manager.AddPackageAsync(new("https://www.example.com"), null, DeploymentOptions.None).Task((info, progress) =>
{
    Console.WriteLine($"{progress.state}, {progress.percentage}%");
}, source.Token);

Exception Stack Trace:

Unhandled Exception: System.Exception: Package could not be opened.

error 0x8007000D: Opening the package from location  failed.
   at Extensions.<Task>d__0`2.MoveNext() in ...
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<Main>d__0.MoveNext() in ...
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<Main>()

Aetopia avatar May 13 '25 11:05 Aetopia

@Aetopia you are trying to install package from a invalid URL. "https://www.example.com"

ekalchev avatar May 13 '25 11:05 ekalchev

@Aetopia you are trying to install package from a invalid URL. "https://www.example.com"

I just wanted to demo the extension method wrapper in action in this case by catching the error thrown by the operation due to a "non-existent" file.

Aetopia avatar May 13 '25 11:05 Aetopia

The issue occurs only in Release builds and if there is no instance of CancellationToken passed and default is used ( or CancenllationToken.None), that suggest that the problem is somehow related to the generated IL by the compiler on probably we can't see it in C# code implementation.

ekalchev avatar May 13 '25 11:05 ekalchev

The issue occurs only in Release builds and if there is no instance of CancellationToken passed and default is used ( or CancenllationToken.None), that suggest that the problem is somehow related to the generated IL by the compiler on probably we can't see it in C# code implementation.

The runtime + compiler implicitly calls WindowsRuntimeSystemExtensions.AsTask() to wrap an async operation. hence you may try inspecting System.Runtime.WindowsRuntime.dll via ILSpy or your preferred decompiler.

I get the gist of the issue though, my question is simply that has anyone tried to make their own async WinRT operation wrapper using TaskCompletionSource instead on relying on the one provided by the runtime, just to rule out its not a runtime specific issue.

Aetopia avatar May 13 '25 11:05 Aetopia