dotnet-bundle icon indicating copy to clipboard operation
dotnet-bundle copied to clipboard

OSX - RuntimeInformation is causing application crashes

Open NoralK opened this issue 3 years ago • 4 comments

I am using RuntimeInformation for OS specific code. This is the snippet of code:

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
  // do windows stuff
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)){
  // do mac stuff
}

~~In Avalonia 0.9.13 this works fine but in 0.10.0.preview4 it causes the application to crash on OSX, Windows is find and I do not know about the other platforms.~~ I have found out that Dotnet.Bundle is causing the issue, not Avalonia. In Dotnet.Bulndle 0.9.12 there is no issue but in 0.9.13 it causes the crash.

The OSx Problem Report is here https://pastebin.com/L2TsVVbC

I originally reported this on Avalonia AvaloniaUI/Avalonia/issues/4596

NoralK avatar Sep 02 '20 16:09 NoralK

Where is this code executed? In your application? When the crash happens, at the build time?

x2bool avatar Sep 04 '20 15:09 x2bool

I'm seeing this same crash with Avalonia 0.10 - reverting to Dotnet.Bundle 0.9.12 does not fix the issue for me.

My self-contained app runs fine unbundled, but launching the bundle causes the crash.

I do use RuntimeInformation, but even disabling those calls doesn't fix it for me.

jonthysell avatar Feb 16 '21 22:02 jonthysell

Seems like I know solution for this issue. This can be path related issue. If you use smth like that in you code Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) Then check if folder exist and create if not.

ia-alpatov avatar Mar 10 '21 20:03 ia-alpatov

I'm seeing this same crash with Avalonia 0.10 - reverting to Dotnet.Bundle 0.9.12 does not fix the issue for me.

My self-contained app runs fine unbundled, but launching the bundle causes the crash.

I do use RuntimeInformation, but even disabling those calls doesn't fix it for me.

I finally figured this out for my app. The problem was an unexpected, unhandled exception at my application start that only happens when MacOS launches from a "bundled app" (aka, what DotNetBundle creates).

The issue was combination of:

  1. Using -p:PublishSingleFile=true to publish a single-file executable
  2. Packaging that executable in a OSX bundle (.app folder) using dotnet-bundle
  3. Calling var fileStream = new FileStream("config.xml", FileMode.OpenOrCreate); outside of a try-catch.

When using a single-file executable, what you actually get is a native binary that first extracts all of your files (application and framework) to a hidden temp folder, then executes your main binary in that folder. This is fine, and works on all platforms, even OSX. In my app, when I created the new FileStream, that config.xml file would get created in that temp folder no problem.

However, when put into the bundle using dotnet-bundle, you get a different behavior: double-click the bundle and the single native binary still runs and extracts your files to the temp folder, however, when it then executes your main binary, it's got some kind of different security context and which throws a security exception trying to create the file.

It's that unhandled exception that causes the app to crash, and unfortunately, with no useful information.

My fix was to:

  1. Switch my publishing profile for OSX to not use single-file, i.e. I removed -p:PublishSingleFile=true so no more extraction and security exceptions
  2. Comb over my application startup and make sure anything that might throw an exception (that I could handle) was properly handled. In this case, I'm loading/creating a config file but it isn't strictly necessary as the app already has a hard-coded config to fallback on. However I also found plenty of other cases where I was not expecting (and therefore not handling) exceptions at startup. Now I throw a dialog with the stack if possible, so I have something to go on, rather than just letting the app silently crash.

Since my goal was load a config file placed side-by-side with the application that the user invoked (i.e., the single-file binary on Windows/Linux and the now .app bundle on OSX), I changed my file loading to look like:

// appPath will be wherever the starting executable is located, which is the compressed single-file on Windows/Linux, and inside the Contents/MacOS folder of a bundled MacOS app
string appPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName) ?? Environment.CurrentDirectory;

// Only want to change behavior in this one case, we don't want to look for/create the config within the .app bundle
bool isMacBundle = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && AppContext.BaseDirectory.EndsWith(".app/Contents/MacOS/");

// Here we make sure that if we *are* inside a macOS bundle, to go up three directories to parent of the dir of the bundle
string configFile = Path.Combine(isMacBundle ? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../")) : appPath, "config.xml");

var fileStream = new FileStream(configFile, FileMode.OpenOrCreate);

It took a lot of experimenting to figure out how to get the path I wanted (side-by side with the "app" the user clicks on), but this setup supports framework-dependent publishes, Windows single-file, Linux single-file, and OSX single-file. The logic also handles the case if the user does try to run an unbundled binary on OSX (single-file or framework-dependent), because it'll behave exactly the same as on Windows/Linux.

jonthysell avatar Jul 05 '21 23:07 jonthysell