fix(themes): keep last-known-good custom theme on transient file-lock

This commit is contained in:
2026-05-07 15:29:05 +02:00
parent aff2528a6f
commit e4ee7aaafa
2 changed files with 24 additions and 6 deletions
+5 -1
View File
@@ -63,7 +63,11 @@ internal static class ThemeJsonLoader
public static Theme LoadFromFile(string path)
{
var json = File.ReadAllText(path);
// FileShare.Read lets concurrent readers and well-behaved editors share
// the handle; atomic-replace editors still raise IOException, caught upstream.
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
return LoadFromString(json);
}
+19 -5
View File
@@ -52,6 +52,16 @@ public sealed class ThemeRegistry
public void Switch(string slug) => _active = Get(slug);
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION. Other
// IO failures are permanent and get the theme dropped instead of retried.
internal static bool IsRecoverableFileLock(Exception? ex)
{
if (ex is not IOException io)
return false;
var code = (uint)io.HResult;
return code == 0x80070020u || code == 0x80070021u;
}
// Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit
// LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup
// neu eingelesen.
@@ -89,12 +99,16 @@ public sealed class ThemeRegistry
theme.RecomputeAbgrCache();
_customCache[key] = (theme, stamp);
}
catch (Exception ex)
catch (Exception ex) when (IsRecoverableFileLock(ex))
{
// Editor mid-save: keep the cached snapshot, leave the stamp
// alone so the next refresh retries automatically.
Plugin.Log.Debug($"Custom theme {Path.GetFileName(path)} is locked, keeping last known good");
if (cached.Theme is not null)
theme = cached.Theme;
}
catch (Exception)
{
// Logging passiert in Plugin.cs durch den Aufrufer; hier still
// ignorieren, damit ein einzelnes kaputtes JSON nicht alle
// Custom-Themes blockt.
_ = ex;
continue;
}
}