Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RemoteExecutor: support single file and NativeAOT #11460

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/Microsoft.DotNet.RemoteExecutor/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,40 @@ namespace Microsoft.DotNet.RemoteExecutor
/// <summary>
/// Provides an entry point in a new process that will load a specified method and invoke it.
/// </summary>
internal static class Program
public static class Program
{
private static int Main(string[] args)
{
int? maybeExitCode = TryExecute(args);
if (maybeExitCode.HasValue)
{
return maybeExitCode.Value;
}

// we should not get here
Console.Error.WriteLine("Remote executor EXE started, but missing magic environmental variable: " + RemoteExecutor.REMOTE_EXECUTOR_ENVIRONMENTAL_VARIABLE);
return -1;
}

/// <summary>
/// Checks if the command line arguments are for the remote executor. If so, attempts to parse and execute the remote function.
/// </summary>
/// <remarks>
/// This entry point is intended be called by single-file test hosts. It allows one applicaiton to both hosts the tests
/// and host the remote executor.
/// This method may exit the process before returning.
/// </remarks>
/// <returns>null the arguments are not for the remote executor, otherwise the exit code for the process as a result of running the remote executor</returns>
public static int? TryExecute(string[] args)
{
if (Environment.GetEnvironmentVariable(RemoteExecutor.REMOTE_EXECUTOR_ENVIRONMENTAL_VARIABLE) is null)
{
return null;
}

// Allow the remote executor to also start more remote executors.
Environment.SetEnvironmentVariable(RemoteExecutor.REMOTE_EXECUTOR_ENVIRONMENTAL_VARIABLE, null);

// The program expects to be passed the target assembly name to load, the type
// from that assembly to find, and the method from that assembly to invoke.
// Any additional arguments are passed as strings to the method.
Expand Down
76 changes: 48 additions & 28 deletions src/Microsoft.DotNet.RemoteExecutor/src/RemoteExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Xunit;
Expand All @@ -16,6 +17,8 @@ namespace Microsoft.DotNet.RemoteExecutor
{
public static partial class RemoteExecutor
{
internal const string REMOTE_EXECUTOR_ENVIRONMENTAL_VARIABLE = "DOTNET_REMOTEEXECUTOR";

/// <summary>
/// A timeout (milliseconds) after which a wait on a remote operation should be considered a failure.
/// </summary>
Expand Down Expand Up @@ -54,37 +57,45 @@ static RemoteExecutor()

Path = typeof(RemoteExecutor).Assembly.Location;

if (IsNetCore())
if (IsSingleFile())
{
// Single file case. Assume that our entry EXE has will detect the special argument and vector into the remote executor.
HostRunner = Process.GetCurrentProcess().MainModule.FileName;
}
else
{
HostRunner = processFileName;
if (IsNetCore())
{
HostRunner = processFileName;

string hostName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet";
string hostName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet";

// Partially addressing https://github.com/dotnet/arcade/issues/6371
// We expect to run tests with dotnet. However in certain scenarios we may have a different apphost (e.g. Visual Studio testhost).
// Attempt to find and use dotnet.
if (!IOPath.GetFileName(HostRunner).Equals(hostName, StringComparison.OrdinalIgnoreCase))
{
string runtimePath = IOPath.GetDirectoryName(typeof(object).Assembly.Location);

// In case we are running the app via a runtime, dotnet.exe is located 3 folders above the runtime. Example:
// runtime -> C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.6\
// dotnet.exe -> C:\Program Files\dotnet\shared\dotnet.exe
// This should also work on Unix and locally built runtime/testhost.
string directory = GetDirectoryName(GetDirectoryName(GetDirectoryName(runtimePath)));
if (directory != string.Empty)
// Partially addressing https://github.com/dotnet/arcade/issues/6371
// We expect to run tests with dotnet. However in certain scenarios we may have a different apphost (e.g. Visual Studio testhost).
// Attempt to find and use dotnet.
if (!IOPath.GetFileName(HostRunner).Equals(hostName, StringComparison.OrdinalIgnoreCase))
{
string dotnetExe = IOPath.Combine(directory, hostName);
if (File.Exists(dotnetExe))
string runtimePath = IOPath.GetDirectoryName(typeof(object).Assembly.Location);

// In case we are running the app via a runtime, dotnet.exe is located 3 folders above the runtime. Example:
// runtime -> C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.6\
// dotnet.exe -> C:\Program Files\dotnet\shared\dotnet.exe
// This should also work on Unix and locally built runtime/testhost.
string directory = GetDirectoryName(GetDirectoryName(GetDirectoryName(runtimePath)));
if (directory != string.Empty)
{
HostRunner = dotnetExe;
string dotnetExe = IOPath.Combine(directory, hostName);
if (File.Exists(dotnetExe))
{
HostRunner = dotnetExe;
}
}
}
}
}
else if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase))
{
HostRunner = Path;
else if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase))
{
HostRunner = Path;
}
}

HostRunnerName = IOPath.GetFileName(HostRunner);
Expand All @@ -95,6 +106,8 @@ static RemoteExecutor()
private static bool IsNetCore() =>
Environment.Version.Major >= 5 || RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase);

