diff --git a/docs/design/features/host-runtime-information.md b/docs/design/features/host-runtime-information.md index 7140bb0f658e39..5501d8c0469813 100644 --- a/docs/design/features/host-runtime-information.md +++ b/docs/design/features/host-runtime-information.md @@ -40,6 +40,10 @@ Directory containing the application. This is used for [`AppContext.BaseDirector [Runtime identifier](https://learn.microsoft.com/dotnet/core/rid-catalog) for the application. This is used for [`RuntimeInformation.RuntimeIdentifier`](https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.runtimeinformation.runtimeidentifier). +`ARGV0` + +The name used to invoke an application host, corresponding to the native process's `argv[0]`. The apphost provides this property for the first element returned by [`Environment.GetCommandLineArgs()`](https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs). Muxer-style hosts, including `dotnet` and `corerun`, do not provide this property, preserving the managed application path as the first element. + ### Deps files `APP_CONTEXT_DEPS_FILES` diff --git a/src/coreclr/vm/corhost.cpp b/src/coreclr/vm/corhost.cpp index e7c525050bd8e0..3dfab73f92b239 100644 --- a/src/coreclr/vm/corhost.cpp +++ b/src/coreclr/vm/corhost.cpp @@ -37,6 +37,7 @@ #endif // !TARGET_UNIX #include "nativelibrary.h" +#include "hostinformation.h" #ifndef DACCESS_COMPILE @@ -217,17 +218,11 @@ HRESULT CorHost2::ExecuteApplication(LPCWSTR pwzAppFullName, } /* - * This method processes the arguments sent to the host which are then used - * to invoke the main method. - * Note - - * [0] - points to the assemblyName that has been sent by the host. - * The rest are the arguments sent to the assembly. - * Also note, this might not always return the exact same identity as the cmdLine - * used to invoke the method. - * - * For example :- - * ActualCmdLine - Foo arg1 arg2. - * (Host1) - Full_path_to_Foo arg1 arg2 + * This method constructs the array returned by Environment.GetCommandLineArgs(). + * The first element is passed separately from argv as exePath and uses the native + * invocation name when provided by the host. Otherwise, the assembly or bundle path + * is used. The remaining elements come from argv, which contains the arguments to the + * main method. */ static PTRARRAYREF SetCommandLineArgs(PCWSTR pwzAssemblyPath, int argc, PCWSTR* argv) { @@ -242,7 +237,10 @@ static PTRARRAYREF SetCommandLineArgs(PCWSTR pwzAssemblyPath, int argc, PCWSTR* // Record the command line. SaveManagedCommandLine(pwzAssemblyPath, argc, argv); - PCWSTR exePath = Bundle::AppIsBundle() ? static_cast(Bundle::AppBundle->Path()) : pwzAssemblyPath; + StackSString invocationName; + PCWSTR exePath = HostInformation::GetProperty(HOST_PROPERTY_ARGV0, invocationName) + ? invocationName.GetUnicode() + : (Bundle::AppIsBundle() ? static_cast(Bundle::AppBundle->Path()) : pwzAssemblyPath); PTRARRAYREF result = NULL; GCPROTECT_BEGIN(result); diff --git a/src/installer/tests/Assets/Projects/HelloWorld/Program.cs b/src/installer/tests/Assets/Projects/HelloWorld/Program.cs index 7fd61b604c4c04..d13ef9c5a7e745 100644 --- a/src/installer/tests/Assets/Projects/HelloWorld/Program.cs +++ b/src/installer/tests/Assets/Projects/HelloWorld/Program.cs @@ -52,6 +52,13 @@ public static void Main(string[] args) Console.WriteLine($"AppContext.GetData({propertyName}) = {propertyValue}"); } break; + case "print_command_line_args": + string[] commandLineArgs = Environment.GetCommandLineArgs(); + for (int i = 0; i < commandLineArgs.Length; i++) + { + Console.WriteLine($"Environment.GetCommandLineArgs()[{i}] = {commandLineArgs[i]}"); + } + break; case "throw_exception": // Disable core dumps - test is intentionally crashing Utilities.CoreDump.Disable(); diff --git a/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs b/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs index f721ffc00916e2..b11fb72c6fc28f 100644 --- a/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs +++ b/src/installer/tests/HostActivation.Tests/FrameworkDependentAppLaunch.cs @@ -29,19 +29,23 @@ public void Muxer_Default() var dotnet = HostTestContext.BuiltDotNet; var appDll = sharedTestState.App.AppDll; - dotnet.Exec(appDll) + dotnet.Exec(appDll, "print_command_line_args") .CaptureStdErr() .CaptureStdOut() .Execute() .Should().Pass() - .And.HaveStdOutContaining("Hello World"); + .And.HaveStdOutContaining("Hello World") + .And.HaveStdOutContaining($"Environment.GetCommandLineArgs()[0] = {appDll}") + .And.HaveStdOutContaining("Environment.GetCommandLineArgs()[1] = print_command_line_args"); - dotnet.Exec("exec", appDll) + dotnet.Exec("exec", appDll, "print_command_line_args") .CaptureStdErr() .CaptureStdOut() .Execute() .Should().Pass() - .And.HaveStdOutContaining("Hello World"); + .And.HaveStdOutContaining("Hello World") + .And.HaveStdOutContaining($"Environment.GetCommandLineArgs()[0] = {appDll}") + .And.HaveStdOutContaining("Environment.GetCommandLineArgs()[1] = print_command_line_args"); } [Fact] diff --git a/src/installer/tests/HostActivation.Tests/SymbolicLinks.cs b/src/installer/tests/HostActivation.Tests/SymbolicLinks.cs index a80d9c9e014e90..0740a312e391b0 100644 --- a/src/installer/tests/HostActivation.Tests/SymbolicLinks.cs +++ b/src/installer/tests/HostActivation.Tests/SymbolicLinks.cs @@ -43,7 +43,8 @@ public void Symlink_all_files_fx(string symlinkRelativePath) symlinks.Add(new SymLink(symlinkPath, file)); } - var result = Command.Create(Path.Combine(testDir.Location, symlinkRelativePath, Path.GetFileName(sharedTestState.FrameworkDependentApp.AppExe))) + string appHostPath = Path.Combine(testDir.Location, symlinkRelativePath, Path.GetFileName(sharedTestState.FrameworkDependentApp.AppExe)); + var result = Command.Create(appHostPath, "print_command_line_args") .CaptureStdErr() .CaptureStdOut() .DotNetRoot(HostTestContext.BuiltDotNet.BinPath) @@ -54,7 +55,9 @@ public void Symlink_all_files_fx(string symlinkRelativePath) // * Unix: The apphost will look next to the resolved apphost for the app dll and find the real thing result .Should().Pass() - .And.HaveStdOutContaining("Hello World"); + .And.HaveStdOutContaining("Hello World") + .And.HaveStdOutContaining($"Environment.GetCommandLineArgs()[0] = {appHostPath}") + .And.HaveStdOutContaining("Environment.GetCommandLineArgs()[1] = print_command_line_args"); } finally { diff --git a/src/native/corehost/host_runtime_contract.h b/src/native/corehost/host_runtime_contract.h index 9bfea43f168c44..8bba040a6d5a03 100644 --- a/src/native/corehost/host_runtime_contract.h +++ b/src/native/corehost/host_runtime_contract.h @@ -19,6 +19,7 @@ #define HOST_PROPERTY_BUNDLE_PROBE "BUNDLE_PROBE" #define HOST_PROPERTY_BUNDLE_EXTRACTION_PATH "BUNDLE_EXTRACTION_PATH" #define HOST_PROPERTY_ENTRY_ASSEMBLY_NAME "ENTRY_ASSEMBLY_NAME" +#define HOST_PROPERTY_ARGV0 "ARGV0" #define HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES "NATIVE_DLL_SEARCH_DIRECTORIES" #define HOST_PROPERTY_PINVOKE_OVERRIDE "PINVOKE_OVERRIDE" #define HOST_PROPERTY_PLATFORM_RESOURCE_ROOTS "PLATFORM_RESOURCE_ROOTS" diff --git a/src/native/corehost/hostpolicy/args.cpp b/src/native/corehost/hostpolicy/args.cpp index 4c64ebde2a1d37..7582542ec49d05 100644 --- a/src/native/corehost/hostpolicy/args.cpp +++ b/src/native/corehost/hostpolicy/args.cpp @@ -23,6 +23,9 @@ bool parse_arguments( pal::string_t managed_application_path; if (init.host_mode == host_mode_t::apphost) { + assert(argc > 0 && argv != nullptr); + args.invocation_name = argv[0]; + // Find the managed app in the same directory managed_application_path = init.host_info.app_path; diff --git a/src/native/corehost/hostpolicy/args.h b/src/native/corehost/hostpolicy/args.h index e13edd4a4f7ca8..0539e7e3d1edc4 100644 --- a/src/native/corehost/hostpolicy/args.h +++ b/src/native/corehost/hostpolicy/args.h @@ -126,6 +126,7 @@ struct arguments_t host_mode_t host_mode; pal::string_t app_root; pal::string_t deps_path; + pal::string_t invocation_name; pal::string_t managed_application; int app_argc; diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index d1285d5221b81c..3bf16e9bf9c322 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -122,6 +122,14 @@ namespace return pal::pal_utf8string(get_filename_without_ext(context->application), value_buffer, value_buffer_size); } + if (::strcmp(key, HOST_PROPERTY_ARGV0) == 0) + { + if (context->invocation_name.empty()) + return -1; + + return pal::pal_utf8string(context->invocation_name, value_buffer, value_buffer_size); + } + if (::strcmp(key, HOST_PROPERTY_BUNDLE_EXTRACTION_PATH) == 0) { if (!bundle::info_t::is_single_file_bundle()) @@ -167,6 +175,7 @@ int hostpolicy_context_t::initialize(const hostpolicy_init_t &hostpolicy_init, c application = args.managed_application; host_mode = hostpolicy_init.host_mode; host_path = hostpolicy_init.host_info.host_path; + invocation_name = args.invocation_name; breadcrumbs_enabled = enable_breadcrumbs; deps_json_t::rid_resolution_options_t rid_resolution_options diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.h b/src/native/corehost/hostpolicy/hostpolicy_context.h index a7d8faf6f7a6f3..e815b59b038ae8 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.h +++ b/src/native/corehost/hostpolicy/hostpolicy_context.h @@ -20,6 +20,7 @@ struct hostpolicy_context_t pal::string_t clr_path; host_mode_t host_mode; pal::string_t host_path; + pal::string_t invocation_name; bool breadcrumbs_enabled; mutable std::unordered_set breadcrumbs;