Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 8b7a0fa

Browse files
committed
Merge pull request #2642 from stephentoub/remoteexecutor
Improve process launching support in Process tests
2 parents 4ff02e9 + e8282f0 commit 8b7a0fa

37 files changed

+602
-1366
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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;
5+
using System.Reflection;
6+
7+
[assembly: System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
8+
9+
namespace RemoteExecutorConsoleApp
10+
{
11+
/// <summary>
12+
/// Provides an entry point in a new process that will load a specified method and invoke it.
13+
/// </summary>
14+
internal static class Program
15+
{
16+
static int Main(string[] args)
17+
{
18+
// The program expects to be passed the target assembly name to load, the type
19+
// from that assembly to find, and the method from that assembly to invoke.
20+
// Any additional arguments are passed as strings to the method.
21+
if (args.Length < 3)
22+
{
23+
Console.Error.WriteLine("Usage: {0} assemblyName typeName methodName", typeof(Program).GetTypeInfo().Assembly.GetName().Name);
24+
return -1;
25+
}
26+
string assemblyName = args[0];
27+
string typeName = args[1];
28+
string methodName = args[2];
29+
string[] additionalArgs = args.Length > 3 ?
30+
args.Subarray(3, args.Length - 3) :
31+
Array.Empty<string>();
32+
33+
// Load the specified assembly, type, and method, then invoke the method.
34+
// The program's exit code is the return value of the invoked method.
35+
Assembly a = null;
36+
Type t = null;
37+
MethodInfo mi = null;
38+
object instance = null;
39+
try
40+
{
41+
a = Assembly.Load(new AssemblyName(assemblyName));
42+
t = a.GetType(typeName);
43+
mi = t.GetMethod(methodName);
44+
if (!mi.IsStatic)
45+
{
46+
instance = Activator.CreateInstance(t);
47+
}
48+
return (int)mi.Invoke(instance, additionalArgs);
49+
}
50+
catch (Exception exc)
51+
{
52+
Console.Error.WriteLine("Exception from RemoteExecutorConsoleApp({0}):", string.Join(", ", args));
53+
Console.Error.WriteLine("Assembly: {0}", a);
54+
Console.Error.WriteLine("Type: {0}", t);
55+
Console.Error.WriteLine("Method: {0}", mi);
56+
Console.Error.WriteLine("Exception: {0}", exc);
57+
return -2;
58+
}
59+
finally
60+
{
61+
IDisposable d = instance as IDisposable;
62+
if (d != null)
63+
{
64+
d.Dispose();
65+
}
66+
}
67+
}
68+
69+
private static MethodInfo GetMethod(this Type type, string methodName)
70+
{
71+
Type t = type;
72+
while (t != null)
73+
{
74+
TypeInfo ti = t.GetTypeInfo();
75+
MethodInfo mi = ti.GetDeclaredMethod(methodName);
76+
if (mi != null)
77+
{
78+
return mi;
79+
}
80+
t = ti.BaseType;
81+
}
82+
return null;
83+
}
84+
85+
private static T[] Subarray<T>(this T[] arr, int offset, int count)
86+
{
87+
var newArr = new T[count];
88+
Array.Copy(arr, offset, newArr, 0, count);
89+
return newArr;
90+
}
91+
}
92+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<PropertyGroup>
55
<ProjectGuid>{F5E941C8-AF2F-47AB-A066-FF25470CE382}</ProjectGuid>
66
<OutputType>Exe</OutputType>
7-
<RootNamespace>InterProcessCommunication.Tests</RootNamespace>
8-
<AssemblyName>InterProcessCommunication.TestConsoleApp</AssemblyName>
7+
<RootNamespace>RemoteExecutorConsoleApp</RootNamespace>
8+
<AssemblyName>RemoteExecutorConsoleApp</AssemblyName>
99
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
1010
</PropertyGroup>
1111
<!-- Default configurations to help VS understand the configurations -->
1212
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' " />
1313
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' " />
1414
<ItemGroup>
15-
<Compile Include="TestConsoleApp.cs" />
15+
<Compile Include="RemoteExecutorConsoleApp.cs" />
1616
<Compile Include="$(CommonPath)\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs">
1717
<Link>Common\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs</Link>
1818
</Compile>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
};
96+
97+
// Profilers / code coverage tools doing coverage of the test process set environment
98+
// variables to tell the targeted process what profiler to load. We don't want the child process
99+
// to be profiled / have code coverage, so we remove these environment variables for that process
100+
// before it's started.
101+
psi.Environment.Remove("Cor_Profiler");
102+
psi.Environment.Remove("Cor_Enable_Profiling");
103+
psi.Environment.Remove("CoreClr_Profiler");
104+
psi.Environment.Remove("CoreClr_Enable_Profiling");
105+
106+
// Return the handle to the process, which may or not be started
107+
return new RemoteInvokeHandle(start ?
108+
Process.Start(psi) :
109+
new Process() { StartInfo = psi });
110+
}
111+
112+
/// <summary>A cleanup handle to the Process created for the remote invocation.</summary>
113+
internal sealed class RemoteInvokeHandle : IDisposable
114+
{
115+
public RemoteInvokeHandle(Process process)
116+
{
117+
Process = process;
118+
}
119+
120+
public Process Process { get; private set; }
121+
122+
public void Dispose()
123+
{
124+
if (Process != null)
125+
{
126+
// A bit unorthodox to do throwing operations in a Dispose, but by doing it here we avoid
127+
// needing to do this in every derived test and keep each test much simpler.
128+
try
129+
{
130+
Assert.True(Process.WaitForExit(FailWaitTimeoutMilliseconds));
131+
Assert.Equal(SuccessExitCode, Process.ExitCode);
132+
}
133+
finally
134+
{
135+
// Cleanup
136+
try { Process.Kill(); }
137+
catch { } // ignore all cleanup errors
138+
139+
Process.Dispose();
140+
Process = null;
141+
}
142+
}
143+
}
144+
}
145+
}
146+
}

