JsonSettings icon indicating copy to clipboard operation
JsonSettings copied to clipboard

Trying to get the executing path via Assembly.GetEntryAssembly().Location throws an ArgumentException in bundled assemblies.

Open devpelux opened this issue 4 years ago • 3 comments

As the title describes, trying to get the executing path via Assembly.GetEntryAssembly().Location throws an ArgumentException in bundled assemblies, because Location returns an empty string. See: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location?#remarks

This happened to my project because i'm using the new "single file" feature of net 5 that generates a single bundled assembly file by merging all the libraries into the exe.

Exception

As you can see Location is empty and even CodeBase throws an exception, the same happens with Assembly.GetExecutingAssembly().Location.

For getting the executing directory is suggested to use AppContext.BaseDirectory. Just changed ExecutingDirectory to this:

public static DirectoryInfo `ExecutingDirectory` => new DirectoryInfo(Path.GetDirectoryName(AppContext.BaseDirectory));

ExecutingExe is no more needed, but you can get it via Path.Combine(AppContext.BaseDirectory, Assembly.GetEntryAssembly().ManifestModule.ScopeName).

See: https://github.com/dotnet/corert/issues/5467#issuecomment-369202524

To debug the issue is a bit tricky: 1 Publish inside a new net 5 Wpf app, select Debug in configuration, then check "produce single file".

Debug

2 Copy the generated files with all the pdb files inside the debug folder (Debug/net5.0...). 3 Run debug.

devpelux avatar May 16 '21 15:05 devpelux

I have made a comparison about this before First of all, using Assembly Method is not good because of the different values it has in .NET Framework, .NET Core and .Net 5 and single file mode (This test is written in a DLL library and run in a WPF program in .Net Framework, .Net Core 3.x and .Net 5 in Single File and portable mode)

note: GetProccess is a native method

SDK Method Portable Publish Status
.Net 4.5 GetCallingAssembly bin\Debug\net45\WpfApp12.exe :heavy_check_mark:
.Net 4.5 GetEntryAssembly bin\Debug\net45\WpfApp12.exe :heavy_check_mark:
.Net 4.5 GetExecutablePath bin\Debug\net45\WpfApp12.exe :heavy_check_mark:
.Net 4.5 GetExecutingAssembly bin\Debug\net45\WpfCustomControlLibrary1.dll :x:
.Net 4.5 GetProccess bin\Debug\net45\WpfApp12.exe :heavy_check_mark:

bin\Debug\netcoreapp3.1\

SDK Method Portable Publish Status
Core 3.1 GetCallingAssembly WpfApp12.dll qshmjn44.1xl\WpfApp12.dll :x:
Core 3.1 GetEntryAssembly WpfApp12.dll qshmjn44.1xl\WpfApp12.dll :x:
Core 3.1 GetExecutablePath WpfApp12.exe publish\WpfApp12.exe :heavy_check_mark:
Core 3.1 GetExecutingAssembly WpfCustomControlLibrary1.dll qshmjn44.1xl\WpfCustomControlLibrary1.dll :x:
Core 3.1 GetProccess WpfApp12.exe publish\WpfApp12.exe :heavy_check_mark:

bin\Debug\net5.0-windows\

SDK Method Portable Publish Status
.Net 5 GetCallingAssembly WpfApp12.dll :x:
.Net 5 GetEntryAssembly WpfApp12.dll :x:
.Net 5 GetExecutablePath WpfApp12.exe publish\WpfApp12.exe :heavy_check_mark:
.Net 5 GetExecutingAssembly WpfCustomControlLibrary1.dll :x:
.Net 5 GetProccess WpfApp12.exe publish\WpfApp12.exe :heavy_check_mark:

Also, using the AppContext.BaseDirectory is not very convenient because it gives different values this is .Net 3

CurrentDirectory : C:\Users\Mahdi\source\repos\net3.1\net3.1\bin\Release\netcoreapp3.1\publish
AppDomain : C:\Users\Mahdi\AppData\Local\Temp\.net\net3.1\vy4mio3s.mik\
AppContext : C:\Users\Mahdi\AppData\Local\Temp\.net\net3.1\vy4mio3s.mik\

and this is .Net 5

CurrentDirectory : C:\Users\Mahdi\source\repos\net3.1\net5\bin\Release\net5.0-windows\publish
AppDomain : C:\Users\Mahdi\source\repos\net3.1\net5\bin\Release\net5.0-windows\publish\
AppContext : C:\Users\Mahdi\source\repos\net3.1\net5\bin\Release\net5.0-windows\publish\

The best solution I have found is to use the native method which gives the same value in all modes and frameworks

[System.Runtime.InteropServices.DllImport("kernel32.dll")]
	static extern uint GetModuleFileName(IntPtr hModule, System.Text.StringBuilder lpFilename, int nSize);
	static readonly int MAX_PATH = 255;
	public static string GetExecutablePath() {
		if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {
			var sb = new System.Text.StringBuilder(MAX_PATH);
			GetModuleFileName(IntPtr.Zero, sb, MAX_PATH);
			return sb.ToString();
		}
		else {
			return System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
		}
	}

ghost1372 avatar May 16 '21 15:05 ghost1372

This solves it for Windows, I do wonder about single-file on Linux.

Nucs avatar May 22 '21 06:05 Nucs

I did not test but it should work for linux System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;

ghost1372 avatar May 23 '21 11:05 ghost1372

We have 3 ways to resolve the executing exe's path so we can resolve paths like "justfile.json". If All fail, we fallback to Current Directory provided by Environment.CurrentDirectory

Nucs avatar Jan 14 '23 06:01 Nucs