So this is what I'm trying to do. I would like to have a log lived AssemblyLoadContext instance in order to quickly execute loaded assemblies.
When the assemblies updated event comes I would like to unload the context (I'm using .net core 3) and create a new context with versions of assemblies. When I try to do that however I get an error when unloading for the second time
so I'm guessing I'm doing something (very) wrong. I'm guessing the context doesn't get actually unloaded?
If the loaded assembly depends on some external dll I also get an error that that dll cannot be loaded, even though it is in the same folder. How can I prevent that?
Any advice would be very appreciated. This is my code
class Program
{
private static AssembliesContext assembliesContext;
const int iterations = 10;
static void Main(string[] args)
{
for (int i = 0; i < iterations; i++)
{
assembliesContext = new AssembliesContext();
RunViaReflection();
var wr = new WeakReference(assembliesContext);
assembliesContext.Unload();
assembliesContext = null;
int retries = 0;
while (wr.IsAlive && retries < 5)
{
GC.Collect();
GC.WaitForPendingFinalizers();
retries++;
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void RunViaReflection()
{
try
{
string dllPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "Execution.Sample", "bin", "Debug", "netcoreapp2.2", "win10-x64", "publish", "Execution.Sample.dll");
using (var fs = new FileStream(dllPath, FileMode.Open, FileAccess.Read))
{
var assembly = assembliesContext.LoadFromStream(fs);
var executableType = assembly.GetTypes()
.Where(x => typeof(IExecutableCode).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)
.FirstOrDefault();
IExecutableCode instance = (IExecutableCode)Activator.CreateInstance(executableType);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
public class AssembliesContext : AssemblyLoadContext
{
public AssembliesContext(): base(true)
{
}
protected override Assembly Load(AssemblyName assemblyName)
{
return null;
}
}
Edit
I got rid of the error by casting the invoke result to Task (since it's an async method):
((Task)method.Invoke(instance, new object[] { })).ConfigureAwait(false).GetAwaiter().GetResult();
I also loaded the depending assemblies like this:
foreach (var ass in assembly.GetReferencedAssemblies())
{
string path = Path.Combine(basePath, $"{ass.Name}.dll");
using (var dependantAssemblyStream = new FileStream(path, FileMode.Open, FileAccess.Read))
{
assembliesContext.LoadFromStream(dependantAssemblyStream);
}
}
I am still wondering how to manage a long lived AssemblyLoadContext since if I try to unload it as a class member it wil not unload, even if I set it to null and instantiate a new one.
Is a different approach warranted?
Edit #2
Right now I have a ConcurrentDictionary of WeakReference items which seems so work fairly ok, but every WeakReference in it loses it's target when the GC runs. Logical, but I still don't know how to make everything work.
If I do something like this
namespace AssemblyLoadTest
{
class Program
{
static void Main(string[] args)
{
AssemblyExecutor executor = new AssemblyExecutor();
for (int i = 0; i < 1; i++)
{
var assemblyPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "Assembly", "Sinapro.IoT.Testing.DotNetRuleTestImplementation.dll");
executor.Execute(assemblyPath);
Console.WriteLine($"Iteration {i} completed");
if (i > 0 && i % 10 == 0)
{
executor.Clear();
}
}
executor.Clear();
while (true)
{
Thread.Sleep(100);
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
public class AssemblyExecutor
{
private readonly ConcurrentDictionary<string, CollectibleAssemblyLoadContext> references = new ConcurrentDictionary<string, CollectibleAssemblyLoadContext>();
[MethodImpl(MethodImplOptions.NoInlining)]
public void Clear()
{
var keys = references.Keys.ToList();
foreach (var k in keys)
{
references.TryRemove(k, out CollectibleAssemblyLoadContext ctx);
ctx.Unload();
}
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC called");
}
public void Execute(string assemblyPath)
{
var wr = references.AddOrUpdate(
assemblyPath,
(k) =>
{
return CreateReference(assemblyPath);
},
(k, existingReference) =>
{
if (existingReference == null || !existingReference.Assemblies.Any())
{
return CreateReference(assemblyPath);
}
return existingReference;
}
);
ExecuteAssembly(wr);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ExecuteAssembly(CollectibleAssemblyLoadContext context)
{
//CollectibleAssemblyLoadContext context = wr.Target as CollectibleAssemblyLoadContext;
if (context == null)
return;
var assembly = context.Assemblies.FirstOrDefault(a => a.ExportedTypes.Any(t => t.Name == "TestEntry"));
var types = assembly.GetTypes().ToList();
var type = assembly.GetType("Sinapro.IoT.Testing.DotNetRuleTestImplementation.TestEntry");
var greetMethod = type.GetMethod("Execute");
var instance = Activator.CreateInstance(type);
var result = greetMethod.Invoke(instance, new object[] { null, null });
}
[MethodImpl(MethodImplOptions.NoInlining)]
private CollectibleAssemblyLoadContext CreateReference(string assemblyPath)
{
Console.WriteLine($"Creating new context");
var context = new CollectibleAssemblyLoadContext(assemblyPath);
using (var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read))
{
var assembly = context.LoadFromStream(fs);
}
return context;
}
}
public class CollectibleAssemblyLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public CollectibleAssemblyLoadContext(string path)
: base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(path);
}
protected override Assembly Load(AssemblyName assemblyName)
{
var deps = AssemblyLoadContext.Default;
var res = deps.Assemblies.Where(d => d.FullName.Contains(assemblyName.Name)).ToList();
if (res.Any())
{
return Assembly.Load(new AssemblyName(res.First().FullName));
}
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath == null)
return null;
return LoadFromAssemblyPath(assemblyPath);
}
}
}
everything runs fine, but the memory consumption keeps increasing. I find it curious though, since there shouldn't be any reference to the AssemblyLoadContext anymore so the GC should've cleared it?
Related
I am using mock library in my .Net unit test and getting an error
cannot be accessed with an instance reference instead use type name.
I am getting this error at following line in my test method where it is calling cq.Instance. I am new to mock library. Could somebody let me know how do I call the static method?
attributeValue.Setup(cq => cq.Instance().CallQueryAsync(request, 1)).Returns(attrValue);
Actual method to be tested
public static async Task<AttributeValueList> GetAttributeSecDateValueList(int attrId)
{
try
{
var request = AttributeValue.ResolveRequest(attrId);
var response = await AsyncProxy<AttributeValue>.Instance().CallQueryAsync(request, (int)AttributeValue.OperationType.GetSecDateValues);
var coll = new AttributeValueList();
coll.AddRange(response);
return coll;
}
catch (Exception e)
{
throw e;
}
}
Proxy class
public class AsyncProxy<RT> : IDisposable
where RT : class, new()
{
readonly WebServiceProxy<RT> _wsProxy;
private AsyncProxy(WebServiceProxy<RT> webServiceProxy)
{
_wsProxy = webServiceProxy;
}
public static async Task<IEnumerable<RT>> Load(object parameters)
{
return await Instance().CallQueryAsync(parameters);
}
public static AsyncProxy<RT> Instance()
{
return new AsyncProxy<RT>(WebServiceProxy<RT>.Instance());
}
/// <summary>
/// Return result set of Poco as smartCollection
/// </summary>
public async Task<SmartCollection<RT>> CallQueryAsync(object request, int? uniqueIdentifier = null, bool isLongRunning = false, [CallerMemberName]string memberName = "")
{
//#if DEBUG
// var stopwatch = new Stopwatch();
// stopwatch.Start();
//#endif
try
{
// We want to get rid of the proxy as soon as we are done with it
using (_wsProxy)
{
var awaited = await _wsProxy.CallQueryAsync(request, uniqueIdentifier, isLongRunning);
if (awaited == null)
return null;
var observableCollection = new SmartCollection<RT>();
foreach (var item in awaited)
observableCollection.Add(item as RT);
return observableCollection;
}
}
finally
{
Dispose();
//#if DEBUG
// stopwatch.Stop();
// Debug.WriteLine(null);
// Debug.WriteLine($"****>>>>> AsyncProxy {memberName} took {stopwatch.ElapsedMilliseconds} ms <<<<<<<<****");
//#endif
}
}
}
test method
[TestMethod]
public void test()
{
Task<SmartCollection<AttributeValue>> attrValue = null;
var request = new AttributeValue();
var attributeValue = new Mock<AsyncProxy<AttributeValue>>();
attributeValue.Setup(cq => cq.Instance().CallQueryAsync(request, 1)).Returns(attrValue);
}
Summary
I'm trying to write a small API for my project that would provide a near drop-in replacement for the System.DllImportAttribute. How would I dynamically either replace or add a body to a method marked static extern?
Background
I've seen a few answers on here (here and here) that show how to intercept and dynamically replace methods, but they are not extern, and I can't get any of them to work with methods that aren't.
My current API does the following:
Finds all methods marked with a NativeCallAttribute and returns a MethodInfo[].
For each MethodInfo in the returned MethodInfo[]:
Loads the specified library (if not already loaded) using either LoadLibrary or dlopen.
Get an IntPtr representing the function pointer for a specified method from the loaded library using either GetProcAddress or dlsym.
Generates a delegate for the native function pointer based on the current MethodInfo.
Gets a MethodInfo from the generated delegate to replace the existing one.
Replaces the old method with the new one.
Code
My current implementation takes a similar approach to this project in terms of getting an attribute set up and gathering information about the attached method, and this stackoverflow answer in terms of replacing the function body with a native delegate.
The current API that I have to load the library and get the function pointer work as they should (as they work in other situations than this), and they have been excluded from the code below.
Current API
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Security;
using TCDFx.ComponentModel;
namespace TCDFx.InteropServices
{
// Indicates that the attributed method is exposed by an native assembly as a static entry point.
[CLSCompliant(false)]
[SuppressUnmanagedCodeSecurity]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class NativeCallAttribute : Attribute
{
// The name of the native method.
public string EntryPoint;
// Initializes a new instance of the NativeCallAttribute.
public NativeCallAttribute(params string[] assemblyNames)
{
if (assemblyNames == null || assemblyNames.Length == 0)
throw new NativeCallException("No assembly specified.");
string[] names = new string[] { };
int i = 0;
foreach (string name in assemblyNames)
{
if (!string.IsNullOrWhiteSpace(name))
{
names[i] = name;
i++;
}
}
AssemblyNames = names;
}
// An ordered list of assembly names.
public string[] AssemblyNames { get; }
}
[SuppressUnmanagedCodeSecurity]
public static class NativeCalls
{
private static readonly object sync = new object();
// Replaces all defined functions with the 'NativeCallAttribute' that are 'static' and 'extern' with their native call.
public static void Load()
{
lock (sync)
{
MethodInfo[] funcInfo = GetNativeCalls();
for (int i = 0; i < funcInfo.Length; i++)
{
NativeCallAttribute attribute = funcInfo[i].GetCustomAttribute<NativeCallAttribute>(false);
NativeAssemblyBase nativeAssembly;
if (IsAssemblyCached(attribute.AssemblyNames, out NativeAssemblyBase cachedAssembly))
nativeAssembly = cachedAssembly;
else
{
if (TryLoadAssembly(attribute.AssemblyNames, out NativeAssemblyBase loadedAssembly, out Exception loadingEx))
nativeAssembly = loadedAssembly;
else
throw loadingEx;
}
string funcName = attribute.EntryPoint ?? funcInfo[i].Name;
IntPtr funcPtr = nativeAssembly.LoadFunctionPointer(funcName);
Delegate funcDelegate = GenerateNativeDelegate(funcName, nativeAssembly.Name, funcInfo[i], funcPtr);
MethodInfo funcInfoNew = funcDelegate.GetMethodInfo();
MethodReplacementState state = ReplaceMethod(funcInfo[i], funcInfoNew);
replacements.Add(state);
}
}
}
// Gets all methods marked with a 'NativeCallAttribute'.
private static MethodInfo[] GetNativeCalls()
{
List<MethodInfo> result = new List<MethodInfo>();
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
Type[] types = assemblies[i].GetTypes();
for (int ii = 0; ii < types.Length; ii++)
{
MethodInfo[] methods = types[ii].GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
for (int iii = 0; iii < methods.Length; iii++)
{
Attribute attr = methods[iii].GetCustomAttribute<NativeCallAttribute>(false);
if (attr != null)
result.Add(methods[iii]);
}
}
}
return result.ToArray();
}
// Gets a 'Delegate' for a native function pointer with information provided from the method to replace.
private static Delegate GenerateNativeDelegate(string funcName, string assemblyName, MethodInfo funcInfo, IntPtr funcPtr)
{
Type returnType = funcInfo.ReturnType;
ParameterInfo[] #params = funcInfo.GetParameters();
Type[] paramTypes = new Type[] { };
for (int i = 0; i < #params.Length; i++)
paramTypes[i] = #params[i].ParameterType;
DynamicMethod nativeMethod = new DynamicMethod($"{assemblyName}_{funcName}", returnType, paramTypes, funcInfo.Module);
ILGenerator ilGenerator = nativeMethod.GetILGenerator();
// Generate the arguments
for (int i = 0; i < #params.Length; i++)
{
//TODO: See if I need separate out code for this...
if (#params[i].ParameterType.IsByRef || #params[i].IsOut)
{
ilGenerator.Emit(OpCodes.Ldarg, i);
ilGenerator.Emit(OpCodes.Ldnull);
ilGenerator.Emit(OpCodes.Stind_Ref);
}
else
{
ilGenerator.Emit(OpCodes.Ldarg, i);
}
}
// Push the funcPtr to the stack
if (IntPtr.Size == 4)
ilGenerator.Emit(OpCodes.Ldc_I4, funcPtr.ToInt32());
else if (IntPtr.Size == 8)
ilGenerator.Emit(OpCodes.Ldc_I8, funcPtr.ToInt64());
else throw new PlatformNotSupportedException();
// Call it and return;
ilGenerator.EmitCall(OpCodes.Call, funcInfo, null);
ilGenerator.Emit(OpCodes.Ret);
Type delegateType = Expression.GetDelegateType((from param in #params select param.ParameterType).Concat(new[] { returnType }).ToArray());
return nativeMethod.CreateDelegate(delegateType);
}
private static bool IsAssemblyCached(string[] assemblyNames, out NativeAssemblyBase cachedAssembly)
{
bool result = false;
cachedAssembly = null;
foreach (string name in assemblyNames)
{
if (!Component.Cache.ContainsKey(Path.GetFileNameWithoutExtension(name)))
{
Type asmType = Component.Cache[Path.GetFileNameWithoutExtension(name)].Value1;
if (asmType == typeof(NativeAssembly))
cachedAssembly = (NativeAssembly)Component.Cache[Path.GetFileNameWithoutExtension(name)].Value2;
else if (asmType == typeof(NativeAssembly))
cachedAssembly = (DependencyNativeAssembly)Component.Cache[Path.GetFileNameWithoutExtension(name)].Value2;
else if (asmType == typeof(NativeAssembly))
cachedAssembly = (EmbeddedNativeAssembly)Component.Cache[Path.GetFileNameWithoutExtension(name)].Value2;
result = true;
break;
}
}
return result;
}
private static bool TryLoadAssembly(string[] assemblyNames, out NativeAssemblyBase loadedAssembly, out Exception exception)
{
bool result = false;
exception = null;
try
{
loadedAssembly = new NativeAssembly(assemblyNames);
}
catch (Exception ex)
{
exception = ex;
loadedAssembly = null;
}
try
{
if (exception != null)
loadedAssembly = new DependencyNativeAssembly(assemblyNames);
}
catch (Exception ex)
{
exception = ex;
loadedAssembly = null;
}
try
{
if (exception == null)
loadedAssembly = new EmbeddedNativeAssembly(assemblyNames);
}
catch (Exception ex)
{
exception = ex;
loadedAssembly = null;
}
return result;
}
private static unsafe MethodReplacementState ReplaceMethod(MethodInfo targetMethod, MethodInfo replacementMethod)
{
if (!(targetMethod.GetMethodBody() == null && targetMethod.IsStatic))
throw new NativeCallException($"Only the replacement of methods marked 'static extern' is supported.");
#if DEBUG
RuntimeHelpers.PrepareMethod(targetMethod.MethodHandle);
RuntimeHelpers.PrepareMethod(replacementMethod.MethodHandle);
#endif
IntPtr target = targetMethod.MethodHandle.Value;
IntPtr replacement = replacementMethod.MethodHandle.Value + 8;
if (!targetMethod.IsVirtual)
target += 8;
else
{
int i = (int)(((*(long*)target) >> 32) & 0xFF);
IntPtr classStart = *(IntPtr*)(targetMethod.DeclaringType.TypeHandle.Value + (IntPtr.Size == 4 ? 40 : 64));
target = classStart + (IntPtr.Size * i);
}
#if DEBUG
target = *(IntPtr*)target + 1;
replacement = *(IntPtr*)replacement + 1;
MethodReplacementState state = new MethodReplacementState(target, new IntPtr(*(int*)target));
*(int*)target = *(int*)replacement + (int)(long)replacement - (int)(long)target;
return state;
#else
MethodReplacementState state = new MethodReplacementState(target, *(IntPtr*)target);
* (IntPtr*)target = *(IntPtr*)replacement;
return state;
#endif
}
private readonly struct MethodReplacementState : IDisposable
{
private readonly IntPtr Location;
private readonly IntPtr OriginalValue;
public MethodReplacementState(IntPtr location, IntPtr origValue)
{
Location = location;
OriginalValue = origValue;
}
public void Dispose() => Restore();
private unsafe void Restore() =>
#if DEBUG
*(int*)Location = (int)OriginalValue;
#else
*(IntPtr*)Location = OriginalValue;
#endif
}
}
}
Test Code
using TCDFx.InteropServices;
namespace NativeCallExample
{
internal class Program
{
internal static void Main()
{
NativeCalls.Load();
Beep(2000, 400);
}
[NativeCall("kernel32.dll")]
private static extern bool Beep(uint frequency, uint duration);
}
}
Expected/Actual Results
I expected it to run as it should (as listed above in the description), but it is crashing dotnet.exe with error code -532462766. No breakpoints are hit anywhere in the code (in the test app or API library), and no exception is thrown. I believe the problem to be happening between steps 2.3 and 2.5 above, but I'm pretty stuck at the moment. Any help would be appreciated!
More Information
If you want to see the referenced code that isn't included and a full copy of what I have for this, you can find it in this branch of my project.
I'm getting the following error when running a process that updates a Lucene index with some User data (a domain object).
Lucene.Net.Store.LockObtainFailedException: Lock obtain timed out:
AzureLock#write.lock. at Lucene.Net.Store.Lock.Obtain(Int64
lockWaitTimeout) in
d:\Lucene.Net\FullRepo\trunk\src\core\Store\Lock.cs: line 97 at
Lucene.Net.Index.IndexWriter.Init(Directory d, Analyzer a, Boolean
create, IndexDeletionPolicy deletionPolicy, Int32 maxFieldLength,
IndexingChain indexingChain, IndexCommit commit) in
d:\Lucene.Net\FullRepo\trunk\src\core\Index\IndexWriter.cs: line 1228
at Lucene.Net.Index.IndexWriter..ctor(Directory d, Analyzer a,
MaxFieldLength mfl) in
d:\Lucene.Net\FullRepo\trunk\src\core\Index\IndexWriter.cs: line 174
at
MyApp.ApplicationServices.Search.Users.UsersSearchEngineService.EnsureIndexWriter()
at
MyApp.ApplicationServices.Search.Users.UsersSearchEngineService.DoWriterAction(Action`1
action) at....
I've inherited the following code and my knowledge of Lucene is not great, so any pointers would be appreciated.
public class UsersSearchEngineService : IUsersSearchEngineService
{
private readonly Directory directory;
private readonly Analyzer analyzer;
private static IndexWriter indexWriter;
private static readonly Object WriterLock = new Object();
private bool disposed;
public UsersSearchEngineService (ILuceneDirectoryProvider directoryProvider)
{
directory = directoryProvider.GetDirectory();
analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
}
public int UpdateIndex(IEnumerable<User> itemsToIndex)
{
var updatedCount = 0;
foreach (var itemToIndex in itemsToIndex)
{
try
{
ExecuteRemoveItem(itemToIndex.UserId);
var document = CreateIndexDocument(itemToIndex);
DoWriterAction(writer => writer.AddDocument(document));
updatedCount++;
}
catch (Exception ex)
{
EventLogProvider.Error("Error updating index for User with id:[{0}]", ex, itemToIndex.UserId);
}
}
DoWriterAction(writer =>
{
writer.Commit();
writer.Optimize();
});
return updatedCount;
}
private static Document CreateIndexDocument(User itemToIndex)
{
var document = new Document();
document.Add(new Field("id", itemToIndex.UserId.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NOT_ANALYZED));
//...
//omitted other fields being added here
//...
return document;
}
void ExecuteRemoveItem(int entryId)
{
var searchQuery = GetIdSearchQuery(entryId);
DoWriterAction(writer => writer.DeleteDocuments(searchQuery));
}
void DoWriterAction(Action<IndexWriter> action)
{
lock (WriterLock)
{
EnsureIndexWriter();
}
action(indexWriter);
}
void EnsureIndexWriter()
{
if (indexWriter != null)
{
return;
}
indexWriter = new IndexWriter(this.directory, this.analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
indexWriter.SetMergePolicy(new LogDocMergePolicy(indexWriter) { MergeFactor = 5 });
var retryStrategy = new ExponentialBackoff(5, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10));
var retryPolicy = new RetryPolicy<LuceneWriterLockedErrorDetectionStrategy>(retryStrategy);
retryPolicy.Retrying += (sender, args) => EventLogProvider.Warn("Retrying lock delete Attempt: " + args.CurrentRetryCount);
if (IndexWriter.IsLocked(this.directory))
{
retryPolicy.ExecuteAction(() =>
{
EventLogProvider.Info("Something left a lock in the index folder: Attempting to it");
IndexWriter.Unlock(directory);
EventLogProvider.Info("Lock Deleted... can proceed");
});
}
}
~UsersSearchEngineService ()
{
Dispose();
}
public void Dispose()
{
lock (WriterLock)
{
if (!disposed)
{
var writer = indexWriter;
if (writer != null)
{
try
{
writer.Dispose();
}
catch (ObjectDisposedException e)
{
EventLogProvider.Error("Exception while disposing SearchEngineService", e);
}
indexWriter = null;
}
var disposeDirectory = directory;
if (disposeDirectory != null)
{
try
{
disposeDirectory.Dispose();
}
catch (ObjectDisposedException e)
{
EventLogProvider.Error("Exception while disposing SearchEngineService", e);
}
}
disposed = true;
}
}
GC.SuppressFinalize(this);
}
}
In case it's relevant:
the application is hosted as an Azure Web App, running 2 instances
the index is stored in Azure Storage
the procedure runs as a scheduled process defined in a CMS.
The CMS will only launch allow one instance of scheduled process to be running at any
one time in a load balanced scenario (e.g. when running 2
instances in Azure)
Does the EnsureIndexWriter method look correct? If not, how should it be reworked?
I have an IIS Application which references a plugin manager which reads all available plugins from a folder, when it is hosted after about 5-10 minutes I start getting the following Exception
[RemotingException: Object '/1608465e_9d80_4b40_be20_4c96904643e0/wizi+0g5od5gwmunm_indiws_253.rem' has been disconnected or does not exist at the server.]
System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg) +14416170
System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type) +388
PluginManager.Core.Interfaces.IPluggin.get_Id() +0
PluginManager.PluginDetails.get_Id() +27
I did some research and came accross ILease and ISponsor but I have no Idea how to implement it or how it works. My current code as it is at this moment, I just removed parts the body of the methods for clarity
[Edit : added the method bodies]
public class AssemblyReflectionProxy : MarshalByRefObject
{
private string _assemblyPath;
public AssemblyReflectionProxy()
{
Id = "";
}
public void LoadAssembly(String assemblyPath)
{
try
{
_assemblyPath = assemblyPath;
Assembly.ReflectionOnlyLoadFrom(assemblyPath);
}
catch (FileNotFoundException)
{
}
}
public TResult Reflect<TResult>(Func<Assembly, TResult> func)
{
var directory = new FileInfo(_assemblyPath).Directory;
ResolveEventHandler resolveEventHandler = (s, e) => OnReflectionOnlyResolve(e, directory);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
var assembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(a => String.Compare(a.Location, _assemblyPath, StringComparison.Ordinal) == 0);
var result = func(assembly);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
return result;
}
public T GetEntryType<T>()
{
var directory = new FileInfo(_assemblyPath).Directory;
ResolveEventHandler resolveEventHandler = (s, e) => OnReflectionOnlyResolve(e, directory);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
var assembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(a => string.Compare(a.Location, _assemblyPath, StringComparison.Ordinal) == 0);
if (assembly != null)
{
var result = assembly.GetTypes();
var type = result.FirstOrDefault(x => x.GetInterface(typeof(T).Name) != null);
if (type != null)
{
var remoteObject = AppDomain.CurrentDomain.CreateInstanceFrom(type.Assembly.Location, type.FullName);
var obj = remoteObject.Unwrap();
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
return (T)obj;
}
}
return default(T);
}
private Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
{
var loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
if (loadedAssembly != null)
{
return loadedAssembly;
}
var assemblyName = new AssemblyName(args.Name);
var dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
if (File.Exists(dependentAssemblyFilename))
{
return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
}
return Assembly.ReflectionOnlyLoad(args.Name);
}
private string Id { get; set; }
internal string GetId()
{
if (String.IsNullOrEmpty(Id))
{
var fileBytes = File.ReadAllBytes(_assemblyPath);
var hash = Convert.ToBase64String(fileBytes).GetHashCode();
var bytes = BitConverter.GetBytes(hash);
StringBuilder sb = new StringBuilder();
foreach (byte b in bytes)
sb.Append(b.ToString("X2"));
Id = sb.ToString();
}
return Id;
}
}
public sealed class AssemblyManager : MarshalByRefObject, IDisposable
{
private readonly Dictionary<string, AppDomain> _assemblyDomains = new Dictionary<string, AppDomain>();
readonly Dictionary<string, AssemblyReflectionProxy> _proxies = new Dictionary<string, AssemblyReflectionProxy>();
public AssemblyManager()
{
}
public string LoadAssembly(string assemblyPath)
{
var fileInfo = new FileInfo(assemblyPath);
var name = fileInfo.Name.Replace(".dll", "");
if (fileInfo.Exists)
{
if (!_assemblyDomains.ContainsKey(name))
{
var appDomain = CreateChildDomain(AppDomain.CurrentDomain, fileInfo.Name);
_assemblyDomains[name] = appDomain;
try
{
Type proxyType = typeof(AssemblyReflectionProxy);
{
var proxy = (AssemblyReflectionProxy)appDomain.CreateInstanceFrom(proxyType.Assembly.Location, proxyType.FullName).Unwrap();
proxy.LoadAssembly(assemblyPath);
_proxies[name] = proxy;
return name;
}
}
catch
{ }
}
else
{
return name;
}
}
return "";
}
public void Unload()
{
}
private AppDomain CreateChildDomain(AppDomain parentDomain, string domainName)
{
var evidence = new Evidence(parentDomain.Evidence);
var setup = parentDomain.SetupInformation;
return AppDomain.CreateDomain(domainName, evidence, setup);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~AssemblyManager()
{
Dispose(false);
}
public IPluggin GetEntryPluggin(string name)
{
IPluggin plugin = default(IPluggin);
if (_proxies.ContainsKey(name))
{
plugin = _proxies[name].GetEntryType<IPluggin>();
}
return plugin;
}
private void Dispose(bool disposing)
{
if (disposing)
{
foreach (var appDomain in _assemblyDomains.Values)
AppDomain.Unload(appDomain);
_assemblyDomains.Clear();
}
}
internal string GetEntryPlugginID(string name)
{
string Id = "";
if (_proxies.ContainsKey(name))
{
Id = _proxies[name].GetId();
}
return Id;
}
}
My Interface is
public interface IPluggin
{
string Name { get; }
string Version { get; }
string Id { get; }
void Initialize();
string[] GetElements();
void SaveSettings(string settings);
void SetBasePath(string path);
}
.NET remoting is kind of deprecated, but I don't see why it would be a problem for communicating between application domains, so...
When you create a new instance of a remoting object, what you actually do is request the remote side to create one for you, while you only maintain a proxy. The remote side associates each of such objects with a lease - something that describes how the lifetime of the object is handled. This means that even if the client side still has strong references to the remote object, the remote object can be garbage collected when its lease expires.
The easiest way to check if this happened is using the RemotingServices.GetLifetimeService method. Just use it on the proxy object, and you will get the information you need - for example, CurrentState will tell you if the remote is still alive. If it is, you can also extend the lease using Renew.
So, the usual way to handle the lifetime of the remote object would be - check if it is still alive; if it is, extend the lease and do whatever you want. Make sure you check CurrentLeaseTime too - it's a good idea to maintain it some reasonable value, rather than always Renewing a fixed amount of time.
I'm currently using the following method to check for test assemblies:
private bool IsTestAssembly(string path)
{
var assembly = Assembly.LoadFrom(path);
foreach (var type in assembly.GetTypes())
{
var a = type.GetCustomAttributes(true).Select(x => x.ToString());
if (a.Contains("Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute"))
return true;
}
return false;
}
But I would like to check this without loading the assembly in memory because I need to be able to delete it afterwards in case the verification fails.
I was hoping I could simply unload the assembly, but I soon discovered that, according to MSDN:
There is no way to unload an individual assembly without unloading all of the application domains that contain it.
Thanks in advance!
I worked out a short solution as suggested by TheGreatCO, i.e. to load the assembly in a new AppDomain:
1) Usage:
// assemblies are unloaded on disposal
using (var analyser = new AssemblyAnalyser())
{
var path = "my.unit.tests.dll";
var b = analyser.IsTestAssembly(path);
Assert.IsTrue(b);
}
2) Implementation:
public class AssemblyAnalyser : MarshalByRefObject, IDisposable
{
public AssemblyAnalyser()
{
var evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
var appSetup = new AppDomainSetup()
{
ApplicationBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
};
appDomain = AppDomain.CreateDomain(otherDomainFriendlyName, evidence, appSetup);
}
public bool IsTestAssembly(string assemblyPath)
{
if (AppDomain.CurrentDomain.FriendlyName != otherDomainFriendlyName)
{
var analyser = appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, GetType().FullName);
return ((AssemblyAnalyser)analyser).IsTestAssembly(assemblyPath);
}
else
{
var assembly = Assembly.LoadFrom(assemblyPath);
return ContainsTestClasses(assembly);
}
}
public void Dispose()
{
if (AppDomain.CurrentDomain.FriendlyName != otherDomainFriendlyName)
{
AppDomain.Unload(appDomain);
GC.SuppressFinalize(this);
}
}
~AssemblyAnalyser()
{
Dispose();
}
private bool ContainsTestClasses(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
var attr = type.GetCustomAttributes(true).Select(x => x.ToString());
if (attr.Contains("Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute"))
return true;
}
return false;
}
private const string otherDomainFriendlyName = "AssemblyAnalyser";
private AppDomain appDomain;
}
Inspect the assemblies using Mono.Cecil. Cecil does not need to load the assembly to inspect it.