与应用程序域替换的Process.Start应用程序、Process、Start

2023-09-07 03:10:09 作者:还有我疼你へ

背景

我有一个使用各种第三方的DLL对PDF文件执行工作的一个Windows服务。这些操作可以用相当多的系统资源,并且偶尔会从内存泄漏发生错误时受到影响。这些DLL托管包装上其他非托管的DLL。

当前的解决方案

我已经被包裹在一个专门的控制台应用程序调用的DLL之一,并呼吁通过的Process.Start该应用程序()缓解这个问题在一种情况下。如果操作失败,有内存泄漏,或尚未发布的文件句柄,它其实并不重要。这个过程将结束,操作系统将恢复把手。

我想这同样的逻辑也适用于其他地方在我的应用程序使用这些DLL。不过,我并不十分兴奋,增加更多的控制台项目,我的解决方案,并编写更锅炉板code调用的Process.Start()和分析控制台应用程序的输出。

新解

类加载和应用程序域在Flash中的概念

这是优雅的替代专用控制台应用程序和的Process.Start()似乎是使用的AppDomain中,像这样的:http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.

我已经实施了类似的code。在我的应用程序,但单元测试并没有被看好。我创建一个FileStream在一个单独的AppDomain测试文件,但不处理它。然后,我尝试创建主域中的另一个的FileStream,它失败,因为未发布的文件锁定。

有趣的是,添加一个空的DomainUnload事件给工人域使得单元测试通过。无论如何,我很担心,也许创造工人的AppDomain不会解决我的问题。

思考?

的code

  ///<总结>
///执行在一个单独的应用程序域的方法。这应该作为一个简单的替换
通过一个控制台应用程序运行code在一个单独的进程///。
///< /总结>
公共牛逼RunInAppDomain< T>(Func键< T> FUNC)
{
    AppDomain中域= AppDomain.CreateDomain(委托执行人+ func.GetHash code(),空,
    新AppDomainSetup {ApplicationBase = Environment.CurrentDirectory});

    domain.DomainUnload + =(发件人,E)=>
    {
    //这个空的事件处理程序修复了单元测试,但我不知道为什么
    };

    尝试
    {
    domain.DoCallBack(新AppDomainDelegateWrapper(域,FUNC).Invoke);

    返回(T)domain.GetData(结果);
    }
    最后
    {
    AppDomain.Unload(域);
    }
}

公共无效RunInAppDomain(动作FUNC)
{
    RunInAppDomain(()=> {FUNC();返回0;});
}

///<总结>
///提供围绕代表一个序列化的包装。
///< /总结>
[可序列化]
私有类AppDomainDelegateWrapper:MarshalByRefObject的
{
    私人只读的AppDomain _domain;
    私人只读代表_delegate;

    公共AppDomainDelegateWrapper(AppDomain中域,代表FUNC)
    {
    _domain =域;
    _delegate = FUNC;
    }

    公共无效的invoke()
    {
    _domain.SetData(结果,_delegate.DynamicInvoke());
    }
}
 

单元测试

  [测试]
公共无效RunInAppDomainCleanupCheck()
{
    常量字符串路径= @../../输出/ AppDomain中挂-file.txt的;

    使用(var文件= File.CreateText(路径))
    {
    file.WriteLine(测试);
    }

    //验证没有在一个AppDomain包裹调用关闭该文件句柄调用返回后,将清除
    Portal.ProcessService.RunInAppDomain(()=>
    {
    //打开一个测试文件,但不要松开。在AppDomain卸载时,手柄应该被释放
    新的FileStream(路径,FileMode.Open,FileAccess.ReadWrite,FileShare.None);
    });

    //睡一会不会有所​​作为
    //Thread.Sleep(10000);

    //创建一个新的FileStream会失败,如果DomainUnload事件不绑定
    使用(var文件=新的FileStream(路径,FileMode.Open,FileAccess.ReadWrite,FileShare.None))
    {
    }
}
 

