Фон
У меня есть служба Windows, которая использует различные сторонние библиотеки DLL для работы с файлами PDF. Эти операции могут использовать довольно много системных ресурсов и иногда, по-видимому, страдают утечками памяти при возникновении ошибок. DLL файлы управляются обертки вокруг других неуправляемых библиотек DLL.
Текущее решение
Я уже смягчаю эту проблему в одном случае, завершая вызов одной из DLL в специальном консольном приложении и вызываю это приложение через Process.Start(). Если операция завершается с ошибкой, и есть утечки памяти или невыпущенные файлы, это не имеет большого значения. Процесс завершится, и ОС восстановит дескрипторы.
Я бы хотел применить эту же логику к другим местам моего приложения, которые используют эти DLL. Тем не менее, я не очень рад добавить дополнительные консольные проекты в свое решение и написать еще больше кода котельной, который вызывает Process.Start() и анализирует вывод консольных приложений.
Новое решение
Элегантной альтернативой выделенным консольным приложениям и Process.Start(), по-видимому, является использование AppDomains, например: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.
Я реализовал аналогичный код в своем приложении, но модульные тесты не были многообещающими. Я создаю FileStream для тестового файла в отдельном AppDomain, но не удаляю его. Затем я пытаюсь создать еще один FileStream в основном домене, и он выходит из строя из-за неизданной блокировки файла.
Интересно, что добавление пустого события DomainUnload в рабочий домен делает проход unit test. Несмотря на это, я обеспокоен тем, что, возможно, создание "рабочего" AppDomains не решит мою проблему.
Мысли?
Код
/// <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 () );
}
}
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 ) )
{
}
}