Skip to content

Commit 3de745f

Browse files
JusterZhuclaude
andcommitted
fix: resolve app startup hang in IDE debug mode
Root causes: 1. Irihi.Lingua NuGet package (unused) — source generator/runtime may cause debug-mode initialization issues. Removed until actual integration. 2. LoadFromResources() called in Initialize() — asset loader may not be ready yet. Moved to OnFrameworkInitializationCompleted(). 3. async void OnFrameworkInitializationCompleted — Avalonia's framework initialization does not reliably support async continuations; the await on LoadAsync could cause the continuation to be dropped on certain synchronization contexts. 4. No synchronous config loading path — startup now uses synchronous Load()/Save() to avoid async-related deadlocks during initialization. Runtime saves remain async (window close, property changes). Changes: - Remove Irihi.Lingua 0.2.0 package reference - Move LoadFromResources() from Initialize() to OnFrameworkInitializationCompleted() - Change OnFrameworkInitializationCompleted back to synchronous (void) - Add IConfigService.Load() + ConfigService.Load() for synchronous startup - Add private ConfigService.Save() for synchronous writes during Load() - Add LoadConfigSafe() with exception-protected synchronous config init Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 589923a commit 3de745f

4 files changed

Lines changed: 115 additions & 9 deletions

File tree

src/App.axaml.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,17 @@ public partial class App : Application
1515
public override void Initialize()
1616
{
1717
AvaloniaXamlLoader.Load(this);
18-
19-
// Load translations from embedded JSON files (falls back to built-in dictionaries)
20-
Services.LocalizationService.Instance.LoadFromResources();
2118
}
2219

23-
public override async void OnFrameworkInitializationCompleted()
20+
public override void OnFrameworkInitializationCompleted()
2421
{
2522
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
2623
{
27-
// Initialize configuration
28-
var configService = ConfigServiceSingleton.Instance;
29-
await configService.LoadAsync();
30-
var config = configService.Config;
24+
// Load translations from embedded JSON files (must be after platform init)
25+
Services.LocalizationService.Instance.LoadFromResources();
26+
27+
// Initialize configuration (synchronous path for startup reliability)
28+
var config = LoadConfigSafe();
3129

3230
// Apply saved theme
3331
RequestedThemeVariant = config.Theme == "Dark"
@@ -49,6 +47,7 @@ public override async void OnFrameworkInitializationCompleted()
4947
mainWindow.WindowState = WindowState.Maximized;
5048

5149
// Save window state on close
50+
var configService = ConfigServiceSingleton.Instance;
5251
mainWindow.Closing += (_, _) =>
5352
{
5453
if (mainWindow.WindowState == WindowState.Maximized)
@@ -71,6 +70,26 @@ public override async void OnFrameworkInitializationCompleted()
7170

7271
base.OnFrameworkInitializationCompleted();
7372
}
73+
74+
/// <summary>
75+
/// Load configuration synchronously with exception protection.
76+
/// Uses blocking I/O on first load to avoid async-initialization races in Avalonia.
77+
/// Save operations remain async (fire-and-forget from window close / property changes).
78+
/// </summary>
79+
private static AppConfig LoadConfigSafe()
80+
{
81+
try
82+
{
83+
var configService = ConfigServiceSingleton.Instance;
84+
// Synchronous load for startup reliability
85+
configService.Load();
86+
return configService.Config;
87+
}
88+
catch
89+
{
90+
return new AppConfig();
91+
}
92+
}
7493
}
7594

7695
/// <summary>

src/Configuration/ConfigService.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,91 @@ public ConfigService()
3636
_backupPath = Path.Combine(_configDir, "config.json.backup");
3737
}
3838

39+
/// <inheritdoc />
40+
public void Load()
41+
{
42+
Directory.CreateDirectory(_configDir);
43+
44+
if (!File.Exists(_configPath))
45+
{
46+
// Try to recover from backup
47+
if (File.Exists(_backupPath))
48+
{
49+
try
50+
{
51+
var backupJson = File.ReadAllText(_backupPath);
52+
Config = JsonConvert.DeserializeObject<AppConfig>(backupJson, JsonSettings) ?? new AppConfig();
53+
Save(); // Restore main file from backup
54+
return;
55+
}
56+
catch
57+
{
58+
// Backup is corrupted; fall through to defaults
59+
}
60+
}
61+
62+
// First run: save defaults so the file exists
63+
Config = new AppConfig();
64+
Save();
65+
return;
66+
}
67+
68+
try
69+
{
70+
var json = File.ReadAllText(_configPath);
71+
Config = JsonConvert.DeserializeObject<AppConfig>(json, JsonSettings) ?? new AppConfig();
72+
73+
// Run schema migrations
74+
Migrate();
75+
}
76+
catch (JsonException)
77+
{
78+
// Config file is corrupted; try backup
79+
if (File.Exists(_backupPath))
80+
{
81+
try
82+
{
83+
var backupJson = File.ReadAllText(_backupPath);
84+
Config = JsonConvert.DeserializeObject<AppConfig>(backupJson, JsonSettings) ?? new AppConfig();
85+
Save();
86+
return;
87+
}
88+
catch
89+
{
90+
// Backup also corrupted; reset
91+
}
92+
}
93+
94+
Config = new AppConfig();
95+
Save();
96+
}
97+
}
98+
99+
/// <summary>Synchronous save for internal use during Load().</summary>
100+
private void Save()
101+
{
102+
_saveLock.Wait();
103+
try
104+
{
105+
Directory.CreateDirectory(_configDir);
106+
107+
if (File.Exists(_configPath))
108+
{
109+
try { File.Copy(_configPath, _backupPath, overwrite: true); }
110+
catch { /* Non-critical */ }
111+
}
112+
113+
var json = JsonConvert.SerializeObject(Config, JsonSettings);
114+
var tempPath = _configPath + ".tmp";
115+
File.WriteAllText(tempPath, json);
116+
File.Move(tempPath, _configPath, overwrite: true);
117+
}
118+
finally
119+
{
120+
_saveLock.Release();
121+
}
122+
}
123+
39124
/// <inheritdoc />
40125
public async Task LoadAsync()
41126
{

src/Configuration/IConfigService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ public interface IConfigService
1010
/// <summary>Current in-memory configuration. Save to persist changes.</summary>
1111
AppConfig Config { get; }
1212

13+
/// <summary>Load configuration from disk synchronously. Called once at startup.</summary>
14+
void Load();
15+
1316
/// <summary>Load configuration from disk. Called once at startup.</summary>
1417
Task LoadAsync();
1518

src/GeneralUpdate.Tools.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
<ProjectReference Include="..\..\..\GeneralUpdate\src\c#\GeneralUpdate.Core\GeneralUpdate.Core.csproj" />
3333
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
3434
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="9.0.0" />
35-
<PackageReference Include="Irihi.Lingua" Version="0.2.0" />
3635
</ItemGroup>
3736

3837
<ItemGroup>

0 commit comments

Comments
 (0)