解决方案

应用程序域和跨域交互是一个非常薄的事,所以应该确保他真的明白怎么回事的工作做什么...嗯...前。比方说,非标: - )

首先,你流的创造方法实际执行上的默认域(惊喜,惊喜!)。为什么?很简单:您传递到AppDomain.DoCallBack该方法的AppDomainDelegateWrapper对象定义,该对象存在于默认域,因此这正是它的方法被执行。 MSDN不说这个有点功能,但它很容易检查:只设置一个断点在AppDomainDelegateWrapper.Invoke

所以,基本上,你必须做出离不开一个包装的对象。为DoCallBack的说法用静态方法。

但你如何通过你的FUNC的说法到其他域,使您的静态方法可以把它捡起来,并执行?

最明显的方法是使用AppDomain.SetData,也可以推出自己的,但不管你到底如何做到这一点,有一个问题:如果FUNC是一个非静态方法,那么对象它是在必须以某种方式传递到其他应用程序域定义。它既可按值传递(而其被复制,逐个字段)或引用(创建具有远程的所有美景跨域的对象引用)。要做到前者,类必须标有[Serializable]属性。要做到后者,它必须从MarshalByRefObject继承的。如果该类既不是一个异常会在尝试的对象传递给其他域抛出。请记住,虽然,通过引用传递pretty的多杀的整体思路,因为你的方法仍然算得上是同一个域的对象存在的 - 也就是默认的

在结束上一段,如果只剩下两个选择:要么通过对标有[Serializable]属性类中定义的方法(和记住的对象将被复制),或通过静态方法。我怀疑,你的目的,你需要前者。

和公正的情况下,它逃脱了你的注意力,我想指出的是,RunInAppDomain(一个是采取行动),你的第二个重载传递一个类中定义一个方法,没有标记[Serializable接口]。看不到任何一类呢?你不必为:用匿名委托含有绑定变量,编译器会为您创建一个。它只是恰巧,编译器不会理会标记自动生成的类[Serializable接口]。不幸的,但这就是生活: - )

说了这么多(很多的话,是不是:-),并假设你的誓言不通过任何非静态和非[Serializable接口]的方法,这里有你的新RunInAppDomain方法:

  ///<总结>
    ///执行在一个单独的应用程序域的方法。这应该作为一个简单的替换
    通过一个控制台应用程序运行code在一个单独的进程///。
    ///< /总结>
    公共静态牛逼RunInAppDomain< T>(Func键< T> FUNC)
    {
        AppDomain中域= AppDomain.CreateDomain(委托执行人+ func.GetHash code(),空,
            新AppDomainSetup {ApplicationBase = Environment.CurrentDirectory});

        尝试
        {
            domain.SetData(toInvoke,FUNC);
            domain.DoCallBack(()=>
            {
                变种F = AppDomain.CurrentDomain.GetData(toInvoke)作为函数功能< T&GT ;;
                AppDomain.CurrentDomain.SetData(结果中,f());
            });

            返回(T)domain.GetData(结果);
        }
        最后
        {
            AppDomain.Unload(域);
        }
    }

    [可序列化]
    私有类ActionDelegateWrapper
    {
        公共行动Func键;
        公众诠释的invoke()
        {
            FUNC();
            返回0;
        }
    }

    公共静态无效RunInAppDomain(动作FUNC)
    {
        RunInAppDomain< INT>(新ActionDelegateWrapper {func =函数} .Invoke);
    }
 

如果你还在我身边,我AP preciate: - )

现在,在固定的机制,花费如此多的时间后,我要告诉你,就是为目的的反正。

的事情是,应用程序域不会帮您为您的目的。他们只照顾管理的对象,而不受管理的code能泄漏,崩溃一切就是了。非托管的code甚至不知道有这样的事情的AppDomain。它只知道有关进程。

