Files
Marco Rossignoli ff879e820b Check nested types for exclude filter (#694)
Check nested types for exclude filter
2020-01-20 15:13:48 +01:00

281 lines
11 KiB
C#

// This code is an adapted porting of CoreFx RemoteExecutor
// https://github.com/dotnet/arcade/tree/master/src/Microsoft.DotNet.RemoteExecutor/src
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Xunit.Sdk;
namespace Coverlet.Tests.RemoteExecutor
{
public static partial class RemoteExecutor
{
// If you want to debug instrumentation you could
// 1) Add a Debug.Launch() inside lambda and attach(slow)
// 2) Temporary pass true to invoke local, it will throw because code try to replace locked files,
// but if you temporary "comment" offensive code(RestoreOriginalModule/s) you can debug all procedure and it's very very very useful
public static IRemoteInvokeHandle Invoke(Func<string, Task<int>> method, string arg = "", bool invokeInProcess = false)
{
if (invokeInProcess)
{
return new LocalInvoker(method.Invoke(arg).GetAwaiter().GetResult());
}
else
{
return Invoke(GetMethodInfo(method), new[] { arg });
}
}
private static IRemoteInvokeHandle Invoke(MethodInfo method, string[] args, bool pasteArguments = true)
{
Type t = method.DeclaringType;
Assembly a = t.GetTypeInfo().Assembly;
string exceptionFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
string resultFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
string metadataArgs = PasteArguments.Paste(new string[] { a.Location, t.FullName, method.Name, exceptionFile }, pasteFirstArgumentUsingArgV0Rules: false);
string hostRunner = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
string passedArgs = pasteArguments ? PasteArguments.Paste(args, pasteFirstArgumentUsingArgV0Rules: false) : string.Join(" ", args);
string testConsoleAppArgs = Assembly.GetExecutingAssembly().Location + " " + metadataArgs + " " + passedArgs;
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = hostRunner;
psi.Arguments = testConsoleAppArgs;
psi.UseShellExecute = false;
return new RemoteInvokeHandle(Process.Start(psi), exceptionFile, resultFile);
}
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(Invoke)}: {fi.Name}"));
}
return d.GetMethodInfo();
}
}
public interface IRemoteInvokeHandle : IDisposable
{
int ExitCode { get; }
}
public struct LocalInvoker : IRemoteInvokeHandle
{
public readonly int ExitCode { get; }
public LocalInvoker(int exitCode) => ExitCode = exitCode;
public void Dispose()
{
if (ExitCode != 0)
{
throw new RemoteExecutionException($"Result '{ExitCode}'");
}
}
}
public sealed class RemoteInvokeHandle : IRemoteInvokeHandle
{
const int FailWaitTimeoutMilliseconds = 60 * 1000;
private readonly Process _process;
private readonly string _exceptionFile;
private readonly string _resultFile;
public RemoteInvokeHandle(Process process, string exceptionFile, string resultFile) => (_process, _exceptionFile, _resultFile) = (process, exceptionFile, resultFile);
public int ExitCode
{
get
{
_process.WaitForExit(FailWaitTimeoutMilliseconds);
return _process.ExitCode;
}
}
public void Dispose()
{
Assert.True(_process.WaitForExit(FailWaitTimeoutMilliseconds), $"Timed out after {FailWaitTimeoutMilliseconds}ms waiting for remote process {_process.Id}");
FileInfo exceptionFileInfo = new FileInfo(_exceptionFile);
if (exceptionFileInfo.Exists && exceptionFileInfo.Length != 0)
{
string exception = File.ReadAllText(_exceptionFile);
File.Delete(_exceptionFile);
throw new RemoteExecutionException(exception);
}
Assert.True(0 == _process.ExitCode, $"Exit code {_process.ExitCode}");
}
}
public sealed class RemoteExecutionException : XunitException
{
public RemoteExecutionException(string stackTrace)
: base("Remote process failed with an unhandled exception.", stackTrace)
{
}
}
internal static class PasteArguments
{
private const char Quote = '\"';
private const char Backslash = '\\';
/// <summary>
/// Repastes a set of arguments into a linear string that parses back into the originals under pre- or post-2008 VC parsing rules.
/// The rules for parsing the executable name (argv[0]) are special, so you must indicate whether the first argument actually is argv[0].
/// </summary>
public static string Paste(IEnumerable<string> arguments, bool pasteFirstArgumentUsingArgV0Rules)
{
var stringBuilder = new StringBuilder();
foreach (string argument in arguments)
{
if (pasteFirstArgumentUsingArgV0Rules && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
pasteFirstArgumentUsingArgV0Rules = false;
// Special rules for argv[0]
// - Backslash is a normal character.
// - Quotes used to include whitespace characters.
// - Parsing ends at first whitespace outside quoted region.
// - No way to get a literal quote past the parser.
bool hasWhitespace = false;
foreach (char c in argument)
{
if (c == Quote)
{
throw new ApplicationException("The argv[0] argument cannot include a double quote.");
}
if (char.IsWhiteSpace(c))
{
hasWhitespace = true;
}
}
if (argument.Length == 0 || hasWhitespace)
{
stringBuilder.Append(Quote);
stringBuilder.Append(argument);
stringBuilder.Append(Quote);
}
else
{
stringBuilder.Append(argument);
}
}
else
{
AppendArgument(stringBuilder, argument);
}
}
return stringBuilder.ToString();
}
public static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length != 0)
{
stringBuilder.Append(' ');
}
// Parsing rules for non-argv[0] arguments:
// - Backslash is a normal character except followed by a quote.
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
// - Parsing stops at first whitespace outside of quoted region.
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
{
// Simple case - no quoting or changes needed.
stringBuilder.Append(argument);
}
else
{
stringBuilder.Append(Quote);
int idx = 0;
while (idx < argument.Length)
{
char c = argument[idx++];
if (c == Backslash)
{
int numBackSlash = 1;
while (idx < argument.Length && argument[idx] == Backslash)
{
idx++;
numBackSlash++;
}
if (idx == argument.Length)
{
// We'll emit an end quote after this so must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2);
}
else if (argument[idx] == Quote)
{
// Backslashes will be followed by a quote. Must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
stringBuilder.Append(Quote);
idx++;
}
else
{
// Backslash will not be followed by a quote, so emit as normal characters.
stringBuilder.Append(Backslash, numBackSlash);
}
continue;
}
if (c == Quote)
{
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
// by another quote (which parses differently pre-2008 vs. post-2008.)
stringBuilder.Append(Backslash);
stringBuilder.Append(Quote);
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append(Quote);
}
}
private static bool ContainsNoWhitespaceOrQuotes(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsWhiteSpace(c) || c == Quote)
{
return false;
}
}
return true;
}
}
}