private static bool IsSingleFile() => string.IsNullOrEmpty(Path);

/// <summary>Returns true if the RemoteExecutor works on the current platform, otherwise false.</summary>
public static bool IsSupported { get; } =
!RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS")) &&
Expand All @@ -103,8 +116,6 @@ private static bool IsNetCore() =>
!RuntimeInformation.IsOSPlatform(OSPlatform.Create("MACCATALYST")) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Create("WATCHOS")) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")) &&
// The current RemoteExecutor design is not compatible with single file
!string.IsNullOrEmpty(typeof(RemoteExecutor).Assembly.Location) &&
Environment.GetEnvironmentVariable("DOTNET_REMOTEEXECUTOR_SUPPORTED") != "0";

/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary>
Expand Down Expand Up @@ -405,6 +416,13 @@ private static RemoteInvokeHandle Invoke(MethodInfo method, string[] args,
throw new PlatformNotSupportedException("RemoteExecutor is not supported on this platform.");
}

// If we were started as the remote executor but did not actually enter the remote executor entrypoint,
// throw to prevent infinitely spawning processes.
if (Environment.GetEnvironmentVariable(REMOTE_EXECUTOR_ENVIRONMENTAL_VARIABLE) is not null)
{
throw new InvalidOperationException("Magic environmental variable to start the remote executor is set! Did your single-file host forget to call Microsoft.DotNet.RemoteExecutor.Program.TryExecute() ?");
}

// 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)
Expand All @@ -421,6 +439,7 @@ private static RemoteInvokeHandle Invoke(MethodInfo method, string[] args,
// Start the other process and return a wrapper for it to handle its lifetime and exit checking.
ProcessStartInfo psi = options.StartInfo;
psi.UseShellExecute = false;
psi.Environment.Add(REMOTE_EXECUTOR_ENVIRONMENTAL_VARIABLE, "1");

if (!options.EnableProfiling)
{
Expand Down Expand Up @@ -462,19 +481,20 @@ private static RemoteInvokeHandle Invoke(MethodInfo method, string[] args,
private static string GetConsoleAppArgs(RemoteInvokeOptions options, out IEnumerable<IDisposable> toDispose)
{
bool isNetCore = IsNetCore();
if (options.RuntimeConfigurationOptions?.Any() == true&& !isNetCore)
bool isSingleFile = IsSingleFile();
if (options.RuntimeConfigurationOptions?.Any() == true && (!isNetCore || isSingleFile))
{
throw new InvalidOperationException("RuntimeConfigurationOptions are only supported on .NET Core");
}

if (!isNetCore)
if (!isNetCore || isSingleFile)
{
toDispose = null;
return string.Empty;
}

string args = "exec";

string runtimeConfigPath = GetRuntimeConfigPath(options, out toDispose);
if (runtimeConfigPath != null)
{
Expand Down