Fix Environment.GetCommandLineArgs()[0] to report the host invocation name - #131671
Conversation
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR changes Environment.GetCommandLineArgs()[0] on CoreCLR to reflect the native host invocation name (argv[0]) when available, instead of the managed assembly (or single-file bundle path), by flowing the invocation name from the host to the runtime via a new host runtime property.
Changes:
- Add a new well-known host runtime property (
HOST_INVOCATION_NAME) and plumb it through hostpolicy to CoreCLR. - Update CoreCLR command-line initialization to prefer
HOST_INVOCATION_NAMEfor element 0 (with existing fallbacks). - Extend HostActivation symbolic-link testing (and the HelloWorld test asset) to validate the new behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/native/corehost/hostpolicy/hostpolicy_context.h | Stores host invocation name in the hostpolicy context. |
| src/native/corehost/hostpolicy/hostpolicy_context.cpp | Serves HOST_INVOCATION_NAME via get_runtime_property and initializes it from parsed args. |
| src/native/corehost/hostpolicy/args.h | Adds invocation_name to the parsed arguments model. |
| src/native/corehost/hostpolicy/args.cpp | Captures argv[0] as the invocation name before host-mode argument parsing. |
| src/native/corehost/host_runtime_contract.h | Defines the HOST_PROPERTY_INVOCATION_NAME key. |
| src/coreclr/vm/corhost.cpp | Uses HostInformation::GetProperty(HOST_INVOCATION_NAME, …) to set Environment.GetCommandLineArgs()[0]. |
| src/coreclr/hosts/corerun/corerun.cpp | Provides HOST_INVOCATION_NAME from corerun and captures its argv[0]. |
| src/installer/tests/HostActivation.Tests/SymbolicLinks.cs | Asserts that [0] preserves the apphost invocation path and [1] remains the first managed arg. |
| src/installer/tests/Assets/Projects/HelloWorld/Program.cs | Adds a test hook to print Environment.GetCommandLineArgs() for verification. |
| docs/design/features/host-runtime-information.md | Documents the new HOST_INVOCATION_NAME property. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/native/corehost/hostpolicy/hostpolicy_context.cpp:131
HOST_INVOCATION_NAMEis surfaced unconditionally for non-libhost modes. Ifargv[0]is empty (which is legal on some platforms/launchers), this would make the property appear present and causeEnvironment.GetCommandLineArgs()[0]to become an empty string rather than falling back to the assembly/bundle path. Consider treating an emptyinvocation_nameas "not found" by returning -1 in that case as well.
if (::strcmp(key, HOST_PROPERTY_INVOCATION_NAME) == 0)
{
if (context->host_mode == host_mode_t::libhost)
return -1;
return pal::pal_utf8string(context->invocation_name, value_buffer, value_buffer_size);
}
src/coreclr/hosts/corerun/corerun.cpp:295
- If
corerunis launched in an unusual way whereargv[0]is the empty string, this implementation would reportHOST_INVOCATION_NAMEas present (length 1) and CoreCLR would then use an empty string forEnvironment.GetCommandLineArgs()[0]. Consider returning -1 wheninvocation_nameis empty so the runtime can fall back to the existing bundle/assembly behavior.
if (::strcmp(key, HOST_PROPERTY_INVOCATION_NAME) == 0)
{
pal::string_utf8_t value_utf8 = pal::convert_to_utf8(config->invocation_name.c_str());
size_t len = value_utf8.size() + 1;
if (value_buffer_size < len)
return len;
::strncpy(value_buffer, value_utf8.c_str(), len - 1);
value_buffer[len - 1] = '\0';
return len;
}
I do not think that this is the behavior we want. IIRC, the desired behavior we have discussed is captured in this table #101837 (comment) |
|
Fixed via 6ef8b95 Note This comment was generated with AI assistance. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
docs/design/features/host-runtime-information.md:45
- The new property description is a bit ambiguous about what happens when running via the
dotnetmuxer. Since the muxer doesn’t provideHOST_INVOCATION_NAME(per this doc and the updated tests), it may help to add an explicit example so readers don’t infer thatdotnet app.dll argwill makeGetCommandLineArgs()[0]becomedotnet.
`HOST_INVOCATION_NAME`
The name used to invoke an application host, corresponding to the native process's `argv[0]`. The apphost and `corerun` hosts provide this property for the first element returned by [`Environment.GetCommandLineArgs()`](https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs). The `dotnet` muxer does not provide this property, preserving the managed application path as the first element.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/native/corehost/hostpolicy/args.cpp:24
- The new
argc > 0guard suggests apphost could be invoked withoutargv[0], but the rest of this function unconditionally assumes apphost has at least one argument (&argv[1],argc - 1). To avoid a misleading partial guard, either validate/return false for invalid inputs, or assert the invariant and always captureargv[0]in apphost mode.
if (init.host_mode == host_mode_t::apphost && argc > 0)
args.invocation_name = argv[0];
|
This looks good to me, modulo the naming nit. @elinor-fung ? |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
docs/design/features/host-runtime-information.md:46
- The new
ARGV0property entry doesn’t indicate which .NET version introduced it. Elsewhere in this doc, newly added properties include an explicit “Added in .NET X” note (for exampleBUNDLE_EXTRACTION_PATH). Without a version marker, readers may incorrectly assumeARGV0has existed since the original .NET 8 host runtime contract.
`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.
dd32cfa to
52bc5aa
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/native/corehost/hostpolicy/args.cpp:28
- The new apphost branch asserts argc/argv are valid, but in release builds the assert is compiled out and argv[0] will be dereferenced unconditionally. If a host ever calls into hostpolicy with an unexpected argc/argv (or argv[0] null), this becomes an immediate crash. Prefer a defensive runtime check (and fail parsing) instead of relying on assert-only validation.
if (init.host_mode == host_mode_t::apphost)
{
assert(argc > 0 && argv != nullptr);
args.invocation_name = argv[0];
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/native/corehost/hostpolicy/hostpolicy_context.cpp:131
HOST_PROPERTY_ARGV0is intended to reflect the nativeargv[0], but the current check treats an empty invocation name as “property not provided” and falls back to the assembly/bundle path. An emptyargv[0]is a valid (if rare) scenario on Unix, so this conflates “not provided” with “provided but empty” and can produce an incorrect fallback value. Consider usinghost_modeto decide whether the property is supported, and return the (possibly empty) string when running under apphost.
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);
}
|
/ba-g #131805 |
… name (dotnet#131671) Back on the topic to try to fix dotnet#101837. I have used ChatGPT 5.6 Sol with guidance from the issue, my previous hacky PR and safest path analysis. ## Summary `Environment.GetCommandLineArgs()[0]` currently reports the managed assembly path, or the resolved bundle path for single-file applications, rather than the name used to invoke the native host. This is especially visible when an apphost is launched through a symbolic link. For apphost launches, this change preserves the native host's original `argv[0]` and uses it only for element zero of the array returned by `Environment.GetCommandLineArgs()`. The `dotnet` muxer continues to use the managed application path for element zero, and all remaining elements retain their existing behavior. ## Implementation - Adds the well-known `ARGV0` host runtime property. - Captures the apphost `argv[0]` in hostpolicy before host-mode argument parsing. - Queries the property from CoreCLR when initializing the managed command-line array. - Retains the existing assembly/bundle-path fallback when a host does not provide the property. - Documents the new host runtime property. The complete native argument vector is intentionally not preserved or exposed. The resulting behavior is: | Command line | `Environment.GetCommandLineArgs()` | | --- | --- | | `dotnet.exe foo.dll 1 2 3` | `foo.dll`, `1`, `2`, `3` | | `foo 1 2 3` | `foo`, `1`, `2`, `3` | | `../bar/special_foo.exe 1 2 3`, with `special_foo.exe` symlinked to `foo.exe` | `../bar/special_foo.exe`, `1`, `2`, `3` | This does not change: - Managed `Main` arguments - `Environment.ProcessPath` - Bundle probing or resolution - Native hosting API signatures - The meaning of `exePath` - Behavior for hosts that do not implement the new property ## Testing - Built Checked CoreCLR. - Built the libraries and Checked host components/host tests. - Ran the focused muxer command-line test and all 19 `HostActivation.Tests.SymbolicLinks` tests successfully. - Manually verified the resulting command-line arrays with both `dotnet` and `corerun`. The symbolic-link test verifies that element zero retains the original apphost invocation path while element one remains the existing managed argument. > [!NOTE] > This PR description was drafted with AI assistance.
|
Should this be tagged as a breaking change (and get a breaking-change doc)? It changed observable behavior for apphost-launched apps in .NET 11. We hit this in Aspire after retargeting our CLI to net11.0. Our tests run through the Microsoft.Testing.Platform apphost, and System.CommandLine derives the root command name from
As a result, help-output snapshots started failing only on Linux/macOS (microsoft/aspire#20436). Anything else that uses I've opened a related issue on System.CommandLine that broke because of the change: dotnet/command-line-api#2850 |
- Pack the RID-specific Aspire.Cli dotnet tool as self-contained so the SDK lays it out as tools/any/<rid>/ instead of tools/net11.0/<rid>/. Older SDKs (e.g. .NET 10) can't install a tool whose only tools/<tfm> folder is newer than themselves, which broke DotnetToolSmokeTests. - Normalize the root command name in help snapshots. .NET 11 reports the apphost invocation name in Environment.GetCommandLineArgs()[0] (dotnet/runtime#131671), and System.CommandLine's Path.GetFileNameWithoutExtension turns the extension-less 'Aspire.Cli.Tests' apphost into 'Aspire.Cli' on Linux/macOS (dotnet/command-line-api#2850). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
## Description This retargets the Aspire CLI from `net10.0` to `net11.0`, with only the changes needed to build, publish Native AOT, package, and pass tests. Follow-up PRs will start using new .NET 11 features. ### User-facing impact - **Nothing changes in behavior for users of the native `aspire` binary.** - **The `Aspire.Cli` dotnet tool package layout changes.** The platform-specific packages (`Aspire.Cli.<rid>`) now put the binary under `tools/any/<rid>/` instead of `tools/net10.0/<rid>/`. If they used `tools/net11.0/<rid>/`, SDKs older than .NET 11 would fail `dotnet tool install -g Aspire.Cli` with "Settings file 'DotnetToolSettings.xml' was not found in the package". The pointer package (`Aspire.Cli`) was already `tools/any/any/`. I verified that a locally built package installs and runs with both the .NET 10.0.401 and .NET 11 SDKs. ### Changes **Retarget** - `Aspire.Cli`, `Aspire.Cli.Tests`, `Aspire.Cli.Benchmarks`, and `eng/clipack` (`Common.projitems`, `Aspire.Cli.NativeSymbols.proj`) now target `net11.0`. - Scripts, CI, the devcontainer, extension scripts, and docs that point at `artifacts/bin/Aspire.Cli/<config>/net10.0` now use `net11.0`. **Native AOT trimming** - The `TrimmerSingleWarn` configuration for `Microsoft.CSharp` / `System.Linq.Expressions` moved to `ResolvedFileToPublish` after `_PrepareTrimConfiguration`. In .NET 11, ILC reads trim metadata from there instead of `ManagedAssemblyToLink`, and the old location caused IL3050 errors. **Compiler** - Removed `using System.Net.Http.Json` from 5 files. It's an implicit using on `net11.0`, so the explicit using triggered IDE0005. **dotnet tool packaging** (`eng/clipack/Common.projitems`) - The RID-specific tool package is packed with `SelfContained=true`, so the SDK uses the TFM-agnostic `tools/any/<rid>/` layout. `PublishAot=true` alone doesn't imply this here. The SDK only infers `SelfContained` from `PublishAot` when `_IsPacking` is set by `dotnet pack`, and clipack packs through the MSBuild task. - `eng/scripts/verify-cli-tool-nupkg.ps1` now requires `tools/any/<rid>/` in the RID package and `tools/any/any/` in the pointer package. Packaging runs with the repo's newer SDK, so this check is what catches a regression back to a `tools/<tfm>/` layout. - The native symbols package now uses `tools/any/<rid>/` too, so it keeps mirroring the tool package layout. **Tool-store detection** (`DotNetToolDetection`) - The installed-tool store is now recognized only under `tools/any/<rid>/`, which is what the self-contained RID package installs. `tools/net10.0/` and `tools/net11.0/` paths are no longer treated as dotnet-tool installs, and the tests cover this. **Tests** - Help snapshots now scrub the root command name to `aspire` (`ScrubRootCommandName()`). Starting with .NET 11, `Environment.GetCommandLineArgs()[0]` is the apphost invocation name rather than the `.dll` (dotnet/runtime#131671). System.CommandLine's `Path.GetFileNameWithoutExtension` then turns the extension-less `Aspire.Cli.Tests` apphost into `Aspire.Cli` on Linux/macOS (reported as dotnet/command-line-api#2850). ### Validation - The `Aspire.Cli.Tests` suite passes locally. The two failures I saw also fail on `main` in my environment, or are flaky. - `dotnet publish -r win-x64` of the CLI succeeds with no trim/AOT warnings, and the native `aspire --version` runs. - The win-x64 tool packages build locally with the `tools/any/` layout and pass `verify-cli-tool-nupkg.ps1`. Copies rewritten to `tools/net11.0/` are rejected. - I installed the tool package with the .NET 10.0.401 and .NET 11 SDKs and ran it with both. Fixes # (issue) ## Checklist - Is this feature complete? - [ ] Yes. Ready to ship. - [x] No. Follow-up changes expected. - Are you including unit tests for the changes and scenario tests if relevant? - [x] Yes - [ ] No - Did you add public API? - [ ] Yes - If yes, did you have an API Review for it? - [ ] Yes - [ ] No - Did you add `<remarks />` and `<code />` elements on your triple slash comments? - [ ] Yes - [ ] No - [x] No - Does the change make any security assumptions or guarantees? - [ ] Yes - If yes, have you done a threat model and had a security review? - [ ] Yes - [ ] No - [x] No --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Hello .NET runtime folks,☺️
Back on the topic to try to fix #101837. I have used ChatGPT 5.6 Sol with guidance from the issue, my previous hacky PR and safest path analysis.
Summary
Environment.GetCommandLineArgs()[0]currently reports the managed assembly path, or the resolved bundle path for single-file applications, rather than the name used to invoke the native host. This is especially visible when an apphost is launched through a symbolic link.For apphost launches, this change preserves the native host's original
argv[0]and uses it only for element zero of the array returned byEnvironment.GetCommandLineArgs(). Thedotnetmuxer continues to use the managed application path for element zero, and all remaining elements retain their existing behavior.Implementation
ARGV0host runtime property.argv[0]in hostpolicy before host-mode argument parsing.The complete native argument vector is intentionally not preserved or exposed. The resulting behavior is:
Environment.GetCommandLineArgs()dotnet.exe foo.dll 1 2 3foo.dll,1,2,3foo 1 2 3foo,1,2,3../bar/special_foo.exe 1 2 3, withspecial_foo.exesymlinked tofoo.exe../bar/special_foo.exe,1,2,3This does not change:
MainargumentsEnvironment.ProcessPathexePathTesting
HostActivation.Tests.SymbolicLinkstests successfully.dotnetandcorerun.The symbolic-link test verifies that element zero retains the original apphost invocation path while element one remains the existing managed argument.
Note
This PR description was drafted with AI assistance.