diff --git a/HellionChat/Themes/ThemeJsonLoader.cs b/HellionChat/Themes/ThemeJsonLoader.cs index e340165..79b69dc 100644 --- a/HellionChat/Themes/ThemeJsonLoader.cs +++ b/HellionChat/Themes/ThemeJsonLoader.cs @@ -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); } diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs index 5b3ce23..82ea605 100644 --- a/HellionChat/Themes/ThemeRegistry.cs +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -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; } }