diff --git a/Knossos.NET/Classes/FsoBuild.cs b/Knossos.NET/Classes/FsoBuild.cs index 3ea0e9f8..09ea8b48 100644 --- a/Knossos.NET/Classes/FsoBuild.cs +++ b/Knossos.NET/Classes/FsoBuild.cs @@ -320,7 +320,7 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, /// Get FSO flags structure of this build using the JSON format V1 /// /// A FlagsJsonV1 structure or null if failed - public FlagsJsonV1? GetFlagsV1() + public async Task GetFlagsV1Async() { var executable = GetExecutable(FsoExecType.Flags); var fullpath = GetExecutablePath(executable); @@ -374,10 +374,15 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, } cmd.Start(); - string result = cmd.StandardOutput.ReadToEnd(); - stderr = cmd.StandardError.ReadToEnd(); + // Read stdout and stderr concurrently to prevent pipe deadlock. + // Sequential reads deadlock if stderr output exceeds the pipe buffer (~4 KB). + var stdoutTask = cmd.StandardOutput.ReadToEndAsync(); + var stderrTask = cmd.StandardError.ReadToEndAsync(); + await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false); + string result = stdoutTask.Result; + stderr = stderrTask.Result; output = result; - cmd.WaitForExit(); + await cmd.WaitForExitAsync().ConfigureAwait(false); if (KnUtils.IsLinux && !string.IsNullOrEmpty(stderr)) { @@ -395,7 +400,7 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, if (!_flagErrorOneWarn) { _flagErrorOneWarn = true; - Dispatcher.UIThread.Invoke(async () => { await MessageBox.Show(MainWindow.instance, libfuseError, "Unable to run FSO", MessageBox.MessageBoxButtons.OK); }); + await Dispatcher.UIThread.InvokeAsync(async () => { await MessageBox.Show(MainWindow.instance, libfuseError, "Unable to run FSO", MessageBox.MessageBoxButtons.OK); }); } } else @@ -404,7 +409,7 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, if (!_flagErrorOneWarn) { _flagErrorOneWarn = true; - Dispatcher.UIThread.Invoke(async ()=> { await MessageBox.Show(MainWindow.instance, stderr, "Unable to run FSO", MessageBox.MessageBoxButtons.OK); }); + await Dispatcher.UIThread.InvokeAsync(async () => { await MessageBox.Show(MainWindow.instance, stderr, "Unable to run FSO", MessageBox.MessageBoxButtons.OK); }); } } return null; diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index 04460e40..8f3ea01f 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -588,10 +588,13 @@ public void CommitPendingChanges() /// Loads data from the GlobalSettings.cs class into this one to display it in the UI /// Also loads flag data from a FSO build, if one is installed /// - public void LoadData() + public async Task LoadDataAsync() { var old_path = KnUtils.GetFSODataFolderPath(); - var flagData = GetFlagData(); + // Fetch flag data on a background thread so the UI thread stays responsive. + // await resumes on Avalonia's UI synchronization context, so all ObservableProperty + // assignments below are safe. + var flagData = await Task.Run(GetFlagDataAsync); // reset the ini info if we have gotten an updated preferred path from FSO. if (old_path != KnUtils.GetFSODataFolderPath()){ @@ -1090,7 +1093,7 @@ public void LoadData() UnCommitedChanges = false; } - private FlagsJsonV1? GetFlagData() + private async Task GetFlagDataAsync() { FlagDataLoaded = false; var builds = Knossos.GetInstalledBuildsList(); @@ -1103,7 +1106,7 @@ public void LoadData() stables.Sort(FsoBuild.CompareVersion); foreach (var stable in stables) { - var flags = stable.GetFlagsV1(); + var flags = await stable.GetFlagsV1Async().ConfigureAwait(false); if (flags != null) { FlagDataLoaded = true; @@ -1120,7 +1123,7 @@ public void LoadData() { foreach (var other in others) { - var flags = other.GetFlagsV1(); + var flags = await other.GetFlagsV1Async().ConfigureAwait(false); if (flags != null) { FlagDataLoaded = true; @@ -1168,7 +1171,7 @@ internal async void BrowseFolderCommand() Knossos.globalSettings.basePath = result[0].Path.LocalPath.ToString(); Knossos.globalSettings.Save(); Knossos.ResetBasePath(); - LoadData(); + await LoadDataAsync(); } } catch (Exception ex) @@ -1184,12 +1187,12 @@ await Dispatcher.UIThread.Invoke(async () => { /// /// Reload data from json /// - internal void ResetCommand() + internal async void ResetCommand() { var pxoUser = Knossos.globalSettings.pxoLogin; var pxoPassword = Knossos.globalSettings.pxoPassword; Knossos.globalSettings = new GlobalSettings(); - LoadData(); + await LoadDataAsync(); Knossos.globalSettings.pxoPassword = pxoPassword; Knossos.globalSettings.pxoLogin = pxoUser; SaveCommand(); @@ -1465,10 +1468,10 @@ internal void GlobalCmdHelp() /// /// Reloads configuration and FSO flag data /// - internal void ReloadFlagData() + internal async void ReloadFlagData() { Knossos.globalSettings.Load(); - LoadData(); + await LoadDataAsync(); } /// diff --git a/Knossos.NET/ViewModels/Templates/DevModFsoSettingsViewModel.cs b/Knossos.NET/ViewModels/Templates/DevModFsoSettingsViewModel.cs index 0df97e23..bf79ddb9 100644 --- a/Knossos.NET/ViewModels/Templates/DevModFsoSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/DevModFsoSettingsViewModel.cs @@ -3,6 +3,7 @@ using Knossos.NET.Models; using Knossos.NET.Views; using System.Linq; +using System.Threading.Tasks; namespace Knossos.NET.ViewModels { @@ -70,7 +71,7 @@ private void LoadFsoPicker() } } - internal void ConfigureBuild() + internal async void ConfigureBuild() { if (editor == null || FsoPicker == null) return; @@ -102,24 +103,24 @@ internal void ConfigureBuild() if(fsoBuild != null) { - var flagsV1=fsoBuild.GetFlagsV1(); + var flagsV1 = await Task.Run(fsoBuild.GetFlagsV1Async); if(flagsV1 != null) { FsoFlags = new FsoFlagsViewModel(flagsV1, CmdLine); } else { - Dispatcher.UIThread.InvokeAsync(async () => + await Dispatcher.UIThread.InvokeAsync(async () => { await MessageBox.Show(MainWindow.instance!, "Unable to get flag data from build " + fsoBuild + " It might be below the minimal version supported (3.8.1) or some other error ocurred.", "Invalid flag data", MessageBox.MessageBoxButtons.OK); }); - + } } else { /* No valid build found, send message */ - Dispatcher.UIThread.InvokeAsync(async () => + await Dispatcher.UIThread.InvokeAsync(async () => { await MessageBox.Show(MainWindow.instance!, "Unable to resolve FSO build dependency, download the correct one or manually select a FSO version. ", "Not engine build found", MessageBox.MessageBoxButtons.OK); }); diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 59ccac34..fa699d80 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -354,7 +354,7 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) GlobalSettingsView?.CheckDisplaySettingsWarning(); } Knossos.globalSettings.Load(); - GlobalSettingsView?.LoadData(); + _ = GlobalSettingsView?.LoadDataAsync(); GlobalSettingsView?.UpdateImgCacheSize(); } @@ -498,7 +498,7 @@ public void RemoveInstalledModVersion(Mod mod) /// public void GlobalSettingsLoadData() { - GlobalSettingsView?.LoadData(); + _ = GlobalSettingsView?.LoadDataAsync(); } internal void ApplySettings() diff --git a/Knossos.NET/ViewModels/Windows/ModSettingsViewModel.cs b/Knossos.NET/ViewModels/Windows/ModSettingsViewModel.cs index b9e00cc9..f3e929cb 100644 --- a/Knossos.NET/ViewModels/Windows/ModSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModSettingsViewModel.cs @@ -377,9 +377,9 @@ internal void SaveSettingsCommand() } } - internal void ConfigureBuildCommand() + internal async void ConfigureBuildCommand() { - ConfigureBuild(false); + await ConfigureBuild(false); } /// @@ -387,7 +387,7 @@ internal void ConfigureBuildCommand() /// Also allows to edit the mod cmdline /// /// - private void ConfigureBuild(bool ignoreUserSettings) + private async Task ConfigureBuild(bool ignoreUserSettings) { if (modJson == null) return; @@ -411,24 +411,24 @@ private void ConfigureBuild(bool ignoreUserSettings) if(fsoBuild != null) { - var flagsV1=fsoBuild.GetFlagsV1(); + var flagsV1 = await Task.Run(fsoBuild.GetFlagsV1Async); if(flagsV1 != null) { FsoFlags = new FsoFlagsViewModel(flagsV1,modJson.GetModCmdLine(ignoreUserSettings)); } else { - Dispatcher.UIThread.InvokeAsync(async () => + await Dispatcher.UIThread.InvokeAsync(async () => { await MessageBox.Show(MainWindow.instance!, "Unable to get flag data from build " + fsoBuild + " It might be below the minimal version supported (3.8.1) or some other error ocurred.", "Invalid flag data", MessageBox.MessageBoxButtons.OK); }); - + } } else { /* No valid build found, send message */ - Dispatcher.UIThread.InvokeAsync(async () => + await Dispatcher.UIThread.InvokeAsync(async () => { await MessageBox.Show(MainWindow.instance!, "Unable to resolve FSO build dependency, download the correct one or manually select a FSO version. ", "Not engine build found", MessageBox.MessageBoxButtons.OK); }); @@ -438,13 +438,13 @@ private void ConfigureBuild(bool ignoreUserSettings) /// /// Reset settings to DEFAULT on UI /// - internal void ResetSettingsCommand() + internal async void ResetSettingsCommand() { CustomDependencies = false; BuildMissingWarning = string.Empty; FsoPicker = new FsoBuildPickerViewModel(null, window); FsoFlags = null; - ConfigureBuild(true); + await ConfigureBuild(true); DepItems.Clear(); CreateDependencyItems(true); IgnoreGlobalCmd = false;