|
| 1 | +// Copyright (c) Microsoft. All rights reserved. |
| 2 | +// Licensed under the MIT license. See LICENSE file in the project root for full license information. |
| 3 | + |
| 4 | +using System.IO; |
| 5 | +using System.Reflection; |
| 6 | +using Xunit; |
| 7 | + |
| 8 | +namespace System.Diagnostics |
| 9 | +{ |
| 10 | + /// <summary>Base class used for all tests that need to spawn a remote process.</summary> |
| 11 | + public abstract class RemoteExecutorTestBase : FileCleanupTestBase |
| 12 | + { |
| 13 | + /// <summary>The CoreCLR host used to host the test console app.</summary> |
| 14 | + protected const string HostRunner = "corerun"; |
| 15 | + /// <summary>The name of the test console app.</summary> |
| 16 | + protected const string TestConsoleApp = "RemoteExecutorConsoleApp.exe"; |
| 17 | + |
| 18 | + /// <summary>A timeout (milliseconds) after which a wait on a remote operation should be considered a failure.</summary> |
| 19 | + internal const int FailWaitTimeoutMilliseconds = 30 * 1000; |
| 20 | + /// <summary>The exit code returned when the test process exits successfully.</summary> |
| 21 | + internal const int SuccessExitCode = 42; |
| 22 | + |
| 23 | + /// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> |
| 24 | + /// <param name="method">The method to invoke.</param> |
| 25 | + /// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param> |
| 26 | + internal static RemoteInvokeHandle RemoteInvoke( |
| 27 | + Func<int> method, |
| 28 | + bool start = true) |
| 29 | + { |
| 30 | + return RemoteInvoke(method.GetMethodInfo(), Array.Empty<string>(), start); |
| 31 | + } |
| 32 | + |
| 33 | + /// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> |
| 34 | + /// <param name="method">The method to invoke.</param> |
| 35 | + /// <param name="arg1">The first argument to pass to the method.</param> |
| 36 | + /// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param> |
| 37 | + internal static RemoteInvokeHandle RemoteInvoke( |
| 38 | + Func<string, int> method, |
| 39 | + string arg, |
| 40 | + bool start = true) |
| 41 | + { |
| 42 | + return RemoteInvoke(method.GetMethodInfo(), new[] { arg }, start); |
| 43 | + } |
| 44 | + |
| 45 | + /// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> |
| 46 | + /// <param name="method">The method to invoke.</param> |
| 47 | + /// <param name="arg1">The first argument to pass to the method.</param> |
| 48 | + /// <param name="arg2">The second argument to pass to the method.</param> |
| 49 | + /// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param> |
| 50 | + internal static RemoteInvokeHandle RemoteInvoke( |
| 51 | + Func<string, string, int> method, |
| 52 | + string arg1, string arg2, |
| 53 | + bool start = true) |
| 54 | + { |
| 55 | + return RemoteInvoke(method.GetMethodInfo(), new[] { arg1, arg2 }, start); |
| 56 | + } |
| 57 | + |
| 58 | + /// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> |
| 59 | + /// <param name="method">The method to invoke.</param> |
| 60 | + /// <param name="arg1">The first argument to pass to the method.</param> |
| 61 | + /// <param name="arg2">The second argument to pass to the method.</param> |
| 62 | + /// <param name="arg3">The third argument to pass to the method.</param> |
| 63 | + /// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param> |
| 64 | + internal static RemoteInvokeHandle RemoteInvoke( |
| 65 | + Func<string, string, string, int> method, |
| 66 | + string arg1, string arg2, string arg3, |
| 67 | + bool start = true) |
| 68 | + { |
| 69 | + return RemoteInvoke(method.GetMethodInfo(), new[] { arg1, arg2, arg3 }, start); |
| 70 | + } |
| 71 | + |
| 72 | + /// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary> |
| 73 | + /// <param name="method">The method to invoke.</param> |
| 74 | + /// <param name="args">The arguments to pass to the method.</param> |
| 75 | + /// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param> |
| 76 | + private static RemoteInvokeHandle RemoteInvoke(MethodInfo method, string[] args, bool start) |
| 77 | + { |
| 78 | + // Verify the specified method is and that it returns an int (the exit code), |
| 79 | + // and that if it accepts any arguments, they're all strings. |
| 80 | + Assert.Equal(typeof(int), method.ReturnType); |
| 81 | + Assert.All(method.GetParameters(), pi => Assert.Equal(typeof(string), pi.ParameterType)); |
| 82 | + |
| 83 | + // And make sure it's in this assembly. This isn't critical, but it helps with deployment to know |
| 84 | + // that the method to invoke is available because we're already running in this assembly. |
| 85 | + Type t = method.DeclaringType; |
| 86 | + Assembly a = t.GetTypeInfo().Assembly; |
| 87 | + Assert.Equal(typeof(RemoteExecutorTestBase).GetTypeInfo().Assembly, a); |
| 88 | + |
| 89 | + // Start the other process and return a wrapper for it to handle its lifetime and exit checking. |
| 90 | + ProcessStartInfo psi = new ProcessStartInfo() |
| 91 | + { |
| 92 | + FileName = HostRunner, |
| 93 | + Arguments = TestConsoleApp + " \"" + a.FullName + "\" " + t.FullName + " " + method.Name + " " + string.Join(" ", args), |
| 94 | + UseShellExecute = false, |
| 95 | + CreateNoWindow = false |
| 96 | + }; |
| 97 | + |
| 98 | + // Profilers / code coverage tools doing coverage of the test process set environment |
| 99 | + // variables to tell the targeted process what profiler to load. We don't want the child process |
| 100 | + // to be profiled / have code coverage, so we remove these environment variables for that process |
| 101 | + // before it's started. |
| 102 | + psi.Environment.Remove("Cor_Profiler"); |
| 103 | + psi.Environment.Remove("Cor_Enable_Profiling"); |
| 104 | + psi.Environment.Remove("CoreClr_Profiler"); |
| 105 | + psi.Environment.Remove("CoreClr_Enable_Profiling"); |
| 106 | + |
| 107 | + // Return the handle to the process, which may or not be started |
| 108 | + return new RemoteInvokeHandle(start ? |
| 109 | + Process.Start(psi) : |
| 110 | + new Process() { StartInfo = psi }); |
| 111 | + } |
| 112 | + |
| 113 | + /// <summary>A cleanup handle to the Process created for the remote invocation.</summary> |
| 114 | + internal sealed class RemoteInvokeHandle : IDisposable |
| 115 | + { |
| 116 | + public RemoteInvokeHandle(Process process) |
| 117 | + { |
| 118 | + Process = process; |
| 119 | + } |
| 120 | + |
| 121 | + public Process Process { get; private set; } |
| 122 | + |
| 123 | + public void Dispose() |
| 124 | + { |
| 125 | + if (Process != null) |
| 126 | + { |
| 127 | + // A bit unorthodox to do throwing operations in a Dispose, but by doing it here we avoid |
| 128 | + // needing to do this in every derived test and keep each test much simpler. |
| 129 | + try |
| 130 | + { |
| 131 | + Assert.True(Process.WaitForExit(FailWaitTimeoutMilliseconds)); |
| 132 | + Assert.Equal(SuccessExitCode, Process.ExitCode); |
| 133 | + } |
| 134 | + finally |
| 135 | + { |
| 136 | + // Cleanup |
| 137 | + try { Process.Kill(); } |
| 138 | + catch { } // ignore all cleanup errors |
| 139 | + |
| 140 | + Process.Dispose(); |
| 141 | + Process = null; |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | +} |
0 commit comments