From 9084ce2eb51e451a16c369d7508c548ca419dbea Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 7 Jun 2014 19:12:45 +0200 Subject: [PATCH] Add JumpToReferenceAsync() overload to allow detecting when the decompilation after the jump has finished. --- ILSpy.BamlDecompiler/BamlResourceEntryNode.cs | 8 +- ILSpy/App.xaml.cs | 8 + ILSpy/Commands/DecompileAllCommand.cs | 2 +- ILSpy/ILSpy.csproj | 1 + ILSpy/MainWindow.xaml.cs | 17 +- ILSpy/TaskHelper.cs | 203 ++++++++++++++++++ ILSpy/TextView/DecompilerTextView.cs | 128 ++++++----- .../ResourceNodes/XamlResourceNode.cs | 5 +- .../ResourceNodes/XmlResourceNode.cs | 5 +- 9 files changed, 312 insertions(+), 65 deletions(-) create mode 100644 ILSpy/TaskHelper.cs diff --git a/ILSpy.BamlDecompiler/BamlResourceEntryNode.cs b/ILSpy.BamlDecompiler/BamlResourceEntryNode.cs index f5395199d..90c40d898 100644 --- a/ILSpy.BamlDecompiler/BamlResourceEntryNode.cs +++ b/ILSpy.BamlDecompiler/BamlResourceEntryNode.cs @@ -26,12 +26,12 @@ namespace ILSpy.BamlDecompiler public override bool View(DecompilerTextView textView) { - AvalonEditTextOutput output = new AvalonEditTextOutput(); IHighlightingDefinition highlighting = null; textView.RunWithCancellation( token => Task.Factory.StartNew( () => { + AvalonEditTextOutput output = new AvalonEditTextOutput(); try { if (LoadBaml(output)) highlighting = HighlightingManager.Instance.GetDefinitionByExtension(".xml"); @@ -39,9 +39,9 @@ namespace ILSpy.BamlDecompiler output.Write(ex.ToString()); } return output; - }, token), - t => textView.ShowNode(t.Result, this, highlighting) - ); + }, token)) + .Then(output => textView.ShowNode(output, this, highlighting)) + .HandleExceptions(); return true; } diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 45baad1d5..1cdb92ce7 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -23,6 +23,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using System.Windows; using System.Windows.Documents; using System.Windows.Navigation; @@ -92,6 +93,7 @@ namespace ICSharpCode.ILSpy AppDomain.CurrentDomain.UnhandledException += ShowErrorBox; Dispatcher.CurrentDispatcher.UnhandledException += Dispatcher_UnhandledException; } + TaskScheduler.UnobservedTaskException += DotNet40_UnobservedTaskException; EventManager.RegisterClassHandler(typeof(Window), Hyperlink.RequestNavigateEvent, @@ -111,6 +113,12 @@ namespace ICSharpCode.ILSpy return argument; } } + + void DotNet40_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + // On .NET 4.0, an unobserved exception in a task terminates the process unless we mark it as observed + e.SetObserved(); + } #region Exception Handling static void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) diff --git a/ILSpy/Commands/DecompileAllCommand.cs b/ILSpy/Commands/DecompileAllCommand.cs index 854ff01a6..2de1c24ef 100644 --- a/ILSpy/Commands/DecompileAllCommand.cs +++ b/ILSpy/Commands/DecompileAllCommand.cs @@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy } }); return output; - }, ct), task => MainWindow.Instance.TextView.ShowText(task.Result)); + }, ct)).Then(output => MainWindow.Instance.TextView.ShowText(output)).HandleExceptions(); } } } diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index fd49ddb07..e46579fda 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -177,6 +177,7 @@ Code + diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index b53cc3c79..78400b727 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -558,6 +558,19 @@ namespace ICSharpCode.ILSpy public void JumpToReference(object reference) { + JumpToReferenceAsync(reference).HandleExceptions(); + } + + /// + /// Jumps to the specified reference. + /// + /// + /// Returns a task that will signal completion when the decompilation of the jump target has finished. + /// The task will be marked as canceled if the decompilation is canceled. + /// + public Task JumpToReferenceAsync(object reference) + { + decompilationTask = TaskHelper.CompletedTask; ILSpyTreeNode treeNode = FindTreeNode(reference); if (treeNode != null) { SelectNode(treeNode); @@ -569,6 +582,7 @@ namespace ICSharpCode.ILSpy } } + return decompilationTask; } #endregion @@ -627,6 +641,7 @@ namespace ICSharpCode.ILSpy DecompileSelectedNodes(); } + Task decompilationTask; bool ignoreDecompilationRequests; void DecompileSelectedNodes(DecompilerTextViewState state = null, bool recordHistory = true) @@ -646,7 +661,7 @@ namespace ICSharpCode.ILSpy if (node != null && node.View(decompilerTextView)) return; } - decompilerTextView.Decompile(this.CurrentLanguage, this.SelectedNodes, new DecompilationOptions() { TextViewState = state }); + decompilationTask = decompilerTextView.DecompileAsync(this.CurrentLanguage, this.SelectedNodes, new DecompilationOptions() { TextViewState = state }); } void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) diff --git a/ILSpy/TaskHelper.cs b/ILSpy/TaskHelper.cs new file mode 100644 index 000000000..2bbb5878e --- /dev/null +++ b/ILSpy/TaskHelper.cs @@ -0,0 +1,203 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; +using ICSharpCode.ILSpy.TextView; + +namespace ICSharpCode.ILSpy +{ + public static class TaskHelper + { + public static readonly Task CompletedTask = FromResult(null); + + public static Task FromResult(T result) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(result); + return tcs.Task; + } + + public static Task FromException(Exception ex) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(ex); + return tcs.Task; + } + + public static Task FromCancellation() + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } + + /// + /// Sets the result of the TaskCompletionSource based on the result of the finished task. + /// + public static void SetFromTask(this TaskCompletionSource tcs, Task task) + { + switch (task.Status) { + case TaskStatus.RanToCompletion: + tcs.SetResult(task.Result); + break; + case TaskStatus.Canceled: + tcs.SetCanceled(); + break; + case TaskStatus.Faulted: + tcs.SetException(task.Exception.InnerExceptions); + break; + default: + throw new InvalidOperationException("The input task must have already finished"); + } + } + + /// + /// Sets the result of the TaskCompletionSource based on the result of the finished task. + /// + public static void SetFromTask(this TaskCompletionSource tcs, Task task) + { + switch (task.Status) { + case TaskStatus.RanToCompletion: + tcs.SetResult(null); + break; + case TaskStatus.Canceled: + tcs.SetCanceled(); + break; + case TaskStatus.Faulted: + tcs.SetException(task.Exception.InnerExceptions); + break; + default: + throw new InvalidOperationException("The input task must have already finished"); + } + } + + public static Task Then(this Task task, Action action) + { + if (action == null) + throw new ArgumentNullException("action"); + return task.ContinueWith(t => action(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + } + + public static Task Then(this Task task, Func func) + { + if (func == null) + throw new ArgumentNullException("func"); + return task.ContinueWith(t => func(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + } + + public static Task Then(this Task task, Func asyncFunc) + { + if (asyncFunc == null) + throw new ArgumentNullException("asyncFunc"); + return task.ContinueWith(t => asyncFunc(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); + } + + public static Task Then(this Task task, Func> asyncFunc) + { + if (asyncFunc == null) + throw new ArgumentNullException("asyncFunc"); + return task.ContinueWith(t => asyncFunc(t.Result), CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); + } + + public static Task Then(this Task task, Action action) + { + if (action == null) + throw new ArgumentNullException("action"); + return task.ContinueWith(t => { + t.Wait(); + action(); + }, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + } + + public static Task Then(this Task task, Func func) + { + if (func == null) + throw new ArgumentNullException("func"); + return task.ContinueWith(t => { + t.Wait(); + return func(); + }, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + } + + public static Task Then(this Task task, Func asyncAction) + { + if (asyncAction == null) + throw new ArgumentNullException("asyncAction"); + return task.ContinueWith(t => { + t.Wait(); + return asyncAction(); + }, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); + } + + public static Task Then(this Task task, Func> asyncFunc) + { + if (asyncFunc == null) + throw new ArgumentNullException("asyncFunc"); + return task.ContinueWith(t => { + t.Wait(); + return asyncFunc(); + }, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); + } + + /// + /// If the input task fails, calls the action to handle the error. + /// + /// + /// Returns a task that finishes successfully when error handling has completed. + /// If the input task ran successfully, the returned task completes successfully. + /// If the input task was cancelled, the returned task is cancelled as well. + /// + public static Task Catch(this Task task, Action action) where TException : Exception + { + if (action == null) + throw new ArgumentNullException("action"); + return task.ContinueWith(t => { + if (t.IsFaulted) { + Exception ex = t.Exception; + while (ex is AggregateException) + ex = ex.InnerException; + if (ex is TException) + action((TException)ex); + else + throw t.Exception; + } + }, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + } + + /// + /// Ignore exceptions thrown by the task. + /// + public static void IgnoreExceptions(this Task task) + { + } + + /// + /// Handle exceptions by displaying the error message in the text view. + /// + public static void HandleExceptions(this Task task) + { + task.Catch(exception => MainWindow.Instance.Dispatcher.BeginInvoke(new Action(delegate { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + output.Write(exception.ToString()); + MainWindow.Instance.TextView.ShowText(output); + }))).IgnoreExceptions(); + } + } +} diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 025d28904..19d175096 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -210,7 +210,18 @@ namespace ICSharpCode.ILSpy.TextView /// When the task is cancelled before completing, the callback is not called; and any result /// of the task (including exceptions) are ignored. /// + [Obsolete("RunWithCancellation(taskCreation).ContinueWith(taskCompleted) instead")] public void RunWithCancellation(Func> taskCreation, Action> taskCompleted) + { + RunWithCancellation(taskCreation).ContinueWith(taskCompleted, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + } + + /// + /// Switches the GUI into "waiting" mode, then calls to create + /// the task. + /// If another task is started before the previous task finishes running, the previous task is cancelled. + /// + public Task RunWithCancellation(Func> taskCreation) { if (waitAdorner.Visibility != Visibility.Visible) { waitAdorner.Visibility = Visibility.Visible; @@ -223,7 +234,15 @@ namespace ICSharpCode.ILSpy.TextView if (previousCancellationTokenSource != null) previousCancellationTokenSource.Cancel(); - var task = taskCreation(myCancellationTokenSource.Token); + var tcs = new TaskCompletionSource(); + Task task; + try { + task = taskCreation(myCancellationTokenSource.Token); + } catch (OperationCanceledException) { + task = TaskHelper.FromCancellation(); + } catch (Exception ex) { + task = TaskHelper.FromException(ex); + } Action continuation = delegate { try { if (currentCancellationTokenSource == myCancellationTokenSource) { @@ -233,21 +252,17 @@ namespace ICSharpCode.ILSpy.TextView AvalonEditTextOutput output = new AvalonEditTextOutput(); output.WriteLine("The operation was canceled."); ShowOutput(output); - } else { - taskCompleted(task); } + tcs.SetFromTask(task); } else { - try { - task.Wait(); - } catch (AggregateException) { - // observe the exception (otherwise the task's finalizer will shut down the AppDomain) - } + tcs.SetCanceled(); } } finally { myCancellationTokenSource.Dispose(); } }; task.ContinueWith(delegate { Dispatcher.BeginInvoke(DispatcherPriority.Normal, continuation); }); + return tcs.Task; } void cancelButton_Click(object sender, RoutedEventArgs e) @@ -281,7 +296,11 @@ namespace ICSharpCode.ILSpy.TextView currentCancellationTokenSource.Cancel(); currentCancellationTokenSource = null; // prevent canceled task from producing output } - this.nextDecompilationRun = null; // remove scheduled decompilation run + if (this.nextDecompilationRun != null) { + // remove scheduled decompilation run + this.nextDecompilationRun.TaskCompletionSource.TrySetCanceled(); + this.nextDecompilationRun = null; + } ShowOutput(textOutput, highlighting); decompiledNodes = nodes; } @@ -343,26 +362,40 @@ namespace ICSharpCode.ILSpy.TextView DecompilationContext nextDecompilationRun; + [Obsolete("Use DecompileAsync() instead")] + public void Decompile(ILSpy.Language language, IEnumerable treeNodes, DecompilationOptions options) + { + DecompileAsync(language, treeNodes, options).HandleExceptions(); + } + /// /// Starts the decompilation of the given nodes. /// The result is displayed in the text view. + /// If any errors occur, the error message is displayed in the text view, and the task returned by this method completes successfully. + /// If the operation is cancelled (by starting another decompilation action); the returned task is marked as cancelled. /// - public void Decompile(ILSpy.Language language, IEnumerable treeNodes, DecompilationOptions options) + public Task DecompileAsync(ILSpy.Language language, IEnumerable treeNodes, DecompilationOptions options) { // Some actions like loading an assembly list cause several selection changes in the tree view, // and each of those will start a decompilation action. + bool isDecompilationScheduled = this.nextDecompilationRun != null; + if (this.nextDecompilationRun != null) + this.nextDecompilationRun.TaskCompletionSource.TrySetCanceled(); this.nextDecompilationRun = new DecompilationContext(language, treeNodes.ToArray(), options); + var task = this.nextDecompilationRun.TaskCompletionSource.Task; if (!isDecompilationScheduled) { Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action( delegate { var context = this.nextDecompilationRun; this.nextDecompilationRun = null; if (context != null) - DoDecompile(context, DefaultOutputLengthLimit); + DoDecompile(context, DefaultOutputLengthLimit) + .ContinueWith(t => context.TaskCompletionSource.SetFromTask(t)).HandleExceptions(); } )); } + return task; } sealed class DecompilationContext @@ -370,6 +403,7 @@ namespace ICSharpCode.ILSpy.TextView public readonly ILSpy.Language Language; public readonly ILSpyTreeNode[] TreeNodes; public readonly DecompilationOptions Options; + public readonly TaskCompletionSource TaskCompletionSource = new TaskCompletionSource(); public DecompilationContext(ILSpy.Language language, ILSpyTreeNode[] treeNodes, DecompilationOptions options) { @@ -379,33 +413,28 @@ namespace ICSharpCode.ILSpy.TextView } } - void DoDecompile(DecompilationContext context, int outputLengthLimit) + Task DoDecompile(DecompilationContext context, int outputLengthLimit) { - RunWithCancellation( + return RunWithCancellation( delegate (CancellationToken ct) { // creation of the background task context.Options.CancellationToken = ct; return DecompileAsync(context, outputLengthLimit); - }, - delegate (Task task) { // handling the result - try { - AvalonEditTextOutput textOutput = task.Result; - ShowOutput(textOutput, context.Language.SyntaxHighlighting, context.Options.TextViewState); - } catch (AggregateException aggregateException) { - textEditor.SyntaxHighlighting = null; - Debug.WriteLine("Decompiler crashed: " + aggregateException.ToString()); - // Unpack aggregate exceptions as long as there's only a single exception: - // (assembly load errors might produce nested aggregate exceptions) - Exception ex = aggregateException; - while (ex is AggregateException && (ex as AggregateException).InnerExceptions.Count == 1) - ex = ex.InnerException; - AvalonEditTextOutput output = new AvalonEditTextOutput(); - if (ex is OutputLengthExceededException) { - WriteOutputLengthExceededMessage(output, context, outputLengthLimit == DefaultOutputLengthLimit); - } else { - output.WriteLine(ex.ToString()); - } - ShowOutput(output); + }) + .Then( + delegate (AvalonEditTextOutput textOutput) { // handling the result + ShowOutput(textOutput, context.Language.SyntaxHighlighting, context.Options.TextViewState); + decompiledNodes = context.TreeNodes; + }) + .Catch(exception => { + textEditor.SyntaxHighlighting = null; + Debug.WriteLine("Decompiler crashed: " + exception.ToString()); + AvalonEditTextOutput output = new AvalonEditTextOutput(); + if (exception is OutputLengthExceededException) { + WriteOutputLengthExceededMessage(output, context, outputLengthLimit == DefaultOutputLengthLimit); + } else { + output.WriteLine(exception.ToString()); } + ShowOutput(output); decompiledNodes = context.TreeNodes; }); } @@ -435,7 +464,7 @@ namespace ICSharpCode.ILSpy.TextView } catch (OutputLengthExceededException ex) { tcs.SetException(ex); } catch (AggregateException ex) { - tcs.SetException(ex); + tcs.SetException(ex.InnerExceptions); } catch (OperationCanceledException) { tcs.SetCanceled(); } @@ -489,7 +518,7 @@ namespace ICSharpCode.ILSpy.TextView output.AddButton( Images.ViewCode, "Display Code", delegate { - DoDecompile(context, ExtendedOutputLengthLimit); + DoDecompile(context, ExtendedOutputLengthLimit).HandleExceptions(); }); output.WriteLine(); } @@ -589,24 +618,17 @@ namespace ICSharpCode.ILSpy.TextView delegate (CancellationToken ct) { context.Options.CancellationToken = ct; return SaveToDiskAsync(context, fileName); - }, - delegate (Task task) { - try { - ShowOutput(task.Result); - } catch (AggregateException aggregateException) { - textEditor.SyntaxHighlighting = null; - Debug.WriteLine("Decompiler crashed: " + aggregateException.ToString()); - // Unpack aggregate exceptions as long as there's only a single exception: - // (assembly load errors might produce nested aggregate exceptions) - Exception ex = aggregateException; - while (ex is AggregateException && (ex as AggregateException).InnerExceptions.Count == 1) - ex = ex.InnerException; - AvalonEditTextOutput output = new AvalonEditTextOutput(); - output.WriteLine(ex.ToString()); - ShowOutput(output); - } - decompiledNodes = context.TreeNodes; - }); + }) + .Then(output => ShowOutput(output)) + .Catch((Exception ex) => { + textEditor.SyntaxHighlighting = null; + Debug.WriteLine("Decompiler crashed: " + ex.ToString()); + // Unpack aggregate exceptions as long as there's only a single exception: + // (assembly load errors might produce nested aggregate exceptions) + AvalonEditTextOutput output = new AvalonEditTextOutput(); + output.WriteLine(ex.ToString()); + ShowOutput(output); + }).HandleExceptions(); } Task SaveToDiskAsync(DecompilationContext context, string fileName) diff --git a/ILSpy/TreeNodes/ResourceNodes/XamlResourceNode.cs b/ILSpy/TreeNodes/ResourceNodes/XamlResourceNode.cs index b6eb09f9c..dd9bc3003 100644 --- a/ILSpy/TreeNodes/ResourceNodes/XamlResourceNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/XamlResourceNode.cs @@ -73,9 +73,8 @@ namespace ICSharpCode.ILSpy.Xaml output.Write(ex.ToString()); } return output; - }, token), - t => textView.ShowNode(t.Result, this, highlighting) - ); + }, token) + ).Then(t => textView.ShowNode(t, this, highlighting)).HandleExceptions(); return true; } } diff --git a/ILSpy/TreeNodes/ResourceNodes/XmlResourceNode.cs b/ILSpy/TreeNodes/ResourceNodes/XmlResourceNode.cs index 3677e97f4..935fbb4b6 100644 --- a/ILSpy/TreeNodes/ResourceNodes/XmlResourceNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/XmlResourceNode.cs @@ -101,9 +101,8 @@ namespace ICSharpCode.ILSpy.Xaml output.Write(ex.ToString()); } return output; - }, token), - t => textView.ShowNode(t.Result, this, highlighting) - ); + }, token) + ).Then(t => textView.ShowNode(t, this, highlighting)).HandleExceptions(); return true; } }