private static MethodInfo GetMethodInfo(Delegate d) { // RemoteInvoke doesn't support marshaling state on classes associated with // the delegate supplied (often a display class of a lambda). If such fields // are used, odd errors result, e.g. NullReferenceExceptions during the remote // execution. Try to ward off the common cases by proactively failing early // if it looks like such fields are needed. if (d.Target != null) { // The only fields on the type should be compiler-defined (any fields of the compiler's own // making generally include '<' and '>', as those are invalid in C# source). Note that this logic // may need to be revised in the future as the compiler changes, as this relies on the specifics of // actually how the compiler handles lifted fields for lambdas. Type targetType = d.Target.GetType(); Assert.All( targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), fi => Assert.True(fi.Name.IndexOf('<') != -1, $"Field marshaling is not supported by {nameof(RemoteInvoke)}: {fi.Name}")); } return(d.GetMethodInfo()); }
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> /// <param name="method">The method to invoke.</param> /// <param name="args">The arguments to pass to the method.</param> /// <param name="options">Options to use for the invocation.</param> /// <param name="pasteArguments">true if this function should paste the arguments (e.g. surrounding with quotes); false if that responsibility is left up to the caller.</param> private static RemoteInvokeHandle RemoteInvoke(MethodInfo method, string[] args, RemoteInvokeOptions options, bool pasteArguments = true) { options = options ?? new RemoteInvokeOptions(); // Verify the specified method returns an int (the exit code) or nothing, // and that if it accepts any arguments, they're all strings. Assert.True(method.ReturnType == typeof(void) || method.ReturnType == typeof(int) || method.ReturnType == typeof(Task <int>)); Assert.All(method.GetParameters(), pi => Assert.Equal(typeof(string), pi.ParameterType)); // And make sure it's in this assembly. This isn't critical, but it helps with deployment to know // that the method to invoke is available because we're already running in this assembly. Type t = method.DeclaringType; Assembly a = t.GetTypeInfo().Assembly; // Start the other process and return a wrapper for it to handle its lifetime and exit checking. ProcessStartInfo psi = options.StartInfo; psi.UseShellExecute = false; if (!options.EnableProfiling) { // Profilers / code coverage tools doing coverage of the test process set environment // variables to tell the targeted process what profiler to load. We don't want the child process // to be profiled / have code coverage, so we remove these environment variables for that process // before it's started. psi.Environment.Remove("Cor_Profiler"); psi.Environment.Remove("Cor_Enable_Profiling"); psi.Environment.Remove("CoreClr_Profiler"); psi.Environment.Remove("CoreClr_Enable_Profiling"); } // If we need the host (if it exists), use it, otherwise target the console app directly. string metadataArgs = PasteArguments.Paste(new string[] { a.FullName, t.FullName, method.Name, options.ExceptionFile }, pasteFirstArgumentUsingArgV0Rules: false); string passedArgs = pasteArguments ? PasteArguments.Paste(args, pasteFirstArgumentUsingArgV0Rules: false) : string.Join(" ", args); string testConsoleAppArgs = ExtraParameter + " " + metadataArgs + " " + passedArgs; // Uap console app is globally registered. if (!File.Exists(HostRunner) && !PlatformDetection.IsUap) { throw new IOException($"{HostRunner} test app isn't present in the test runtime directory."); } if (options.RunAsSudo) { psi.FileName = "sudo"; psi.Arguments = HostRunner + " " + testConsoleAppArgs; // Create exception file up front so there are no permission issue when RemoteInvokeHandle tries to delete it. File.WriteAllText(options.ExceptionFile, ""); } else { psi.FileName = HostRunner; psi.Arguments = testConsoleAppArgs; } // Return the handle to the process, which may or not be started return(new RemoteInvokeHandle(options.Start ? Process.Start(psi) : new Process() { StartInfo = psi }, options, a.FullName, t.FullName, method.Name )); }
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> /// <param name="method">The method to invoke.</param> /// <param name="args">The arguments to pass to the method.</param> /// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param> /// <param name="psi">The ProcessStartInfo to use, or null for a default.</param> private static RemoteInvokeHandle RemoteInvoke(MethodInfo method, string[] args, RemoteInvokeOptions options) { options = options ?? new RemoteInvokeOptions(); // Verify the specified method is and that it returns an int (the exit code), // and that if it accepts any arguments, they're all strings. Assert.True(method.ReturnType == typeof(int) || method.ReturnType == typeof(Task <int>)); Assert.All(method.GetParameters(), pi => Assert.Equal(typeof(string), pi.ParameterType)); // And make sure it's in this assembly. This isn't critical, but it helps with deployment to know // that the method to invoke is available because we're already running in this assembly. Type t = method.DeclaringType; Assembly a = t.GetTypeInfo().Assembly; Assert.Equal(typeof(RemoteExecutorTestBase).GetTypeInfo().Assembly, a); // Start the other process and return a wrapper for it to handle its lifetime and exit checking. var psi = options.StartInfo; psi.UseShellExecute = false; if (!options.EnableProfiling) { // Profilers / code coverage tools doing coverage of the test process set environment // variables to tell the targeted process what profiler to load. We don't want the child process // to be profiled / have code coverage, so we remove these environment variables for that process // before it's started. psi.Environment.Remove("Cor_Profiler"); psi.Environment.Remove("Cor_Enable_Profiling"); psi.Environment.Remove("CoreClr_Profiler"); psi.Environment.Remove("CoreClr_Enable_Profiling"); } // If we need the host (if it exists), use it, otherwise target the console app directly. string testConsoleAppArgs = "\"" + a.FullName + "\" " + t.FullName + " " + method.Name + " " + string.Join(" ", args); if (!File.Exists(TestConsoleApp)) { throw new IOException("RemoteExecutorConsoleApp test app isn't present in the test runtime directory."); } if (IsFullFramework) { psi.FileName = TestConsoleApp; psi.Arguments = testConsoleAppArgs; } else if (IsNetNative) { psi.FileName = TestConsoleApp; psi.Arguments = testConsoleAppArgs; // The test pipeline does not have the infrastructure to copy RemoteExecutorConsoleApp.exe to the test directly, let alone // ILC it against whatever assembly it might be asked to load up. (Is UWP even allowed to spin up a process!?) // Until, and unless that's fixed, throw an informative exception so as not to waste debugging time. throw new Exception(".NET Native platforms cannot run tests that use RemoteExecutorTestBase.RemoteInvoke()"); } else { psi.FileName = HostRunner; psi.Arguments = TestConsoleApp + " " + testConsoleAppArgs; } // Return the handle to the process, which may or not be started return(new RemoteInvokeHandle(options.Start ? Process.Start(psi) : new Process() { StartInfo = psi }, options)); }