mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-06-25 07:59:41 -04:00
This commit is contained in:
@@ -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);
|
||||
|
||||
//***********************************************//
|
||||
|
||||
@@ -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([]));
|
||||
|
||||
@@ -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);
|
||||
|
||||
82
Source/LibationFileManager/InstallFolderUnblock.cs
Normal file
82
Source/LibationFileManager/InstallFolderUnblock.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user