From 063e317ea775e7f4461950b58e9aeb9f4692ba9d Mon Sep 17 00:00:00 2001 From: tombogle Date: Thu, 28 Mar 2024 11:46:15 -0400 Subject: [PATCH 1/2] Checked for disposed log box or caller-requested cancel in SafeInvoke so we don't try to write messages or scroll. --- CHANGELOG.md | 1 + .../Progress/LogBox/LogBoxFormForTest.cs | 12 ++--- .../Progress/LogBox/LogBoxTests.cs | 51 +++++++++++++++++++ SIL.Windows.Forms/Progress/LogBox.cs | 12 ++--- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aeb7d97e..9a04449eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - [SIL.Core] Fixed bug in extension method GetLongestUsefulCommonSubstring when string ends with an Object replacement character +- [SIL.Core] LogBox: Checked for disposed log box or caller-requested cancel in SafeInvoke so we don't try to write messages or scroll. ## [13.0.0] - 2023-12-07 diff --git a/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxFormForTest.cs b/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxFormForTest.cs index e8dc1b009..cdda0157a 100644 --- a/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxFormForTest.cs +++ b/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxFormForTest.cs @@ -6,25 +6,19 @@ namespace SIL.Windows.Forms.Tests.Progress.LogBox public class LogBoxFormForTest : IDisposable { private Form _window; - private Windows.Forms.Progress.LogBox _box; + private Forms.Progress.LogBox _box; public LogBoxFormForTest () { CreateWindow(); } - public Windows.Forms.Progress.LogBox progress - { - get - { - return _box; - } - } + public Forms.Progress.LogBox progress => _box; private void CreateWindow() { _window = new Form(); - _box = new Windows.Forms.Progress.LogBox(); + _box = new Forms.Progress.LogBox(); _box.Dock = DockStyle.Fill; _window.Controls.Add(_box); diff --git a/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs b/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs index 5cd4baa01..d04755671 100644 --- a/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs +++ b/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs @@ -31,6 +31,57 @@ public void ShowLogBox() } } + [Test] + [Category("SkipOnTeamCity")] + public void WriteError_Disposed_IgnoreRequestsToWriteMessages() + { + Console.WriteLine("Showing LogBox"); + using (var e = new LogBoxFormForTest()) + { + progress = e.progress; + progress.WriteMessage("Hi, Mom!"); + progress.Dispose(); + progress.WriteError("Ignore this."); + Assert.IsTrue(progress.ErrorEncountered); + Assert.IsFalse(progress.Rtf.Contains("Ignore this.")); + Assert.IsTrue(progress.Rtf.Contains("Hi, Mom!")); + } + } + + [Test] + [Category("SkipOnTeamCity")] + public void WriteMessage_CancelRequested_IgnoreRequestsToWriteMessages() + { + Console.WriteLine("Showing LogBox"); + using (var e = new LogBoxFormForTest()) + { + progress = e.progress; + progress.WriteMessage("Hi, Mom!"); + progress.CancelRequested = true; + progress.WriteMessage("Ignore this."); + Assert.IsFalse(progress.ErrorEncountered); + Assert.IsFalse(progress.Rtf.Contains("Ignore this.")); + Assert.IsTrue(progress.Rtf.Contains("Hi, Mom!")); + } + } + + [Test] + [Category("SkipOnTeamCity")] + public void WriteError_CancelRequested_IgnoreRequestsToWriteMessages() + { + Console.WriteLine("Showing LogBox"); + using (var e = new LogBoxFormForTest()) + { + progress = e.progress; + progress.WriteMessage("Hi, Mom!"); + progress.CancelRequested = true; + progress.WriteError("User cancelled operation."); + Assert.IsTrue(progress.ErrorEncountered); + Assert.IsFalse(progress.Rtf.Contains("User cancelled operation.")); + Assert.IsTrue(progress.Rtf.Contains("Hi, Mom!")); + } + } + [Test] [Explicit] [Category("LongRunning")] diff --git a/SIL.Windows.Forms/Progress/LogBox.cs b/SIL.Windows.Forms/Progress/LogBox.cs index a373eba7d..46298898e 100644 --- a/SIL.Windows.Forms/Progress/LogBox.cs +++ b/SIL.Windows.Forms/Progress/LogBox.cs @@ -286,6 +286,8 @@ public void WriteMessage(string message, params object[] args) /// private void SafeInvoke(Control box, Action action) { + if (box.IsDisposed || box.Disposing || CancelRequested) + return; box.SafeInvoke(action, errorHandling:ControlExtensions.ErrorHandlingAction.IgnoreAll); } @@ -418,15 +420,11 @@ private void WriteErrorInternal(string message, params object[] args) { Write(Color.Red, _box.Font.Style, message, args); - // There is no Invoke method on ToolStripItems (which the _reportLink is) - // and setting it to visible from another thread seems to work okay. - _reportLink.Visible = true; - - menuStrip1.Invoke(new Action(() => + SafeInvoke(menuStrip1, () => { + _reportLink.Visible = true; menuStrip1.Visible = true; - })); - + }); ErrorEncountered = true; } From 37f55d3b7dd8ca64afad1612db6c77adafe125d2 Mon Sep 17 00:00:00 2001 From: tombogle Date: Thu, 4 Apr 2024 11:36:25 -0400 Subject: [PATCH 2/2] Changed RaiseExceptionIfFailed to not throw an exception if user cancelled Backed out/adjusted previous change to check for disposed log box or caller-requested cancel in SafeInvoke Fixed some typos and comments --- CHANGELOG.md | 3 + Palaso.sln.DotSettings | 1 + .../CommandLineRunner.cs | 55 ++++++++++++------- SIL.Core/IO/PathHelper.cs | 6 +- .../Progress/LogBox/LogBoxTests.cs | 10 ++-- .../Extensions/ControlExtensions.cs | 4 +- SIL.Windows.Forms/Progress/LogBox.cs | 2 - 7 files changed, 51 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a04449eb..20b3d9524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Changed + - [SIL.Archiving] Upgraded to L10nSharp 7.0.0 - [SIL.Windows.Forms] Upgraded to L10nSharp 7.0.0 - [SIL.Windows.Forms.DblBundle] Upgraded to L10nSharp 7.0.0 - [SIL.Windows.Forms.Keyboarding] Upgraded to L10nSharp 7.0.0 - [SIL.Windows.Forms.WritingSystems] Upgraded to L10nSharp 7.0.0 +- [SIL.Core] `RaiseExceptionIfFailed` no longer throws an exception if user cancelled ## [13.0.1] - 2024-01-09 diff --git a/Palaso.sln.DotSettings b/Palaso.sln.DotSettings index 01cb9abbf..b55cbf111 100644 --- a/Palaso.sln.DotSettings +++ b/Palaso.sln.DotSettings @@ -31,6 +31,7 @@ True True True + True True True True diff --git a/SIL.Core/CommandLineProcessing/CommandLineRunner.cs b/SIL.Core/CommandLineProcessing/CommandLineRunner.cs index dc2231215..df02ee518 100644 --- a/SIL.Core/CommandLineProcessing/CommandLineRunner.cs +++ b/SIL.Core/CommandLineProcessing/CommandLineRunner.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2018 SIL International +// Copyright (c) 2024 SIL International // This software is licensed under the MIT License (http://opensource.org/licenses/MIT) using System; @@ -6,9 +6,11 @@ using System.Globalization; using System.IO; using System.Text; +using JetBrains.Annotations; using SIL.IO; using SIL.PlatformUtilities; using SIL.Progress; +using static System.String; namespace SIL.CommandLineProcessing { @@ -23,7 +25,7 @@ public class CommandLineRunner private Process _process; /// - /// This one doesn't attemtp to influence the encoding used + /// This one doesn't attempt to influence the encoding used /// public static ExecutionResult Run(string exePath, string arguments, string fromDirectory, int secondsBeforeTimeOut, IProgress progress) { @@ -102,9 +104,11 @@ public ExecutionResult Start(string exePath, string arguments, Encoding encoding exePath = "mono"; } progress.WriteVerbose("running '{0} {1}' from '{2}'", exePath, arguments, fromDirectory); - ExecutionResult result = new ExecutionResult(); - result.Arguments = arguments; - result.ExePath = exePath; + ExecutionResult result = new ExecutionResult + { + Arguments = arguments, + ExePath = exePath + }; using (_process = new Process()) { @@ -191,19 +195,24 @@ public ExecutionResult Start(string exePath, string arguments, Encoding encoding } /// - /// On Windows, We can't get unicode over the command-line barrier, so - /// instead create 8.3 filename, which, happily, will have no non-english characters - /// for any part of the path. This is safe to call from Linux, too + /// On Windows, We can't get Unicode over the command-line barrier, so + /// instead create 8.3 filename, which, happily, will have no non-English characters + /// for any part of the path. This is safe to call from Linux, too. /// + [PublicAPI] public static string MakePathToFileSafeFromEncodingProblems(string path) { if (Directory.Exists(path)) - throw new ArgumentException(string.Format("MakePathToFileSafeFromEncodingProblems() is only for files, but {0} is a directory.", path)); + { + throw new ArgumentException( + $"MakePathToFileSafeFromEncodingProblems() is only for files, but {path} is a directory."); + } var safe = ""; - //if the filename doesn't exist yet, we can't get the 8.3 name. So we make it, get the name, then delete it. - //NB: this will not yet deal with the problem of creating a directory + // If the filename doesn't exist yet, we can't get the 8.3 name. So we make it, get + // the name, then delete it. + // NB: this will not yet deal with the problem of creating a directory if (!File.Exists(path)) { File.WriteAllText(path, ""); @@ -223,11 +232,12 @@ public static string MakePathToFileSafeFromEncodingProblems(string path) /// instead create 8.3 filename, which, happily, will have no non-english characters /// for any part of the path. This is safe to call from Linux, too /// + [PublicAPI] public static string MakePathToDirectorySafeFromEncodingProblems(string path) { if (File.Exists(path)) throw new ArgumentException( - string.Format( + Format( "MakePathToDirectorySafeFromEncodingProblems() is only for directories, but {0} is a file.", path)); @@ -251,9 +261,10 @@ public static string MakePathToDirectorySafeFromEncodingProblems(string path) /// - /// Some command line applications (e.g. exiftool) can handle html-encoded characters in their command arguments + /// Some command line applications (e.g. exiftool) can handle HTML-encoded characters in their command arguments /// /// From Rick Strahl at http://www.west-wind.com/weblog/posts/2009/Feb/05/Html-and-Uri-String-Encoding-without-SystemWeb + [PublicAPI] public static string HtmlEncodeNonAsciiCharacters(string text) { if (text == null) @@ -289,17 +300,23 @@ public class ExecutionResult public string StandardOutput; public string ExePath; public string Arguments; - public bool DidTimeOut { get { return ExitCode == kTimedOut; } } - public bool UserCancelled { get { return ExitCode == kCancelled; } } + public bool DidTimeOut => ExitCode == kTimedOut; + public bool UserCancelled => ExitCode == kCancelled; + [PublicAPI] public void RaiseExceptionIfFailed(string contextDescription, params object[] originalPath) { - if (ExitCode == 0) + // REVIEW: This method used to raise a regular ApplicationException in the case of + // UserCancelled. If anything, I assume we would want it to raise a + // UserCancelledException, but since the method name says "IfFailed" and the caller + // should always already know if the user cancelled, not sure there is any need to + // raise an exception in this case. + if (ExitCode == 0 || UserCancelled) return; var builder = new StringBuilder(); - builder.AppendLine(string.Format("{0} {1} failed.", ExePath, Arguments)); - builder.AppendLine("In the context of " + string.Format(contextDescription, originalPath)); + builder.AppendLine($"{ExePath} {Arguments} failed."); + builder.AppendLine("In the context of " + Format(contextDescription, originalPath)); builder.AppendLine("StandardError: " + StandardError); builder.AppendLine("StandardOutput: " + StandardOutput); if (DidTimeOut) @@ -308,7 +325,7 @@ public void RaiseExceptionIfFailed(string contextDescription, params object[] or } } - + [PublicAPI] public class UserCancelledException : ApplicationException { public UserCancelledException() diff --git a/SIL.Core/IO/PathHelper.cs b/SIL.Core/IO/PathHelper.cs index e5c8eb310..2c800e8ff 100644 --- a/SIL.Core/IO/PathHelper.cs +++ b/SIL.Core/IO/PathHelper.cs @@ -122,8 +122,10 @@ static extern uint GetShortPathName( uint cchBuffer); /// - /// When calling external exe's on Windows any non-ascii characters can get converted to '?'. This - /// will convert them to 8.3 format which is all ascii (and do nothing on Linux). + /// When calling external executables on Windows any non-ASCII characters can get converted to '?'. This + /// will convert them to 8.3 format which is all ascii (and do nothing on Linux). Note that this + /// is dependent on the drive being able to generate 8.3 filenames, so this is not guaranteed to produce + /// a short name. /// public static string MakePathSafeFromEncodingProblems(string path) { diff --git a/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs b/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs index d04755671..9b392717a 100644 --- a/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs +++ b/SIL.Windows.Forms.Tests/Progress/LogBox/LogBoxTests.cs @@ -50,7 +50,7 @@ public void WriteError_Disposed_IgnoreRequestsToWriteMessages() [Test] [Category("SkipOnTeamCity")] - public void WriteMessage_CancelRequested_IgnoreRequestsToWriteMessages() + public void WriteMessage_CancelRequested_HandleRequestsToWriteMessages() { Console.WriteLine("Showing LogBox"); using (var e = new LogBoxFormForTest()) @@ -58,16 +58,16 @@ public void WriteMessage_CancelRequested_IgnoreRequestsToWriteMessages() progress = e.progress; progress.WriteMessage("Hi, Mom!"); progress.CancelRequested = true; - progress.WriteMessage("Ignore this."); + progress.WriteMessage("Do not ignore this."); Assert.IsFalse(progress.ErrorEncountered); - Assert.IsFalse(progress.Rtf.Contains("Ignore this.")); + Assert.IsTrue(progress.Rtf.Contains("Do not ignore this.")); Assert.IsTrue(progress.Rtf.Contains("Hi, Mom!")); } } [Test] [Category("SkipOnTeamCity")] - public void WriteError_CancelRequested_IgnoreRequestsToWriteMessages() + public void WriteError_CancelRequested_HandleRequestsToWriteMessages() { Console.WriteLine("Showing LogBox"); using (var e = new LogBoxFormForTest()) @@ -77,8 +77,8 @@ public void WriteError_CancelRequested_IgnoreRequestsToWriteMessages() progress.CancelRequested = true; progress.WriteError("User cancelled operation."); Assert.IsTrue(progress.ErrorEncountered); - Assert.IsFalse(progress.Rtf.Contains("User cancelled operation.")); Assert.IsTrue(progress.Rtf.Contains("Hi, Mom!")); + Assert.IsTrue(progress.Rtf.Contains("User cancelled operation.")); } } diff --git a/SIL.Windows.Forms/Extensions/ControlExtensions.cs b/SIL.Windows.Forms/Extensions/ControlExtensions.cs index b236ca00b..08658a759 100644 --- a/SIL.Windows.Forms/Extensions/ControlExtensions.cs +++ b/SIL.Windows.Forms/Extensions/ControlExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Windows.Forms; @@ -129,7 +129,7 @@ public static IAsyncResult SafeInvoke(this Control control, Action action, strin if (!control.InvokeRequired) { - if (control.IsDisposed) + if (control.IsDisposed || control.Disposing) { if (errorHandling == ErrorHandlingAction.Throw) throw new ObjectDisposedException("SafeInvoke called after the control was disposed. (" + nameForErrorReporting + ")"); diff --git a/SIL.Windows.Forms/Progress/LogBox.cs b/SIL.Windows.Forms/Progress/LogBox.cs index 46298898e..dee4fc29e 100644 --- a/SIL.Windows.Forms/Progress/LogBox.cs +++ b/SIL.Windows.Forms/Progress/LogBox.cs @@ -286,8 +286,6 @@ public void WriteMessage(string message, params object[] args) /// private void SafeInvoke(Control box, Action action) { - if (box.IsDisposed || box.Disposing || CancelRequested) - return; box.SafeInvoke(action, errorHandling:ControlExtensions.ErrorHandlingAction.IgnoreAll); }