MiloszKrajewski / LibZ

LibZ, the alternative to ILMerge
Other
220 stars 33 forks source link

Runtime exception merging .NET 4.6 assemblies #9

Open mythz opened 8 years ago

mythz commented 8 years ago

Hi Milosz,

This is the issue from our twitter conversation where I'm having issues trying to merge my .NET 4.6 project which uses Roslyn. Scenario 1 fails immediately when run with being unable to find the PCL ServiceStack.Interfaces.dll, Scenario 4 is the closest to working where the App will run, but will fail after hitting "Play" button to execute a script which calls a back-end Web Service in Gistlyn.ServiceInterface.dll with:

(FileNotFoundException) Could not load file or assembly 'Gistlyn.ServiceInterface, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

But it does work if you run the merged Gistlyn-merged.exe within the same directory with the other .dlls, but it fails when you try to execute it stand-alone outside of the directory.

I've created a repro of this issue at Gistlyn-libz.zip.

It has all the .dlls for the app and the linbz-merge.bat script I'm running:

COPY Gistlyn.exe Gistlyn-merged.exe 

libz.exe add --libz all.libz --include *.dll --safe-load
libz.exe inject-libz --assembly Gistlyn-merged.exe --libz all.libz
libz.exe instrument --assembly Gistlyn-merged.exe --libz-resources

It it helps, the source code for the project is in ServiceStack/Gistlyn/src. Please let me know if there's any other info you need.

MiloszKrajewski commented 8 years ago

I will be answering in small chunks as there are multiple questions here, and because I don't know all the answer.

So, "Scenario 1" is not working with PCL. and it won't be. It is "not-a-bug" or "won't-fix" (I don't care which one). What I can do is to be more precise in documentation. For PCL use scenarios 2-7 with --safe-load.

Why? PCLs need to be loaded from disk not from memory, the only way to force loading from disk is to use --safe-load which is not available in on inject-dll. A little bit more about that can be found here

MiloszKrajewski commented 8 years ago

Look complex so it would need a lot of you cooperation here. First, it is very important to understand how it works. It "hijacks" an event "AppDomain.ResolveAssembly" which is triggered when application cannot find an assembly. That's right - "when it cannot find an assembly". So if assembly is in application folder it does not trigger this event. I assume, and I have to emphasize that I don't know that, I just assume, that when you press "Play" it creates kind-of application domain for Roslyn and executes script inside this domain. It does have to be called "domain" it might be a "scope" or something like this. Now, when this script is executed I bet it does not use main assembly resolver, so when you reference something from inside the "scope" it does not even trigger "ResolveAssembly" on main domain.

I guess, the "scope" itself may have an event like "ResolveAssembly" but I don't know that.

mythz commented 8 years ago

I'm assuming you mean the AppDomain.AssemblyResolve event. Yeah it looks like the issue is likely due to creating our own AppDomains which are created without a custom AssemblyResolve event, is there something we can reassign this to, to hook it up with the libz custom AssemblyResolve handler? e.g. is it available in a static class using Reflection?

MiloszKrajewski commented 8 years ago

It can be done, but I never dealt with this myself. This guy did though.

mythz commented 8 years ago

ok thanks but I'd strongly prefer not to have a hard dependency on Libz inside my App if possible. I've tried modifying my app to use reflection to assign all the event AssemblyResolve event handlers registered on the CurrentDomain to the newly created AppDomain with:

var domain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), evidence, setup);
var fi = typeof(AppDomain).GetField("_AssemblyResolve", BindingFlags.Instance | BindingFlags.NonPublic);
if (fi != null)
{
    var eventHandler = fi.GetValue(AppDomain.CurrentDomain) as ResolveEventHandler;
    if (eventHandler != null)
    {
        foreach (ResolveEventHandler fn in eventHandler.GetInvocationList())
        {
            domain.AssemblyResolve += fn;
        }
    }
}

Which does have some effect as it changes the error from Gistlyn.ServiceInterface to:

(FileNotFoundException) Could not load file or assembly 'Gistlyn.AppConsole, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

I've also tried registering all ReflectionOnlyAssemblyResolve events in-case you had any registered there also and I've also tried registering a fallback AssemblyResolve event returning the Assembly of the AppHost (i.e. where Gistlyn.AppConsole lives) but that didn't help either.

It would be ideal if you could provide an official API we can hook into (i.e. easily find with reflection) so that Libz can wireup to handle the AssemblyResolve events of any new AppDomains we create. Otherwise I suppose this use-case just isn't supported, but Libz does go further than all other merge utilities out there which chokes on either 4.6 classes or the PCL library.

MiloszKrajewski commented 8 years ago

I guess you are right. I should think about better support for applications with multiple app domains. I don't think you can get it wthough dependency on LibZ, though. I mean this NewDomain.AssemblyResolve needs to be configured by you in code (scanning code every use of AppDomain doesn't look right to me).

mythz commented 8 years ago

It would be great if I can call into a LibZ API to configure a new AppDomain, I could use reflection to call it to avoid the hard dependency on LibZ but this shouldn't have an impact on the API itself.

A static API like this would be nice:

LibZ.Bootstrap.LibZResolver.ConfigureAppDomain(domain);
ghost commented 6 years ago

Hello guys,

Sorry for old thread!

I really don't understand what does ivan5o write from "this guy"

Can I have example? I have to try hard but I don't have to get success. I want check if embedded Test.dll won't execute from Helper.

How do I see it.

using System;
using System.Security.AccessControl;
using System.Security.Policy;
using LibZ.Bootstrap;

namespace TestConsole
{
    class Helper : MarshalByRefObject
    {
        internal static void InitalizeLibZFileContainer(Action startup = null)
        {
            LibZResolver.RegisterFileContainer("Test.dll");
            if (null != startup)
                LibZResolver.Startup(startup);
        }

        public Helper()
        {
            InitalizeLibZFileContainer();
        }

        static void Main(string[] args)
        {
            InitalizeLibZFileContainer(() =>
            {
                Console.WriteLine("Test");
                Console.ReadLine();
                Type test = Type.GetType("Test");
                string testName = test.Assembly.FullName;
                Console.WriteLine(testName);
                Console.ReadLine();
            });
            Type type = typeof(Helper);
            string assemblyName = type.Assembly.FullName;
            Evidence evidence = type.Assembly.Evidence;
            AppDomainSetup info = new AppDomainSetup();
            AppDomain appDomain = AppDomain.CreateDomain("Helper.AppDomain", evidence, info);
            appDomain.CreateInstance(assemblyName, type.FullName);
        }
    }
}

It is Helper.cs into exe ( console )

using System;

namespace Test
{
    public class Test
    {
        public Test()
        {
            Console.WriteLine("Test from library Test.dll");
        }
    }
}

Test.cs into library dll.

How do I fix it?