The path of the current process executable is often needed for logging or to find more files next to the current process executable.
This API is needed more than before now that we support single-file publishing and assemblies do not have physical file paths anymore. See https://github.com/dotnet/designs/blob/master/accepted/2020/form-factors.md#single-file for details.
We have internal APIs to get current process executable path in this repo that return current executable path, e.g. here: https://github.com/dotnet/runtime/search?q=GetExePath&unscoped_q=GetExePath
namespace System
{
public partial class Environment
{
// Returns the path to the file that launched the process. For framework dependent apps
// this will return the path to dotnet.exe. For environment where this concept doesn't
// exist, for example WebAssembly, the method will return null.
public string? ProcessPath => (pseudo) Process.GetCurrentProcess().MainModule.FileName;
// Returns the directory path of the application.
public string? AppBaseDirectory { get; }
}
}
``` C#
Console.WriteLine(Environment.ProcessExecutablePath);
string logFilePath = Path.Combine(Environment.AppBaseDirectory, "logfile.txt");
```
Examples of equivalent APIs in other environments:
os.Executable
in https://golang.org/pkg/os/#ExecutableThe current cross-platform way to get full path of current process is Process.GetCurrentProcess().MainModule.FileName
. This workaround is very inefficient because of it does a lot more than what is required to get the current process path. More discussion on this issue is in https://github.com/dotnet/runtime/issues/13051 .
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.
I usually do it via Environment.GetCommandLineArgs()[0]
but that's not pretty/intuitive. I like the proposal!
(also tagging IO, b/c it seems relevant)
Environment.GetCommandLineArgs()[0]
Environment.ProcessExecutablePath
would return same path as Process.GetCurrentProcess().MainModule.FileName
.
Environment.ProcessExecutablePath
would not always return same path as Environment.GetCommandLineArgs()[0]
. GetCommandLineArgs[0]
is virtual arg0.
For example, if you run dotnet myprogram.dll
, Environment.GetCommandLineArgs()[0]
is myprogram
path. Environment.ProcessExecutablePath
would be dotnet
path.
In what environments would it return null?
Wasm?
Environment.ProcessExecutablePath
would not always return same path asEnvironment.GetCommandLineArgs()[0]
.GetCommandLineArgs[0]
is virtual arg0.For example, if you run
dotnet myprogram.dll
,Environment.GetCommandLineArgs()[0]
ismyprogram
path.Environment.ProcessExecutablePath
would bedotnet
path.
Interesting. Is this discrepancy desirable? It seems for a .NET developer, the virtualized output would generally make more sense, no?
We can describe the API by what it returns. It is the current proposal. The process executable path is non-ambiguous definition.
Or we can describe the value by what it is meant to be used for. It leads to some kind of policy to compute or configure the path. The existing AppContext.BaseDirectory
is on this plan . I think that AppContext.BaseDirectory
serves the scenarios that need the virtualized path well already. The problem is that the policy we use to set AppContext.BaseDirectory
does not always do what you want. Then you are left with P/Invoking OS-specific APIs because we do not provide the efficient platform-neutral wrapper for the policy-free API. https://github.com/dotnet/runtime/issues/13051 has example of this situation that we have hit with single-file.
As I was looking into this, I have found that there is API that returns policy-free process executable in WinForms: Application.ExecutablePath
. So this would be a cross-platform duplicate of the WinForms API.
Would it make sense to expose both the native and virtual paths:
public class Environment
{
// existing; new in .NET 5. returns cached value of Process.GetCurrentProcess().Id
public static int ProcessId { get; }
+ // file containing native entrypoint, which ProcessId refers to
+ // (e.g. abs. path to `dotnet` in `dotnet myapp.dll`)
+ public static string? ProcessExecutablePath { get; }
+ // file containing managed entrypoint
+ // (e.g. abs. path to `myapp.dll` in `dotnet myapp.dll`)
+ public static string ProcessMainModulePath { get; }
}
I am not sure sure what the difference between ProcessExecutablePath
and ProcessMainModulePath
would. What would be the implementation used for ProcessExecutablePath
and when would these two APIs return different paths?
Results would be hosting-dependent:
dotnet myapp.dll
, ProcessExecutablePath
will return /path/to/dotnet, and ProcessMainModulePath
will return /path/to/myapp.dll../myapp
(native executable; runtime-dependent
or self-contained
), both ProcessExecutablePath
and ProcessMainModulePath
will return /path/to/myapp
.This will make it explicit whether we are interested in the file, strictly containing the dotnet application entrypoint, or the native entrypoint (latter of which corresponds to Environment.ProcessId
).
in case of dotnet myapp.dll, ProcessExecutablePath will return /path/to/dotnet, and ProcessMainModulePath will return /path/to/myapp.dll.
If ProcessMainModulePath
is a cached value of Process.GetCurrentProcess().MainModule.FileName
as your comment states, it will not return /path/to/myapp.dll
in this case.
Assembly.GetEntryAssembly().Location
is typically not what you want to use as a path. You typically want to use AppContext.BaseDirectory
instead. Assembly.GetEntryAssembly()?.Location
returns null/empty string when CoreCLR is hosted and there is no entry assembly; or when the entry assembly is in single-file bundle and it does not physically exists on disk.
I was thinking that in cases where assembly with managed entrypoint does not physically exist, the second API will return the same value as ProcessExecutablePath
.
Extremely often developers also want to get the directory path instead. Yes I know this is a one liner from the new executable path propery but since we're discussing this proposal. Would it make sense to add a property for this as well considering how often it is going to be used? (FYI: On Winforms there is Application.StartupPath
besides the Application.ExecutablePath
)
namespace System
{
public class Environment {
// Returns null in environments where the current executable path is not available
public static string? ProcessExecutablePath { get; }
++ // Returns null in environments where the current executable directory is not available
++ public static string? ProcessDirectoryPath { get; }
}
}
Usage Example:
string logFile = Path.Combine(Environment.ProcessDirectoryPath, "logfile.txt");
Console.WriteLine(logFile );
On Winforms there is Application.StartupPath
Application.StartupPath
on WinForms just returns AppContext.BaseDirectory
. AppContext.BaseDirectory
is virtualized and it is the right option in most situations where people want to look for files that are part of the application. I think is ok to write the extra code in the less common situations where you really want the actual process .exe path.
I would like to use this for forking the current process - I've use that a couple of times for console applications.
For that purpose returning dotnet
is not that useful - we want myprogram.dll
.
To start second instance of the current process, you need both dotnet
and myprogram.dll
(or even full command line - depends on what you want to do).
It seems there are three concepts:
Main
The current proposal would represent them as follows:
Environment.ProcessExecutablePath
Environment.GetCommandLineArgs()[0]
AppContext.BaseDirectory
I don't mind having all three concepts, but I am concerned that there are three different ways to acquire them, some non-intuitive. Since the differences are nuanced and we know that the developers will usually pick the thing that is easiest to discover, which is likely going to be Environment.ProcessExecutablePath
, which won't be right choice for many scenarios.
I think it would be better if we were to expose these three choices on the same type with well-picked names and IntelliSense documentation explaining how to choose among them. My proposal is:
```C#
namespace System
{
public partial class Environment
{
// Returns the path to the file that launched the process. For framework dependent apps
// this will return the path to dotnet.exe. For environment where this concept doesn't
// exist, for example WebAssebmly, the method will return null.
public string? ApplicationProcessPath => Process.GetCurrentProcess().MainModule.FileName;
// Returns the path to the file that contains the `Main` method.
public string ApplicationEntryPointPath => GetCommandLineArgs()[0];
// Returns the directory path of your application. Same as AppContext.BaseDirectory.
public string ApplicationBaseDirectory => AppContext.BaseDirectory;
}
}
```
ApplicationProcessPath
they should probably all be marked nullableAppContext
non-nullable and return an empty string, but we could normalize that here.AppContext
but we don't believe that's a type people should be looking it because it's the platform's quirking mechanismAppEntryPointPath
returns a non-existing path for single file which makes it ill-defined (see: https://github.com/dotnet/runtime/issues/40874)FileVersionInfo
off of the application```C#
namespace System
{
public partial class Environment
{
// Returns the path to the file that launched the process. For framework dependent apps
// this will return the path to dotnet.exe. For environment where this concept doesn't
// exist, for example WebAssembly, the method will return null.
public string? AppProcessPath => Process.GetCurrentProcess().MainModule.FileName;
// Returns the path to the file that contains the `Main` method.
// Excluded. See comment above.
// public string? AppEntryPointPath => GetCommandLineArgs()[0];
// Returns the directory path of your application. Same as AppContext.BaseDirectory.
public string? AppBaseDirectory => AppContext.BaseDirectory;
}
}
```
ApplicationEntryPointPath
can return AppProcessPath
for single app and EntryPoint.Assembly.Location
for non-single app. GetCommandLineArgs()[0]
is not strictly needed to implement it.
// Returns the directory path of your application. Same as AppContext.BaseDirectory.
public string? AppBaseDirectory => AppContext.BaseDirectory;
One thing to be leery of is that AppContext.BaseDirectory
is not documented as "the directory path of your application". But instead it is documented as
Gets the pathname of the base directory that the assembly resolver uses to probe for assemblies.
For the newly proposed property, I think we want to be explicit that this property is "the directory path of your application". That way we won't ever get into the confusion that we had in 3.x with PublishSingleFile and AppContext.BaseDirectory. See https://github.com/dotnet/runtime/issues/3704.
Environment.GetCommandLineArgs()[0]
is also complicated. For embedded single-file we'd like it to return the executable path, since all the modules are embedded in the host and don't have a path of their own.
I think we want to be explicit that this property is "the directory path of your application"
What would you suggest that the implementation of the new property to be? I do not think there is one implementation that works for everybody.
From the top post on this issue:
Background and Motivation
The path of the current process executable is often needed for logging or to find more files next to the current process executable.
Examples of equivalent APIs in other environments:
os.Executable in https://golang.org/pkg/os/#Executable
Reading the golang doc, it says:
The main use case is finding resources located relative to an executable.
Which is exactly how AppContext.BaseDirectory
has been used historically. And this has caused major issues in 3.x with PublishSingleFile
which made AppContext.BaseDirectory
returns some random %TEMP%
directory. But that issue was justified as "well, this property was documented as 'the base directory that the assembly resolver uses to probe for assemblies', so you shouldn't be using it to find resources next to your executable."
We can use AppContext.BaseDirectory
for the implementation, if we are guaranteeing that going forward AppContext.BaseDirectory
will match "the directory path of your application". But as it is today, the way AppContext.BaseDirectory
is documented is that it is about where assemblies get probed, which is not the same as the main use case this API is trying to solve.
Which is exactly how AppContext.BaseDirectory has been used historically. And this has caused major issues in 3.x with PublishSingleFile which made AppContext.BaseDirectory returns some random %TEMP% directory
The resources were unzipped into the %TEMP% directory as well in the .NET 3.x single-file, in some cases at last. We made AppContext.BaseDirectory to return the %TEMP% directory in .NET 3 because we believed that it makes more cases work than it breaks.
My uber point is - if we introduce the new API Environment.AppBaseDirectory
, it is imperative we document it correctly, and that the documented definition of this API means it can be used for the intended use case (loading files relative to your application).
We should not say "it does the same thing as AppContext.BaseDirectory" - that is wrong. They are 2 different intended use cases.
Agree that this would need to be documented and that writing a good prescriptive documentation for the APIs like Environment.AppBaseDirectory
is not easy. FWIW, we have at least 3 APIs that all return the same path today: AppContext.BaseDirectory
, AppDomain.CurrentDomain.BaseDirectory
, Application.StartupPath
. This is adding 4th API that returns the same path.
Unrelated: I keep wondering whether Environment.AppProcessPath
is the right name. This API is about the process, not the app. Should it be just Environment.ProcessPath
? We have Environment.ProcessId
, not Environment.AppProcessId
. Opinions?
I think something that was strong in the API review was that each member should have the App
prefix to help discoverability in IntelliSense. That way, someone stumbling upon these APIs will be able to evaluate all of the options.
FWIW, we have at least 3 APIs that all return the same path today: AppContext.BaseDirectory, AppDomain.CurrentDomain.BaseDirectory, Application.StartupPath. This is adding 4th API that returns the same path.
They may return the same path in most cases, but the intention of these APIs is different, which makes them different.
Application.StartupPath
is doc'd in code as:
C#
/// <summary>
/// Gets the path for the executable file that started the application.
/// </summary>
public static string StartupPath
Today, it is implemented as calling AppContext.BaseDirectory;
which is wrong. Because if I used Application.StartupPath
in a 3.x PublishSingleFile
application, it will return the %TEMP%
directory - which is definitely the wrong location according to its documented behavior. The executable file that started the application is the one installed on disk, not the fake self-extracted assemblies in %TEMP%
.
So when we add the above Environment.AppBaseDirectory
, WinForms should be updated to call the new API instead of AppContext.BaseDirectory
. That way when the next PublishSingleFile
-like feature comes along, there is no confusion about what Environment.AppBaseDirectory
should return.
So we will have 4 APIs, but 2 sets of "duplicated" behaviors:
Should it be just Environment.ProcessPath ?
I think I agree with you. In the API review, the intention of appending an Application
prefix was to put these 3 APIs together. We then dropped one of them. And then shortened Application
to App
. But now that we are left with just 2 APIs that aren't really talking about the same thing, I think it makes sense to drop App
from ProcessPath
. Especially since there are times where this will be the path to %PROGRAMFILES%\dotnet\dotnet.exe
.
Given this discussion, I think we should push this back to API Review as I think we should get some clarity on the 2 new APIs here.
For Environment.AppBaseDirectory
, it may be useful to describe the policy for what it will return in different scenarios:
dotnet program.dll
program.exe
+ program.dll
(default publish)I'd also add scenarios for iOS/Android where the native process executable isn't necessarily next to the assemblies.
C#
namespace System
{
public partial class Environment
{
public static string? ProcessPath { get; }
}
}
Most helpful comment
Unrelated: I keep wondering whether
Environment.AppProcessPath
is the right name. This API is about the process, not the app. Should it be justEnvironment.ProcessPath
? We haveEnvironment.ProcessId
, notEnvironment.AppProcessId
. Opinions?