#1873 , #1876 - Handle Windows Application Control blocking after in-app upgrades

This commit is contained in:
rmcrackan
2026-06-16 17:25:18 -04:00
parent 23f36e2ef1
commit d955cb7605
8 changed files with 250 additions and 20 deletions

View File

@@ -69,6 +69,8 @@ public static class LibationScaffolding
// // outdated. kept here as an example of what belongs in this area
// // Migrations.migrate_to_v5_2_0__pre_config();
InstallFolderUnblock.TryUnblockProcessDirectoryIfWindows();
Configuration.SetLibationVersion(BuildVersion);
//***********************************************//

View File

@@ -151,13 +151,14 @@ public class App : Application
List<DataLayer.LibraryBook> library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
}
catch (Exception ex) when (StartupAssemblyBootstrap.IsMissingDependencyAssembly(ex))
catch (Exception ex) when (StartupAssemblyBootstrap.IsInstallFolderAssemblyLoadFailure(ex))
{
Serilog.Log.Logger.Error(ex, "Failed to load library at startup");
StartupAssemblyBootstrap.TryGetStartupFailureMessage(ex, out var title, out var body);
await MessageBox.Show(
MainWindow,
StartupAssemblyBootstrap.GetLibraryLoadFailureMessage(),
"Library load failed",
body,
title,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync([]));

View File

@@ -128,13 +128,24 @@ static class Program
{
var dispatcher = new DispatcherFrame();
var mbAlert = new MessageBoxAlertAdminDialog("""
Libation encountered a fatal error and must close.
string caption;
string message;
if (StartupAssemblyBootstrap.TryGetStartupFailureMessage(exception, out var title, out var body))
{
caption = title;
message = body;
}
else
{
caption = "Libation Crash";
message = """
Libation encountered a fatal error and must close.
Please consider reporting this issue on GitHub, including the contents of the LibationCrash.log file created in your user folder.
""",
"Libation Crash",
exception);
Please consider reporting this issue on GitHub, including the contents of the LibationCrash.log file created in your user folder.
""";
}
var mbAlert = new MessageBoxAlertAdminDialog(message, caption, exception);
mbAlert.Closed += (_, _) => dispatcher.Continue = false;
mbAlert.Show();
Dispatcher.UIThread.PushFrame(dispatcher);

View File

@@ -0,0 +1,82 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace LibationFileManager;
/// <summary>
/// Removes Mark-of-the-Web (Zone.Identifier) from install-folder binaries on Windows.
/// Helps after manual downloads and in-app upgrades before Application Control evaluates files.
/// </summary>
public static class InstallFolderUnblock
{
private static readonly string[] UnblockExtensions = [".dll", ".exe"];
public static void TryUnblockProcessDirectoryIfWindows()
{
if (!OperatingSystem.IsWindows())
return;
try
{
_ = UnblockDirectory(Configuration.ProcessDirectory);
}
catch
{
// Best-effort before logging may be available.
}
}
public static int UnblockDirectory(string directory)
{
if (!OperatingSystem.IsWindows() || string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
return 0;
var unblocked = 0;
if (TryUnblockFile(directory))
unblocked++;
foreach (var file in Directory.EnumerateFiles(directory))
{
if (!ShouldUnblock(file))
continue;
if (TryUnblockFile(file))
unblocked++;
}
return unblocked;
}
private static bool ShouldUnblock(string path)
{
var extension = Path.GetExtension(path);
foreach (var candidate in UnblockExtensions)
{
if (extension.Equals(candidate, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private static bool TryUnblockFile(string path)
{
var zoneIdentifier = path + ":Zone.Identifier";
if (!DeleteFile(zoneIdentifier))
{
var error = Marshal.GetLastWin32Error();
// File or stream does not exist.
if (error is 2 or 3)
return false;
throw new IOException($"Could not remove Zone.Identifier from '{path}'. Win32 error {error}.");
}
return true;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteFile(string lpFileName);
}

View File

@@ -9,6 +9,8 @@ namespace LibationFileManager;
public static class StartupAssemblyBootstrap
{
public const string EntityFrameworkCoreSqliteAssemblyFileName = "Microsoft.EntityFrameworkCore.Sqlite.dll";
public const int ApplicationControlBlockedHResult = unchecked((int)0x800711C7);
private const string TroubleshootApplicationControlUrl = "https://getlibation.com/docs/advanced/troubleshoot#windows-smart-app-control-and-in-app-upgrades";
/// <summary>
/// Registers <see cref="InteropFactory"/> assembly resolution and verifies required install-folder assemblies exist.
@@ -42,6 +44,9 @@ public static class StartupAssemblyBootstrap
This often happens after an incomplete in-app upgrade. Quit Libation completely, then install a fresh copy of the latest release to a new folder (do not overlay files on top of the old install).
If the error mentions an Application Control policy or Smart App Control, see:
{TroubleshootApplicationControlUrl}
Install folder:
{Configuration.ProcessDirectory}
@@ -49,6 +54,78 @@ public static class StartupAssemblyBootstrap
{Path.Combine(Configuration.ProcessDirectory, EntityFrameworkCoreSqliteAssemblyFileName)}
""";
public static string GetApplicationControlBlockedMessage(Exception? ex = null)
{
var blockedFile = TryGetBlockedAssemblyPath(ex) ?? "(unknown)";
return $"""
Windows blocked Libation from loading a required file in its install folder. This often happens after an in-app upgrade when Smart App Control or another Application Control policy is enabled.
Blocked file:
{blockedFile}
Install folder:
{Configuration.ProcessDirectory}
Your library database, accounts, and settings are stored separately and should be unaffected.
To recover:
1. Quit Libation completely.
2. Download the latest release zip from GitHub and extract it to a new folder (do not copy files on top of the old install).
3. Run Libation from the new folder.
If Windows still blocks the app, review Smart App Control under Windows Security -> App & browser control, or run this in PowerShell (replace the path if needed):
Unblock-File -Path '{Configuration.ProcessDirectory}\*' -Recurse
More help:
{TroubleshootApplicationControlUrl}
""";
}
public static bool IsApplicationControlBlockedAssembly(Exception ex)
{
for (var current = ex; current is not null; current = current.InnerException)
{
if (current is FileLoadException fileLoadException)
{
if (fileLoadException.HResult == ApplicationControlBlockedHResult)
return true;
if (fileLoadException.Message.Contains("Application Control policy", StringComparison.OrdinalIgnoreCase))
return true;
}
if (current.Message.Contains("Application Control policy", StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
public static bool IsInstallFolderAssemblyLoadFailure(Exception ex) =>
IsApplicationControlBlockedAssembly(ex) || IsMissingDependencyAssembly(ex);
public static bool TryGetStartupFailureMessage(Exception ex, out string title, out string body)
{
if (IsApplicationControlBlockedAssembly(ex))
{
title = "Libation blocked by Windows security";
body = GetApplicationControlBlockedMessage(ex);
return true;
}
if (IsMissingDependencyAssembly(ex))
{
title = "Library load failed";
body = GetLibraryLoadFailureMessage();
return true;
}
title = string.Empty;
body = string.Empty;
return false;
}
public static bool IsMissingDependencyAssembly(Exception ex)
{
for (var current = ex; current is not null; current = current.InnerException)
@@ -65,6 +142,20 @@ public static class StartupAssemblyBootstrap
return false;
}
private static string? TryGetBlockedAssemblyPath(Exception? ex)
{
for (var current = ex; current is not null; current = current.InnerException)
{
if (current is FileLoadException { FileName: { Length: > 0 } blockedPath })
return blockedPath;
if (current is FileNotFoundException { FileName: { Length: > 0 } missingPath })
return missingPath;
}
return null;
}
private static void ValidateEntityFrameworkCoreSqlitePresent()
{
var path = Path.Combine(Configuration.ProcessDirectory, EntityFrameworkCoreSqliteAssemblyFileName);

View File

@@ -79,11 +79,13 @@ static class Program
if (Configuration.Instance.SerilogInitialized)
Log.Error(ex, "Fatal error during startup");
var (title, body) = StartupAssemblyBootstrap.IsMissingDependencyAssembly(ex)
? ("Library load failed", StartupAssemblyBootstrap.GetLibraryLoadFailureMessage())
: (
"Fatal error, pre-logging",
"An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.");
string title;
string body;
if (!StartupAssemblyBootstrap.TryGetStartupFailureMessage(ex, out title, out body))
{
title = "Fatal error, pre-logging";
body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
}
try
{
@@ -210,14 +212,11 @@ static class Program
{
await form.InitLibraryAsync(await libraryLoadTask);
}
catch (Exception ex) when (StartupAssemblyBootstrap.IsMissingDependencyAssembly(ex))
catch (Exception ex) when (StartupAssemblyBootstrap.IsInstallFolderAssemblyLoadFailure(ex))
{
Log.Error(ex, "Failed to load library at startup");
MessageBoxLib.ShowAdminAlert(
form,
StartupAssemblyBootstrap.GetLibraryLoadFailureMessage(),
"Library load failed",
ex);
StartupAssemblyBootstrap.TryGetStartupFailureMessage(ex, out var title, out var body);
MessageBoxLib.ShowAdminAlert(form, body, title, ex);
await form.InitLibraryAsync([]);
}
}

View File

@@ -57,6 +57,16 @@ internal class WinInterop : IInteropFunctions
if (proc.ExitCode != 0)
throw new InvalidOperationException($"ZipExtractor exited with code {proc.ExitCode}.");
try
{
var unblockedCount = InstallFolderUnblock.UnblockDirectory(thisDir);
Serilog.Log.Logger.Information("Unblocked {UnblockedCount} install files after upgrade in {InstallDir}", unblockedCount, thisDir);
}
catch (Exception ex)
{
Serilog.Log.Logger.Warning(ex, "Could not unblock install files after upgrade in {InstallDir}", thisDir);
}
TrySyncInstallMetadata();
}

View File

@@ -32,6 +32,40 @@ Platform-specific steps: [Windows](#hangover-windows) · [macOS](#hangover-macos
## Windows
### Smart App Control and in-app upgrades {#windows-smart-app-control-and-in-app-upgrades}
After accepting an in-app update, Libation may fail to restart with an error like:
`An Application Control policy has blocked this file. (0x800711C7)`
Windows **Smart App Control** (and similar Application Control policies on recent Windows 11 builds) can block DLLs that were just written when the in-app upgrader overlays a new release onto your existing install folder. The blocked path is usually under your **Libation install folder** (where `Libation.exe` lives), not your user data folder (`%UserProfile%\Libation`).
**Symptoms**
- Fatal crash immediately after an in-app upgrade (Chardonnay / Avalonia).
- Classic may start but library import or database access fails with the same `0x800711C7` message on a `.dll` in the install folder.
- Windows Security may also warn about an unsigned library.
**Fix (recommended)**
1. Quit Libation completely.
2. Download the latest [release zip](https://github.com/rmcrackan/Libation/releases/latest) from GitHub.
3. Extract to a **new folder** (for example `C:\Apps\Libation`). Do **not** copy new files on top of the old install folder.
4. Run `Libation.exe` from the new folder. Your library database, accounts, and settings in `%UserProfile%\Libation` (or the path in `appsettings.json` -> `LibationFiles`) are separate and should still work.
**If Windows still blocks the new install**
1. Open **Windows Security** -> **App & browser control** and review **Smart App Control** (Evaluation or On modes are the usual trigger).
2. In PowerShell, unblock the install folder (adjust the path):
```powershell
Unblock-File -Path 'C:\Apps\Libation\*' -Recurse
```
3. Avoid running Libation from cloud-sync folders (OneDrive, etc.) if you can; use a normal local path for the install folder.
Related reports: [#1876](https://github.com/rmcrackan/Libation/issues/1876), [#1873](https://github.com/rmcrackan/Libation/issues/1873).
### Hangover (Windows)
Hangover.exe is located in the folder containing Libation.exe. Double-click it to run it.