src/Scenarios/InterProcessCommunication.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ VisualStudioVersion = 14.0.22823.1
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterProcessCommunication.Tests", "tests\InterProcessCommunication\InterProcessCommunication.Tests.csproj", "{C8B651EA-21B8-45B3-9617-3BA952D0F12B}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterProcessCommunication.TestConsoleApp", "tests\InterProcessCommunication\InterProcessCommunication.TestConsoleApp\InterProcessCommunication.TestConsoleApp.csproj", "{F5E941C8-AF2F-47AB-A066-FF25470CE382}"
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteExecutorConsoleApp", "..\Common\tests\System\Diagnostics\RemoteExecutorConsoleApp\RemoteExecutorConsoleApp.csproj", "{F5E941C8-AF2F-47AB-A066-FF25470CE382}"
99
EndProject
1010
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{EFFF9E5B-58A0-4D0E-891D-40EF450FE7DA}"
1111
ProjectSection(SolutionItems) = preProject

src/Scenarios/tests/InterProcessCommunication/AnonymousPipesTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Diagnostics;
45
using System.IO;
56
using System.IO.Pipes;
67
using Xunit;
78

89
namespace InterProcessCommunication.Tests
910
{
10-
public class AnonymousPipesTests : IpcTestBase
11+
public class AnonymousPipesTests : RemoteExecutorTestBase
1112
{
1213
[Fact]
1314
public void PingPong()
@@ -16,7 +17,7 @@ public void PingPong()
1617
// Then spawn another process to communicate with.
1718
using (var outbound = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
1819
using (var inbound = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
19-
using (var remote = RemoteInvoke("PingPong_OtherProcess", outbound.GetClientHandleAsString(), inbound.GetClientHandleAsString()))
20+
using (var remote = RemoteInvoke(PingPong_OtherProcess, outbound.GetClientHandleAsString(), inbound.GetClientHandleAsString()))
2021
{
2122
// Close our local copies of the handles now that we've passed them of to the other process
2223
outbound.DisposeLocalCopyOfClientHandle();
@@ -32,7 +33,7 @@ public void PingPong()
3233
}
3334
}
3435

35-
public static int PingPong_OtherProcess(string inHandle, string outHandle)
36+
private static int PingPong_OtherProcess(string inHandle, string outHandle)
3637
{
3738
// Create the clients associated with the supplied handles
3839
using (var inbound = new AnonymousPipeClientStream(PipeDirection.In, inHandle))

src/Scenarios/tests/InterProcessCommunication/EventWaitHandleTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Threading;
67
using Xunit;
78

89
namespace InterProcessCommunication.Tests
910
{
10-
public class EventWaitHandleTests : IpcTestBase
11+
public class EventWaitHandleTests : RemoteExecutorTestBase
1112
{
1213
[ActiveIssue("https://github.com/dotnet/coreclr/issues/1237", PlatformID.AnyUnix)]
1314
[Theory]
@@ -22,7 +23,7 @@ public void PingPong(EventResetMode mode)
2223
// Create the two events and the other process with which to synchronize
2324
using (var inbound = new EventWaitHandle(true, mode, inboundName))
2425
using (var outbound = new EventWaitHandle(false, mode, outboundName))
25-
using (var remote = RemoteInvoke("PingPong_OtherProcess", mode.ToString(), outboundName, inboundName))
26+
using (var remote = RemoteInvoke(PingPong_OtherProcess, mode.ToString(), outboundName, inboundName))
2627
{
2728
// Repeatedly wait for one event and then set the other
2829
for (int i = 0; i < 10; i++)
@@ -37,7 +38,7 @@ public void PingPong(EventResetMode mode)
3738
}
3839
}
3940

40-
public static int PingPong_OtherProcess(string modeName, string inboundName, string outboundName)
41+
private static int PingPong_OtherProcess(string modeName, string inboundName, string outboundName)
4142
{
4243
EventResetMode mode = (EventResetMode)Enum.Parse(typeof(EventResetMode), modeName);
4344

src/Scenarios/tests/InterProcessCommunication/InterProcessCommunication.TestConsoleApp/TestConsoleApp.cs

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)