diff --git a/CreamInstaller/Forms/UpdateForm.cs b/CreamInstaller/Forms/UpdateForm.cs index ccbf6dc..41eea21 100644 --- a/CreamInstaller/Forms/UpdateForm.cs +++ b/CreamInstaller/Forms/UpdateForm.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; -using System.Drawing; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; using CreamInstaller.Components; using CreamInstaller.Utility; @@ -12,7 +17,12 @@ namespace CreamInstaller.Forms; internal sealed partial class UpdateForm : CustomForm { - private Release latestRelease; + private static readonly string PackagePath = ProgramData.DirectoryPath + @"\" + Program.RepositoryPackage; + private static readonly string ExecutablePath = ProgramData.DirectoryPath + @"\" + Program.RepositoryExecutable; + private static readonly string UpdaterPath = ProgramData.DirectoryPath + @"\updater.cmd"; + + private CancellationTokenSource cancellation; + private ProgramRelease latestRelease; internal UpdateForm() { @@ -45,11 +55,11 @@ internal sealed partial class UpdateForm : CustomForm #if !DEBUG Version currentVersion = new(Program.Version); #endif - List releases = null; + List releases = null; string response = await HttpClientManager.EnsureGet($"https://api.github.com/repos/{Program.RepositoryOwner}/{Program.RepositoryName}/releases"); if (response is not null) - releases = JsonConvert.DeserializeObject>(response) - ?.Where(release => !release.Draft && !release.Prerelease && release.Assets.Count > 0).ToList(); + releases = JsonConvert.DeserializeObject>(response) + ?.Where(release => !release.Draft && !release.Prerelease && release.Asset is not null).ToList(); latestRelease = releases?.FirstOrDefault(); #if DEBUG if (latestRelease?.Version is not { } latestVersion) @@ -66,7 +76,7 @@ internal sealed partial class UpdateForm : CustomForm changelogTreeView.Visible = true; for (int r = releases!.Count - 1; r >= 0; r--) { - Release release = releases[r]; + ProgramRelease release = releases[r]; #if !DEBUG if (release.Version <= currentVersion) continue; @@ -94,18 +104,7 @@ internal sealed partial class UpdateForm : CustomForm retry: try { - string fileName = Path.GetFileName(Program.CurrentProcessFilePath); - if (fileName != Program.ApplicationExecutable) - { - using DialogForm form = new(this); - if (form.Show(SystemIcons.Warning, - "WARNING: " + Program.ApplicationExecutable + " was renamed!" + "\n\nThis will cause undesirable behavior when updating the program!", - "Ignore", "Abort") == DialogResult.Cancel) - { - Application.Exit(); - return; - } - } + UpdaterPath.DeleteFile(); OnLoad(); } catch (Exception e) @@ -118,8 +117,9 @@ internal sealed partial class UpdateForm : CustomForm private void OnIgnore(object sender, EventArgs e) => StartProgram(); - private void OnUpdate(object sender, EventArgs e) + private async void OnUpdate(object sender, EventArgs e) { + progressBar.Value = 0; progressBar.Visible = true; ignoreButton.Visible = false; updateButton.Text = "Cancel"; @@ -127,20 +127,109 @@ internal sealed partial class UpdateForm : CustomForm updateButton.Click += OnUpdateCancel; changelogTreeView.Location = progressBar.Location with { Y = progressBar.Location.Y + progressBar.Size.Height + 6 }; Refresh(); - Progress progress = new(); - progress.ProgressChanged += delegate(object _, double _progress) + Progress progress = new(); + IProgress iProgress = progress; + progress.ProgressChanged += delegate(object _, int _progress) { - progressLabel.Text = $"Updating . . . {(int)_progress}%"; - progressBar.Value = (int)_progress; + progressLabel.Text = $"Updating . . . {_progress}%"; + progressBar.Value = _progress; }; progressLabel.Text = "Updating . . . "; - // do update - OnLoad(); + cancellation = new(); + bool success = true; + PackagePath.DeleteFile(true); + await using Stream update = PackagePath.CreateFile(true); + bool retry = true; + try + { + if (cancellation is null || Program.Canceled) + throw new TaskCanceledException(); + using HttpResponseMessage response = await HttpClientManager.HttpClient.GetAsync(latestRelease.Asset.BrowserDownloadUrl, + HttpCompletionOption.ResponseHeadersRead, cancellation.Token); + response.EnsureSuccessStatusCode(); + if (cancellation is null || Program.Canceled) + throw new TaskCanceledException(); + await using Stream download = await response.Content.ReadAsStreamAsync(cancellation.Token); + double bytes = latestRelease.Asset.Size; + byte[] buffer = new byte[16384]; + long bytesRead = 0; + int newBytes; + while (cancellation is not null && !Program.Canceled + && (newBytes = await download.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellation.Token)) != 0) + { + if (cancellation is null || Program.Canceled) + throw new TaskCanceledException(); + await update.WriteAsync(buffer.AsMemory(0, newBytes), cancellation.Token); + bytesRead += newBytes; + int report = (int)(bytesRead / bytes * 100); + if (report <= progressBar.Value) + continue; + iProgress.Report(report); + } + iProgress.Report((int)(bytesRead / bytes * 100)); + if (cancellation is null || Program.Canceled) + throw new TaskCanceledException(); + } + catch (TaskCanceledException) + { + success = false; + } + catch (Exception ex) + { + retry = ex.HandleException(this, Program.Name + " encountered an exception while updating"); + success = false; + } + cancellation?.Dispose(); + cancellation = null; + await update.DisposeAsync(); + bool canContinue = success && !Program.Canceled; + if (canContinue) + updateButton.Enabled = false; + ExecutablePath.DeleteFile(canContinue); + if (canContinue) + await Task.Run(() => PackagePath.ExtractZip(ProgramData.DirectoryPath, true, this)); + PackagePath.DeleteFile(canContinue); + if (canContinue) + { + string currentPath = Program.CurrentProcessFilePath; + string currentDirectory = Path.GetDirectoryName(currentPath); + string properExecutable = Path.GetFileName(ExecutablePath); + string properExecutablePath = Path.Combine(currentDirectory!, properExecutable!); + StringBuilder commands = new(); + commands.AppendLine(CultureInfo.InvariantCulture, $"\nTASKKILL /F /T /PID {Program.CurrentProcessId}"); + commands.AppendLine(CultureInfo.InvariantCulture, $":LOOP"); + commands.AppendLine(CultureInfo.InvariantCulture, $"TASKLIST | FIND \"{Program.CurrentProcessId}\""); + commands.AppendLine(CultureInfo.InvariantCulture, $"IF NOT ERRORLEVEL 1 ("); + commands.AppendLine(CultureInfo.InvariantCulture, $" TIMEOUT /T 1"); + commands.AppendLine(CultureInfo.InvariantCulture, $" GOTO LOOP"); + commands.AppendLine(CultureInfo.InvariantCulture, $")"); + commands.AppendLine(CultureInfo.InvariantCulture, $"DEL /F /Q \"{currentPath}\""); + commands.AppendLine(CultureInfo.InvariantCulture, $"DEL /F /Q \"{properExecutablePath}\""); + commands.AppendLine(CultureInfo.InvariantCulture, $"MOVE /Y \"{ExecutablePath}\" \"{properExecutablePath}\""); + commands.AppendLine(CultureInfo.InvariantCulture, $"START \"\" /D \"{currentDirectory}\" \"{properExecutable}\""); + commands.AppendLine(CultureInfo.InvariantCulture, $"EXIT"); + UpdaterPath.WriteFile(commands.ToString(), true, this); + Process process = new(); + ProcessStartInfo startInfo = new() + { + WorkingDirectory = ProgramData.DirectoryPath, FileName = "cmd.exe", Arguments = $"/C START \"UPDATER\" /B {Path.GetFileName(UpdaterPath)}", + CreateNoWindow = true + }; + process.StartInfo = startInfo; + process.Start(); + return; + } + if (!retry) + StartProgram(); + else + OnLoad(); } private void OnUpdateCancel(object sender, EventArgs e) { - // cancel update + cancellation?.Cancel(); + cancellation?.Dispose(); + cancellation = null; } protected override void Dispose(bool disposing) @@ -148,5 +237,6 @@ internal sealed partial class UpdateForm : CustomForm if (disposing) components?.Dispose(); base.Dispose(disposing); + OnUpdateCancel(null, null); } } \ No newline at end of file diff --git a/CreamInstaller/Program.cs b/CreamInstaller/Program.cs index 7f24d2f..4f96ec0 100644 --- a/CreamInstaller/Program.cs +++ b/CreamInstaller/Program.cs @@ -2,7 +2,6 @@ using System; using System.Diagnostics; using System.Drawing; using System.Linq; -using System.Reflection; using System.Threading; using System.Windows.Forms; using CreamInstaller.Forms; @@ -20,19 +19,18 @@ internal static class Program internal const string RepositoryOwner = "pointfeev"; internal static readonly string RepositoryName = Name; internal static readonly string RepositoryPackage = Name + ".zip"; + internal static readonly string RepositoryExecutable = Name + ".exe"; #if DEBUG internal static readonly string ApplicationName = Name + " v" + Version + "-debug: " + Description; internal static readonly string ApplicationNameShort = Name + " v" + Version + "-debug"; - internal static readonly string ApplicationExecutable = Name + "-debug.exe"; // should be the same as in .csproj #else internal static readonly string ApplicationName = Name + " v" + Version + ": " + Description; internal static readonly string ApplicationNameShort = Name + " v" + Version; - internal static readonly string ApplicationExecutable = Name + ".exe"; // should be the same as in .csproj #endif - internal static readonly Assembly EntryAssembly = Assembly.GetEntryAssembly(); private static readonly Process CurrentProcess = Process.GetCurrentProcess(); internal static readonly string CurrentProcessFilePath = CurrentProcess.MainModule?.FileName; + internal static readonly int CurrentProcessId = CurrentProcess.Id; internal static bool BlockProtectedGames = true; internal static readonly string[] ProtectedGames = { "PAYDAY 2" }; diff --git a/CreamInstaller/Release.cs b/CreamInstaller/ProgramRelease.cs similarity index 84% rename from CreamInstaller/Release.cs rename to CreamInstaller/ProgramRelease.cs index 3e4e8de..c2851b5 100644 --- a/CreamInstaller/Release.cs +++ b/CreamInstaller/ProgramRelease.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; namespace CreamInstaller; -public class Release +public class ProgramRelease { + private Asset asset; + private string[] changes; private Version version; @@ -28,6 +31,8 @@ public class Release [JsonProperty("body", NullValueHandling = NullValueHandling.Ignore)] public string Body { get; set; } + public Asset Asset => asset ??= Assets.FirstOrDefault(a => a.Name == Program.RepositoryPackage); + public Version Version => version ??= new(TagName[1..]); public string[] Changes => changes ??= Body.Replace("- ", "").Split("\r\n"); @@ -38,12 +43,6 @@ public class Asset [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } - [JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)] - public string ContentType { get; set; } - - [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] - public string State { get; set; } - [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] public int Size { get; set; } diff --git a/CreamInstaller/Resources/Koaloader.cs b/CreamInstaller/Resources/Koaloader.cs index 650a17f..da1311c 100644 --- a/CreamInstaller/Resources/Koaloader.cs +++ b/CreamInstaller/Resources/Koaloader.cs @@ -79,7 +79,7 @@ internal static class Koaloader { /*if (installForm is not null) installForm.UpdateUser("Generating Koaloader configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/ - config.CreateFile(true, installForm); + config.CreateFile(true, installForm).Close(); StreamWriter writer = new(config, true, Encoding.UTF8); WriteConfig(writer, targets, modules, installForm); writer.Flush(); diff --git a/CreamInstaller/Resources/Resources.cs b/CreamInstaller/Resources/Resources.cs index a2ebbb9..769f961 100644 --- a/CreamInstaller/Resources/Resources.cs +++ b/CreamInstaller/Resources/Resources.cs @@ -458,9 +458,9 @@ internal static class Resources resource?.CopyTo(file); break; } - catch + catch (Exception e) { - if (filePath.IOWarn("Failed to write a crucial manifest resource (" + resourceIdentifier + ")") is not DialogResult.OK) + if (filePath.IOWarn("Failed to write a crucial manifest resource (" + resourceIdentifier + ")", e) is not DialogResult.OK) break; } } @@ -474,9 +474,9 @@ internal static class Resources fileStream.Write(resource); break; } - catch + catch (Exception e) { - if (filePath.IOWarn("Failed to write a crucial resource") is not DialogResult.OK) + if (filePath.IOWarn("Failed to write a crucial resource", e) is not DialogResult.OK) break; } } diff --git a/CreamInstaller/Resources/ScreamAPI.cs b/CreamInstaller/Resources/ScreamAPI.cs index 95fd015..2daaf93 100644 --- a/CreamInstaller/Resources/ScreamAPI.cs +++ b/CreamInstaller/Resources/ScreamAPI.cs @@ -39,7 +39,7 @@ internal static class ScreamAPI { /*if (installForm is not null) installForm.UpdateUser("Generating ScreamAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/ - config.CreateFile(true, installForm); + config.CreateFile(true, installForm).Close(); StreamWriter writer = new(config, true, Encoding.UTF8); WriteConfig(writer, new(overrideCatalogItems.ToDictionary(pair => pair.Key, pair => pair.Value), PlatformIdComparer.String), new(entitlements.ToDictionary(pair => pair.Key, pair => pair.Value), PlatformIdComparer.String), installForm); diff --git a/CreamInstaller/Resources/SmokeAPI.cs b/CreamInstaller/Resources/SmokeAPI.cs index 185a22c..830ffbe 100644 --- a/CreamInstaller/Resources/SmokeAPI.cs +++ b/CreamInstaller/Resources/SmokeAPI.cs @@ -60,7 +60,7 @@ internal static class SmokeAPI { /*if (installForm is not null) installForm.UpdateUser("Generating SmokeAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/ - config.CreateFile(true, installForm); + config.CreateFile(true, installForm).Close(); StreamWriter writer = new(config, true, Encoding.UTF8); WriteConfig(writer, selection.Id, new(extraApps.ToDictionary(pair => pair.Key, pair => pair.Value), PlatformIdComparer.String), new(overrideDlc.ToDictionary(pair => pair.Key, pair => pair.Value), PlatformIdComparer.String), diff --git a/CreamInstaller/Resources/UplayR1.cs b/CreamInstaller/Resources/UplayR1.cs index 026a571..e79cf08 100644 --- a/CreamInstaller/Resources/UplayR1.cs +++ b/CreamInstaller/Resources/UplayR1.cs @@ -33,7 +33,7 @@ internal static class UplayR1 { /*if (installForm is not null) installForm.UpdateUser("Generating Uplay R1 Unlocker configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/ - config.CreateFile(true, installForm); + config.CreateFile(true, installForm).Close(); StreamWriter writer = new(config, true, Encoding.UTF8); WriteConfig(writer, new(blacklistDlc.ToDictionary(pair => pair.Key, pair => pair.Value), PlatformIdComparer.String), installForm); writer.Flush(); diff --git a/CreamInstaller/Resources/UplayR2.cs b/CreamInstaller/Resources/UplayR2.cs index 1099fd3..688da87 100644 --- a/CreamInstaller/Resources/UplayR2.cs +++ b/CreamInstaller/Resources/UplayR2.cs @@ -35,7 +35,7 @@ internal static class UplayR2 { /*if (installForm is not null) installForm.UpdateUser("Generating Uplay R2 Unlocker configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/ - config.CreateFile(true, installForm); + config.CreateFile(true, installForm).Close(); StreamWriter writer = new(config, true, Encoding.UTF8); WriteConfig(writer, new(blacklistDlc.ToDictionary(pair => pair.Key, pair => pair.Value), PlatformIdComparer.String), installForm); writer.Flush(); diff --git a/CreamInstaller/Utility/ExceptionHandler.cs b/CreamInstaller/Utility/ExceptionHandler.cs index 6e7db74..307c1e4 100644 --- a/CreamInstaller/Utility/ExceptionHandler.cs +++ b/CreamInstaller/Utility/ExceptionHandler.cs @@ -8,10 +8,8 @@ namespace CreamInstaller.Utility; internal static class ExceptionHandler { - internal static bool HandleException(this Exception e, Form form = null, string caption = null, string acceptButtonText = "Retry", - string cancelButtonText = "Cancel") + internal static string FormatException(this Exception e) { - caption ??= Program.Name + " encountered an exception"; StringBuilder output = new(); int stackDepth = 0; while (e is not null) @@ -41,7 +39,14 @@ internal static class ExceptionHandler e = e.InnerException; stackDepth++; } - string outputString = output.ToString(); + return output.ToString(); + } + + internal static bool HandleException(this Exception e, Form form = null, string caption = null, string acceptButtonText = "Retry", + string cancelButtonText = "Cancel") + { + caption ??= Program.Name + " encountered an exception"; + string outputString = e.FormatException(); if (string.IsNullOrWhiteSpace(outputString)) outputString = e?.ToString() ?? "Unknown exception"; using DialogForm dialogForm = new(form ?? Form.ActiveForm); diff --git a/CreamInstaller/Utility/SafeIO.cs b/CreamInstaller/Utility/SafeIO.cs index 4712866..5b5b2cf 100644 --- a/CreamInstaller/Utility/SafeIO.cs +++ b/CreamInstaller/Utility/SafeIO.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Drawing; using System.IO; using System.IO.Compression; @@ -38,9 +39,9 @@ internal static class SafeIO Directory.CreateDirectory(directoryPath); break; } - catch + catch (Exception e) { - if (!crucial || directoryPath.DirectoryExists() || directoryPath.IOWarn("Failed to create a crucial directory", form) is not DialogResult.OK) + if (!crucial || directoryPath.DirectoryExists() || directoryPath.IOWarn("Failed to create a crucial directory", e, form) is not DialogResult.OK) break; } } @@ -55,9 +56,9 @@ internal static class SafeIO Directory.Move(directoryPath, newDirectoryPath); break; } - catch + catch (Exception e) { - if (!crucial || !directoryPath.DirectoryExists() || directoryPath.IOWarn("Failed to move a crucial directory", form) is not DialogResult.OK) + if (!crucial || !directoryPath.DirectoryExists() || directoryPath.IOWarn("Failed to move a crucial directory", e, form) is not DialogResult.OK) break; } } @@ -72,9 +73,10 @@ internal static class SafeIO Directory.Delete(directoryPath, true); break; } - catch + catch (Exception e) { - if (!crucial || !directoryPath.DirectoryExists() || directoryPath.IOWarn("Failed to delete a crucial directory", form) is not DialogResult.OK) + if (!crucial || !directoryPath.DirectoryExists() + || directoryPath.IOWarn("Failed to delete a crucial directory", e, form) is not DialogResult.OK) break; } } @@ -91,10 +93,10 @@ internal static class SafeIO ? Directory.EnumerateFiles(directoryPath, filePattern, new EnumerationOptions { RecurseSubdirectories = true }) : Directory.EnumerateFiles(directoryPath, filePattern); } - catch + catch (Exception e) { if (!crucial || !directoryPath.DirectoryExists() - || directoryPath.IOWarn("Failed to enumerate a crucial directory's files", form) is not DialogResult.OK) + || directoryPath.IOWarn("Failed to enumerate a crucial directory's files", e, form) is not DialogResult.OK) break; } return Enumerable.Empty(); @@ -112,10 +114,10 @@ internal static class SafeIO ? Directory.EnumerateDirectories(directoryPath, directoryPattern, new EnumerationOptions { RecurseSubdirectories = true }) : Directory.EnumerateDirectories(directoryPath, directoryPattern); } - catch + catch (Exception e) { if (!crucial || !directoryPath.DirectoryExists() - || directoryPath.IOWarn("Failed to enumerate a crucial directory's subdirectories", form) is not DialogResult.OK) + || directoryPath.IOWarn("Failed to enumerate a crucial directory's subdirectories", e, form) is not DialogResult.OK) break; } return Enumerable.Empty(); @@ -123,19 +125,19 @@ internal static class SafeIO internal static bool FileExists(this string filePath) => File.Exists(filePath); - internal static void CreateFile(this string filePath, bool crucial = false, Form form = null) + internal static FileStream CreateFile(this string filePath, bool crucial = false, Form form = null) { while (!Program.Canceled) try { - File.Create(filePath).Close(); - break; + return File.Create(filePath); } - catch + catch (Exception e) { - if (!crucial || filePath.IOWarn("Failed to create a crucial file", form) is not DialogResult.OK) + if (!crucial || filePath.IOWarn("Failed to create a crucial file", e, form) is not DialogResult.OK) break; } + return null; } internal static void MoveFile(this string filePath, string newFilePath, bool crucial = false, Form form = null) @@ -148,9 +150,9 @@ internal static class SafeIO File.Move(filePath, newFilePath); break; } - catch + catch (Exception e) { - if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to move a crucial file", form) is not DialogResult.OK) + if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to move a crucial file", e, form) is not DialogResult.OK) break; } } @@ -165,9 +167,9 @@ internal static class SafeIO File.Delete(filePath); break; } - catch + catch (Exception e) { - if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to delete a crucial file", form) is not DialogResult.OK) + if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to delete a crucial file", e, form) is not DialogResult.OK) break; } } @@ -181,9 +183,9 @@ internal static class SafeIO { return File.ReadAllText(filePath, Encoding.UTF8); } - catch + catch (Exception e) { - if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to read a crucial file", form) is not DialogResult.OK) + if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to read a crucial file", e, form) is not DialogResult.OK) break; } return null; @@ -198,9 +200,9 @@ internal static class SafeIO { return File.ReadAllBytes(filePath); } - catch + catch (Exception e) { - if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to read a crucial file", form) is not DialogResult.OK) + if (!crucial || !filePath.FileExists() || filePath.IOWarn("Failed to read a crucial file", e, form) is not DialogResult.OK) break; } return null; @@ -214,9 +216,9 @@ internal static class SafeIO File.WriteAllText(filePath, text, Encoding.UTF8); break; } - catch + catch (Exception e) { - if (!crucial || filePath.IOWarn("Failed to write a crucial file", form) is not DialogResult.OK) + if (!crucial || filePath.IOWarn("Failed to write a crucial file", e, form) is not DialogResult.OK) break; } } @@ -231,16 +233,24 @@ internal static class SafeIO ZipFile.ExtractToDirectory(archivePath, destinationPath); break; } - catch + catch (Exception e) { - if (!crucial || !archivePath.FileExists() || archivePath.IOWarn("Failed to extract a crucial zip file", form) is not DialogResult.OK) + if (!crucial || !archivePath.FileExists() || archivePath.IOWarn("Failed to extract a crucial zip file", e, form) is not DialogResult.OK) break; } } - internal static DialogResult IOWarn(this string filePath, string message, Form form = null) + internal static DialogResult IOWarn(this string filePath, string message, Exception e, Form form = null) { - using DialogForm dialogForm = new(form ?? Form.ActiveForm); - return dialogForm.Show(SystemIcons.Warning, message + ": " + filePath.BeautifyPath(), "Retry", "OK"); + form ??= Form.ActiveForm; + if (form is null || !form.InvokeRequired) + return filePath.IOWarnInternal(message, e, form); + return form.Invoke(() => filePath.IOWarnInternal(message, e, form)); + } + + private static DialogResult IOWarnInternal(this string filePath, string message, Exception e, Form form = null) + { + using DialogForm dialogForm = new(form); + return dialogForm.Show(SystemIcons.Warning, message + ": " + filePath.BeautifyPath() + "\n\n" + e.FormatException(), "Retry", "OK"); } } \ No newline at end of file diff --git a/preview.png b/preview.png index 4d45881..afc1fab 100644 Binary files a/preview.png and b/preview.png differ