Runtime: API Proposal: get full path of current process executable

Created on 14 Aug 2020  路  35Comments  路  Source: dotnet/runtime

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.

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

Proposed API

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; }
    }
}

Usage Examples

``` C#
Console.WriteLine(Environment.ProcessExecutablePath);

string logFilePath = Path.Combine(Environment.AppBaseDirectory, "logfile.txt");
```

Examples of equivalent APIs in other environments:

Alternative Designs

The 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 .

api-approved area-System.Runtime

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 just Environment.ProcessPath ? We have Environment.ProcessId, not Environment.AppProcessId. Opinions?

All 35 comments

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 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.

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:

  • in case of dotnet myapp.dll, ProcessExecutablePath will return /path/to/dotnet, and ProcessMainModulePath will return /path/to/myapp.dll.
  • in case of ./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:

  1. The process executable
  2. The module containing the developer's Main
  3. The developer's application directory

The current proposal would represent them as follows:

  1. Environment.ProcessExecutablePath
  2. Environment.GetCommandLineArgs()[0]
  3. 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;
}

}
```

Video

  • Similar to ApplicationProcessPath they should probably all be marked nullable

    • We decided to make AppContext non-nullable and return an empty string, but we could normalize that here.

  • We considered putting the APIs on AppContext but we don't believe that's a type people should be looking it because it's the platform's quirking mechanism
  • AppEntryPointPath returns a non-existing path for single file which makes it ill-defined (see: https://github.com/dotnet/runtime/issues/40874)

    • The API would be useful if it could be passed to the assembly load APIs, which doesn't seem to work

    • The only other scenario would getting the 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:

  • AppContext.BaseDirectory, AppDomain.CurrentDomain.BaseDirectory

    • I assume this legacy duplication is because we were trying to get rid of AppDomain, so we introduced AppContext. And then re-introduced AppDomain.

  • Application.StartupPath, Environment.AppBaseDirectory

    • This duplication is because Application.StartupPath existed first, and is only available in a WinForms app. And we "brought it lower" in the stack so it exists for all .NET applications.

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)
  • .NET 5 single-file application
  • .NET 3 unzip to disk single-file application
  • Hosted runtime (there multiple variant possible, hopefully they do not matter - nethost.lib, COM, managed C++, ...)

I'd also add scenarios for iOS/Android where the native process executable isn't necessarily next to the assemblies.

Video

  • Let's separate the issue of loading content files (#41341) the path of the OS process.
  • This looks good.

C# namespace System { public partial class Environment { public static string? ProcessPath { get; } } }

Was this page helpful?
0 / 5 - 0 ratings