2

Closed

Compiler.BuildMsi() & async/await

description

Hello!

I got an exception from Compiler.BuildMsi() method. I tried to find the reason and it took me a long time. Below I will explain my situation and my solution. I think it will help you to improve Wix#.

Compiler.BuildMsi() builds an installer in separated AppDomain. When AppDomain is created .NET passes CallContext of the parent domain to the created child domain. It means content of this CallContext will be serialized and then deserialized to be transferred it between domains. If CallContext contains non-serializable instance any invocation to the child AppDomain will return an exception. And that exception will not reflect the real problem.

Also we can't control content of the CallContext because we can use a third party code. So it would be great if we had "clean" CallContext before creating new AppDomain. To do this we can use ExecutionContext.SuppressFlow() method which suppresses the flow of the execution context i.e. after call SuppressFlow() new threads won't get parent CallContext and will have to use its own "clean" CallContext. Consequently you can create new AppDomain within new thread and don't worry about CallContext.

After work with child AppDomain you must restore the flow of the execution context using AsyncFlowControl.Undo() method. The Undo() method must be called in the same thread where SuppressFlow() method was invoked thus next code waits for completion a thread using Task.Wait() and doesn't use await.

Unfortunately only this workaround helped me and I can use Compiler in any CallContext (include async/await code). Probably there is more elegant solution but I think it must be part of your library. :)
// ...

await ExecuteInNewContext(() => Compiler.BuildMsi(...));

// ...

private static Task<T> ExecuteInNewContext<T>(Func<T> action)
{
    var taskResult = new TaskCompletionSource<T>();

    var asyncFlow = ExecutionContext.SuppressFlow();

    try
    {
        Task.Run(() =>
                 {
                     try
                     {
                         var result = action();

                         taskResult.SetResult(result);
                     }
                     catch (Exception exception)
                     {
                         taskResult.SetException(exception);
                     }
                 })
            .Wait();
    }
    finally
    {
        asyncFlow.Undo();
    }

    return taskResult.Task;
}
Closed Dec 3, 2016 at 8:24 AM by oleg_s
Moved to GitHub

comments

oleg_s wrote Nov 28, 2016 at 3:00 AM

Hi guys,

It's an interesting use-case for the MSI generation you have here. Honestly I have never expected Wix# to be used in inter-AppDomain scenarios. In fact I only expected it to be hosted by either some sort of CI, VisualStudio and CS-Script.

Though despite your problem being very specific I do see the benefits from including your workaround into the codebase. Particularly because there is an excellent spot for it: WixSharp.CommonTasks..

However if I do that it will require upgrading the WixSharp.dll target platform to v4.5. And this in turn most likely will create problems building managed CA and ManagedUI for target platforms lower than v4.5. I need to do some more testing as there is a good chance that the default app.config for MakeSfxCA.exe will handle runtime downgrading just fine.

Anyway, as a first step I would prefer to include your work around as a dedicated sample in the next release (may be this week). Would you be able to assist me by providing a simple Hello World style working code sample. Something like:
class Program
{
    static void Main(string[] args)
    {
        var project = 
            new Project("MyProduct",
                new Dir("ProgramFiles",
                ...

        AsyncBuild(project);
        Console.ReadLine();
    }

    static async void AsyncBuild(Project project)
    {
        await ExecuteInNewContext(project.Compile);
    } 

    async static Task<T> ExecuteInNewContext<T>(Func<T> action)
    {
        var taskResult = new TaskCompletionSource<T>();
        var asyncFlow = ExecutionContext.SuppressFlow();

       ...

        return taskResult.Task;
    }
}

oleg_s wrote Dec 2, 2016 at 10:48 AM

When you are ready to reply on this do it via GitHub issues page: https://github.com/oleg-shilo/wixsharp.

AlexMAS wrote Dec 2, 2016 at 7:01 PM

Thanks for your fast reply. Yes, you are right, we use Wix# non standard way. In our project Wix# creates MSI in runtime on server side. I know it is not typical situation but it is requirements our customers. :) Sure I'll try to help you in this issue. :) I've created new issue on GitHub - https://github.com/oleg-shilo/wixsharp/issues/1

oleg_s wrote Dec 3, 2016 at 8:23 AM

Great. Thank you. I will close this issue here and we will continue working with it on GitHub