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

API Proposal: get full path of current process executable #40862

Closed
jkotas opened this issue Aug 14, 2020 · 35 comments · Fixed by #42768
Closed

API Proposal: get full path of current process executable #40862

jkotas opened this issue Aug 14, 2020 · 35 comments · Fixed by #42768
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Milestone

Comments

@jkotas
Copy link
Member

jkotas commented Aug 14, 2020

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

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

@jkotas jkotas added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Aug 14, 2020
@Dotnet-GitSync-Bot
Copy link
Collaborator

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.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Aug 14, 2020
@jkotas jkotas added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Aug 14, 2020
@terrajobst
Copy link
Member

terrajobst commented Aug 14, 2020

I usually do it via Environment.GetCommandLineArgs()[0] but that's not pretty/intuitive. I like the proposal!

@terrajobst
Copy link
Member

(also tagging IO, b/c it seems relevant)

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

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.

@danmoseley
Copy link
Member

In what environments would it return null?

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

Wasm?

@terrajobst
Copy link
Member

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?

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

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. #13051 has example of this situation that we have hit with single-file.

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

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.

@am11
Copy link
Member

am11 commented Aug 15, 2020

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

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

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?

@am11
Copy link
Member

am11 commented Aug 15, 2020

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

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

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.

@jkotas
Copy link
Member Author

jkotas commented Aug 15, 2020

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.

@am11
Copy link
Member

am11 commented Aug 15, 2020

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.

@Symbai
Copy link

Symbai commented Aug 16, 2020

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

@jkotas
Copy link
Member Author

jkotas commented Aug 16, 2020

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.

@YairHalberstadt
Copy link
Contributor

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.

@jkotas
Copy link
Member Author

jkotas commented Aug 16, 2020

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

@carlossanlop carlossanlop added this to the 6.0.0 milestone Aug 17, 2020
@carlossanlop carlossanlop removed the untriaged New issue has not been triaged by the area owner label Aug 17, 2020
@terrajobst
Copy link
Member

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:

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

@jkotas
Copy link
Member Author

jkotas commented Aug 19, 2020

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.

@eerhardt
Copy link
Member

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.

@jkotas
Copy link
Member Author

jkotas commented Aug 19, 2020

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.

@jkotas
Copy link
Member Author

jkotas commented Aug 19, 2020

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?

@reflectronic
Copy link
Contributor

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.

@eerhardt
Copy link
Member

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:

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

@eerhardt eerhardt added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-approved API was approved in API review, it can be implemented labels Aug 20, 2020
@jkotas
Copy link
Member Author

jkotas commented Aug 20, 2020

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++, ...)

@akoeplinger
Copy link
Member

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

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Aug 25, 2020
@terrajobst
Copy link
Member

terrajobst commented Aug 25, 2020

Video

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

jkotas added a commit to jkotas/runtime that referenced this issue Sep 26, 2020
jkotas added a commit that referenced this issue Oct 1, 2020
Fixes #40862

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Ryan Lucia <ryan@luciaonline.net>
@ghost ghost locked as resolved and limited conversation to collaborators Dec 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Projects
None yet
Development

Successfully merging a pull request may close this issue.