所以,在最后,你最好的选择仍然是当前解决方案:只产卵另一个进程,并感到高兴。而且,我同意了previous的答案,你不必写每个案例的另一个控制台应用程序。只需通过一个静态方法的完全限定名称,并有控制台应用程序加载程序集,加载你的类型,并调用方法。实际上,你可以将它打包整齐pretty的一个非常大致相同的方式,你试着用AppDomain中。您可以创建一个名为像RunInAnotherProcess的方法,将审查的说法,得到完整的类型名称和方法名出来的(同时确保该方法是静态的),并产卵控制台应用程序,这将做休息。

保重,祝你好运。

费奥多尔

Background

I have a Windows service that uses various third-party DLLs to perform work on PDF files. These operations can use quite a bit of system resources, and occasionally seem to suffer from memory leaks when errors occur. The DLLs are managed wrappers around other unmanaged DLLs.

Current Solution

I'm already mitigating this issue in one case by wrapping a call to one of the DLLs in a dedicated console app and calling that app via Process.Start(). If the operation fails and there are memory leaks or unreleased file handles, it doesn't really matter. The process will end and the OS will recover the handles.

I'd like to apply this same logic to the other places in my app that use these DLLs. However, I'm not terribly excited about adding more console projects to my solution, and writing even more boiler-plate code that calls Process.Start() and parses the output of the console apps.

New Solution

An elegant alternative to dedicated console apps and Process.Start() seems to be the use of AppDomains, like this: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.

I've implemented similar code in my application, but the unit tests have not been promising. I create a FileStream to a test file in a separate AppDomain, but don't dispose it. I then attempt to create another FileStream in the main domain, and it fails due to the unreleased file lock.

Interestingly, adding an empty DomainUnload event to the worker domain makes the unit test pass. Regardless, I'm concerned that maybe creating "worker" AppDomains won't solve my problem.

Thoughts?

The Code

/// <summary>
/// Executes a method in a separate AppDomain.  This should serve as a simple replacement
/// of running code in a separate process via a console app.
/// </summary>
public T RunInAppDomain<T>( Func<T> func )
{
    AppDomain domain = AppDomain.CreateDomain ( "Delegate Executor " + func.GetHashCode (), null,
    	new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory } );

    domain.DomainUnload += ( sender, e ) =>
    {
    	// this empty event handler fixes the unit test, but I don't know why
    };

    try
    {
    	domain.DoCallBack ( new AppDomainDelegateWrapper ( domain, func ).Invoke );

    	return (T)domain.GetData ( "result" );
    }
    finally
    {
    	AppDomain.Unload ( domain );
    }
}

public void RunInAppDomain( Action func )
{
    RunInAppDomain ( () => { func (); return 0; } );
}

/// <summary>
/// Provides a serializable wrapper around a delegate.
/// </summary>
[Serializable]
private class AppDomainDelegateWrapper : MarshalByRefObject
{
    private readonly AppDomain _domain;
    private readonly Delegate _delegate;

    public AppDomainDelegateWrapper( AppDomain domain, Delegate func )
    {
    	_domain = domain;
    	_delegate = func;
    }

    public void Invoke()
    {
    	_domain.SetData ( "result", _delegate.DynamicInvoke () );
    }
}

The unit test

[Test]
public void RunInAppDomainCleanupCheck()
{
    const string path = @"../../Output/appdomain-hanging-file.txt";

    using( var file = File.CreateText ( path ) )
    {
    	file.WriteLine( "test" );
    }

    // verify that file handles that aren't closed in an AppDomain-wrapped call are cleaned up after the call returns
    Portal.ProcessService.RunInAppDomain ( () =>
    {
    	// open a test file, but don't release it.  The handle should be released when the AppDomain is unloaded
    	new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None );
    } );

    // sleeping for a while doesn't make a difference
    //Thread.Sleep ( 10000 );

    // creating a new FileStream will fail if the DomainUnload event is not bound
    using( var file = new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ) )
    {
    }
}

解决方案

Application domains and cross-domain interaction is a very thin matter, so one should make sure he really understands how thing work before doing anything... Mmm... Let's say, "non-standard" :-)

