4

I'm attempting to create an open source library that spawn a new AppDomain and runs a PowerShell script in it. I have a static method that takes the name of the powershell file and the name of the AppDomain. The method executes successfully when called from a C# console app, but not PowerShell.

I know the dll is being loaded in the second app domain because of this entry in the fusionlog.

The class declaraton and constructor looks like this.

public class AppDomainPoshRunner : MarshalByRefObject{

    public AppDomainPoshRunner (){
        Console.WriteLine("Made it here.");
    }
}

That message in the constructor gets output when I call CreateInstanceFromAndUnwrap whether I run the dll from a C# console app or from the PowerShell app.

The failure occurs when I cast the value returned by CreateInstanceFromAndUnwrap to AppDomainPoshRunner in the static method below.

    public static string[] RunScriptInAppDomain(string fileName, string appDomainName = "Unamed")
    {
        var assembly = Assembly.GetExecutingAssembly();

        var setupInfo = new AppDomainSetup
                            {
                                ApplicationName = appDomainName,
                                // TODO: Perhaps we should setup an even handler to reload the AppDomain similar to ASP.NET in IIS.
                                ShadowCopyFiles = "true"
                            };
        var appDomain = AppDomain.CreateDomain(string.Format("AppDomainPoshRunner-{0}", appDomainName), null, setupInfo);
        try {
            var runner = appDomain.CreateInstanceFromAndUnwrap(assembly.Location, typeof(AppDomainPoshRunner).FullName);
            if (RemotingServices.IsTransparentProxy(runner))
                Console.WriteLine("The unwrapped object is a proxy.");
            else
                Console.WriteLine("The unwrapped object is not a proxy!");  
            Console.WriteLine("The unwrapped project is a {0}", runner.GetType().FullName);
            /* This is where the error happens */
            return ((AppDomainPoshRunner)runner).RunScript(fileName);
        }
        finally
        {
            AppDomain.Unload(appDomain);
        }
    }

When running that in PowerShell I get an InvalidCastExcception with the message Unable to cast transparent proxy to type JustAProgrammer.ADPR.AppDomainPoshRunner.

What am I doing wrong?

Justin Dearing
  • 14,270
  • 22
  • 88
  • 161
  • The Type of `runner` needs to inherit from `AppDomainPoshRunner`. – Scott Sep 24 '11 at 02:39
  • So, you're saying that when you call `RunScriptInAppDomain()` from a C# console app, it runs fine, but when you call it from PowerShell, it throws that exception? – svick Sep 24 '11 at 02:43
  • @Scott, the type of `runner` is `AppDomainPoshRunner`. – svick Sep 24 '11 at 02:44
  • @svick as scott said, runner is of type AppDomainPoshRunner. – Justin Dearing Sep 24 '11 at 02:51
  • @Scott, correct, RunScriptInAppDomain() works fine from the C# console app, but not from the powershell app. Source code for both is in the github. – Justin Dearing Sep 24 '11 at 02:51
  • FYI tried running the script as a visual studip debug target in the csproj, in powershell.exe and in PowerGUI – Justin Dearing Sep 24 '11 at 02:51
  • Yes, it's just called `AppDomainPoshRunner`. It can't be considered to be the same thing as the `AppDomainPoshRunner` found in this assembly. The class that you're instantiating with `CreateInstanceFromAndUnwrap` needs to derive from this assembly's `AppDomainPoshRunner` and it should work. – Scott Sep 24 '11 at 02:52
  • @Scott so is my problem that the static method creating the secondary appdomain is in the same class that I am instantiating in the second app domain? – Justin Dearing Sep 24 '11 at 03:20
  • No, I'll write up an answer soon. – Scott Sep 24 '11 at 03:23
  • Actually I think could very well be the cause. Have you tried it? – Scott Sep 24 '11 at 03:34
  • I'll try now, but why does it work from C# then? – Justin Dearing Sep 24 '11 at 03:37
  • Here is a branch of my code where I moved that statis member into a separate assembly. https://github.com/zippy1981/AppDomainPoshRunner/tree/Multi-Assembly Same error – Justin Dearing Sep 24 '11 at 03:56
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/3741/discussion-between-justin-dearing-and-scott) – Justin Dearing Sep 24 '11 at 03:56

2 Answers2

3

I had the same problem: I created sandbox with Execute only permissions (the min one can have) to execute untrusted code in very restricted environment. All worked great in C# application, but did not work (the same cast exception) when starting point was vbs script creating .NET COM object. I think PowerShell also uses COM. I found workaround using AppDomain.DoCallBack, which avoids getting proxy from the appdomain. This is the code. If you find a better option, please post. Registering in GAC is not a good solution for me...

    class Test
    {
        /*
         create appdomain as usually
        */

        public static object Execute(AppDomain appDomain, Type type, string method, params object[] parameters)
        {
            var call = new CallObject(type, method, parameters);
            appDomain.DoCallBack(call.Execute);
            return call.GetResult();
        }
    }
    [Serializable]
    public class CallObject
    {
        internal CallObject(Type type, string method, object[] parameters)
        {
            this.type = type;
            this.method = method;
            this.parameters = parameters;
        }

        [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
        public void Execute()
        {
            object instance = Activator.CreateInstance(this.type);
            MethodInfo target = this.type.GetMethod(this.method);
            this.result.Data = target.Invoke(instance, this.parameters);
        }

        internal object GetResult()
        {
            return result.Data;
        }

        private readonly string method;
        private readonly object[] parameters;
        private readonly Type type;
        private readonly CallResult result = new CallResult();

        private class CallResult : MarshalByRefObject
        {
            internal object Data { get; set; }
        }
    }
Dima
  • 699
  • 1
  • 6
  • 18
1

It sounds very much like a loading context issue. Type identity isn't just about the physical assembly file; it's also about how and where it was loaded. Here's an old blog post from Suzanne Cook that you'll probably have to read fifteen times until you can begin to make sense of your problem.

Choosing a Binding Context

http://blogs.msdn.com/b/suzcook/archive/2003/05/29/57143.aspx

Before you say "but it works in a console app," remember that when running it from powershell, you have an entirely different kettle of fish as regards the calling appdomain's context, probing paths, identity etc.

Good luck!

x0n
  • 51,312
  • 7
  • 89
  • 111
  • Thanks for that article. Even if it doesn't directly solve this problem, its certainly useful if you muck about with AppDomains. – Justin Dearing Sep 26 '11 at 11:24
  • Putting in the GAC is a quick fix as GAC'd assemblies always end up in the Load context. Once you unwrap a remote instance, the AppDomainPoshRunner is loaded into the calling appdomain in a different context than the one in the callee. This gives them different identities. If you wanted to avoid putting the entire implementation in the GAC, I'd create an IScriptRunner interface in a shared assembly and GAC that one instead. Leave the concrete impl in the local non-gac'd assembly, but it will need to be strong named too or it won't be able to load the GAC'd shared assembly. – x0n Sep 26 '11 at 21:48