First of all, your stream-creating method actually executes on your "default" domain (surprise-surprise!). Why? Simple: the method that you pass into AppDomain.DoCallBack is defined on an AppDomainDelegateWrapper object, and that object exists on your default domain, so that is where its method gets executed. MSDN doesn't say about this little "feature", but it's easy enough to check: just set a breakpoint in AppDomainDelegateWrapper.Invoke.

So, basically, you have to make do without a "wrapper" object. Use static method for DoCallBack's argument.

But how do you pass your "func" argument into the other domain so that your static method can pick it up and execute?

The most evident way is to use AppDomain.SetData, or you can roll your own, but regardless of how exactly you do it, there is another problem: if "func" is a non-static method, then the object that it's defined on must be somehow passed into the other appdomain. It may be passed either by value (whereas it gets copied, field by field) or by reference (creating a cross-domain object reference with all the beauty of Remoting). To do former, the class has to be marked with a [Serializable] attribute. To do latter, it has to inherit from MarshalByRefObject. If the class is neither, an exception will be thrown upon attempt to pass the object to the other domain. Keep in mind, though, that passing by reference pretty much kills the whole idea, because your method will still be called on the same domain that the object exists on - that is, the default one.

Concluding the above paragraph, you are left with two options: either pass a method defined on a class marked with a [Serializable] attribute (and keep in mind that the object will be copied), or pass a static method. I suspect that, for your purposes, you will need the former.

And just in case it has escaped your attention, I would like to point out that your second overload of RunInAppDomain (the one that takes Action) passes a method defined on a class that isn't marked [Serializable]. Don't see any class there? You don't have to: with anonymous delegates containing bound variables, the compiler will create one for you. And it just so happens that the compiler doesn't bother to mark that autogenerated class [Serializable]. Unfortunate, but this is life :-)

Having said all that (a lot of words, isn't it? :-), and assuming your vow not to pass any non-static and non-[Serializable] methods, here are your new RunInAppDomain methods:

    /// <summary>
    /// Executes a method in a separate AppDomain.  This should serve as a simple replacement
    /// of running code in a separate process via a console app.
    /// </summary>
    public static T RunInAppDomain<T>(Func<T> func)
    {
        AppDomain domain = AppDomain.CreateDomain("Delegate Executor " + func.GetHashCode(), null,
            new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory });

        try
        {
            domain.SetData("toInvoke", func);
            domain.DoCallBack(() => 
            { 
                var f = AppDomain.CurrentDomain.GetData("toInvoke") as Func<T>;
                AppDomain.CurrentDomain.SetData("result", f());
            });

            return (T)domain.GetData("result");
        }
        finally
        {
            AppDomain.Unload(domain);
        }
    }

    [Serializable]
    private class ActionDelegateWrapper
    {
        public Action Func;
        public int Invoke()
        {
            Func();
            return 0;
        }
    }

    public static void RunInAppDomain(Action func)
    {
        RunInAppDomain<int>( new ActionDelegateWrapper { Func = func }.Invoke );
    }

If you're still with me, I appreciate :-)

Now, after spending so much time on fixing that mechanism, I am going to tell you that is was purposeless anyway.

The thing is, AppDomains won't help you for your purposes. They only take care of managed objects, while unmanaged code can leak and crash all it wants. Unmanaged code doesn't even know there are such things as appdomains. It only knows about processes.

So, in the end, your best option remains your current solution: just spawn another process and be happy about it. And, I would agree with the previous answers, you don't have to write another console app for each case. Just pass a fully qualified name of a static method, and have the console app load your assembly, load your type, and invoke the method. You can actually package it pretty neatly in a very much the same way as you tried with AppDomains. You can create a method called something like "RunInAnotherProcess", which will examine the argument, get the full type name and method name out of it (while making sure the method is static) and spawn the console app, which will do the rest.

Take care and good luck.

Fyodor