diff --git a/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs b/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs index 4db4ad455..9b87d99be 100644 --- a/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs +++ b/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs @@ -33,8 +33,8 @@ namespace ICSharpCode.Decompiler.PowerShell lock (syncObject) { completed++; - progress = new ProgressRecord(1, "Decompiling " + fileName, $"Completed {completed} of {value.TotalNumberOfFiles}: {value.Status}") { - PercentComplete = (int)(completed * 100.0 / value.TotalNumberOfFiles) + progress = new ProgressRecord(1, "Decompiling " + fileName, $"Completed {completed} of {value.TotalUnits}: {value.Status}") { + PercentComplete = (int)(completed * 100.0 / value.TotalUnits) }; } } diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 90c855872..ab488ee4d 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -312,6 +312,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs index 174711c8b..991c59cae 100644 --- a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.Metadata; @@ -9,13 +8,10 @@ using System.Text; using System.Xml.Linq; using ICSharpCode.Decompiler.CSharp; -using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.DebugInfo; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Tests.Helpers; -using ICSharpCode.Decompiler.TypeSystem; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.DiaSymReader.Tools; using NUnit.Framework; @@ -72,6 +68,60 @@ namespace ICSharpCode.Decompiler.Tests } } + [Test] + public void ProgressReporting() + { + // Generate a PDB for an assembly and validate that the progress reporter is called with reasonable values + (string peFileName, string pdbFileName) = CompileTestCase(nameof(ProgressReporting)); + + var moduleDefinition = new PEFile(peFileName); + var resolver = new UniversalAssemblyResolver(peFileName, false, moduleDefinition.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage); + var decompiler = new CSharpDecompiler(moduleDefinition, resolver, new DecompilerSettings()); + + var lastFilesWritten = 0; + var totalFiles = -1; + + Action reportFunc = progress => { + if (totalFiles == -1) + { + // Initialize value on first call + totalFiles = progress.TotalUnits; + } + + Assert.AreEqual(progress.TotalUnits, totalFiles); + Assert.AreEqual(progress.UnitsCompleted, lastFilesWritten + 1); + + lastFilesWritten = progress.UnitsCompleted; + }; + + using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, nameof(ProgressReporting) + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite)) + { + pdbStream.SetLength(0); + PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true, progress: new TestProgressReporter(reportFunc)); + + pdbStream.Position = 0; + var metadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); + var generatedPdbId = new BlobContentId(metadataReader.DebugMetadataHeader.Id); + } + + Assert.AreEqual(totalFiles, lastFilesWritten); + } + + private class TestProgressReporter : IProgress + { + private Action reportFunc; + + public TestProgressReporter(Action reportFunc) + { + this.reportFunc = reportFunc; + } + + public void Report(DecompilationProgress value) + { + reportFunc(value); + } + } + private void TestGeneratePdb([CallerMemberName] string testName = null) { const PdbToXmlOptions options = PdbToXmlOptions.IncludeEmbeddedSources | PdbToXmlOptions.ThrowOnError | PdbToXmlOptions.IncludeTokens | PdbToXmlOptions.ResolveTokens | PdbToXmlOptions.IncludeMethodSpans; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/CustomPdbId.xml b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/CustomPdbId.xml index e0708da65..e5d691c3d 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/CustomPdbId.xml +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/CustomPdbId.xml @@ -1,11 +1,11 @@ - + + + + + + \ No newline at end of file diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 3057cbb6f..c49fde67c 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -223,8 +223,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler return Path.Combine(dir, file); } }, StringComparer.OrdinalIgnoreCase).ToList(); - int total = files.Count; - var progress = ProgressIndicator; + var progressReporter = ProgressIndicator; + var progress = new DecompilationProgress { TotalUnits = files.Count, Title = "Exporting project..." }; DecompilerTypeSystem ts = new DecompilerTypeSystem(module, AssemblyResolver, Settings); Parallel.ForEach( Partitioner.Create(files, loadBalance: true), @@ -253,7 +253,9 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler throw new DecompilerException(module, $"Error decompiling for '{file.Key}'", innerException); } } - progress?.Report(new DecompilationProgress(total, file.Key)); + progress.Status = file.Key; + Interlocked.Increment(ref progress.UnitsCompleted); + progressReporter?.Report(progress); }); return files.Select(f => ("Compile", f.Key)).Concat(WriteAssemblyInfo(ts, cancellationToken)); } @@ -705,16 +707,4 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler return TargetServices.DetectTargetFramework(module).Moniker != null; } } - - public readonly struct DecompilationProgress - { - public readonly int TotalNumberOfFiles; - public readonly string Status; - - public DecompilationProgress(int total, string status = null) - { - this.TotalNumberOfFiles = total; - this.Status = status ?? ""; - } - } } diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index cc8e342bd..b0b419aab 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -28,6 +28,7 @@ using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using System.Security.Cryptography; using System.Text; +using System.Threading; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; @@ -49,7 +50,14 @@ namespace ICSharpCode.Decompiler.DebugInfo return file.Reader.ReadDebugDirectory().Any(entry => entry.Type == DebugDirectoryEntryType.CodeView); } - public static void WritePdb(PEFile file, CSharpDecompiler decompiler, DecompilerSettings settings, Stream targetStream, bool noLogo = false, BlobContentId? pdbId = null) + public static void WritePdb( + PEFile file, + CSharpDecompiler decompiler, + DecompilerSettings settings, + Stream targetStream, + bool noLogo = false, + BlobContentId? pdbId = null, + IProgress progress = null) { MetadataBuilder metadata = new MetadataBuilder(); MetadataReader reader = file.Metadata; @@ -72,10 +80,24 @@ namespace ICSharpCode.Decompiler.DebugInfo return Path.Combine(ns, WholeProjectDecompiler.CleanUpFileName(typeName.Name) + ".cs"); } - foreach (var sourceFile in reader.GetTopLevelTypeDefinitions().GroupBy(BuildFileNameFromTypeName)) + var sourceFiles = reader.GetTopLevelTypeDefinitions().GroupBy(BuildFileNameFromTypeName).ToList(); + DecompilationProgress currentProgress = new() { + TotalUnits = sourceFiles.Count, + UnitsCompleted = 0, + Title = "Generating portable PDB..." + }; + + foreach (var sourceFile in sourceFiles) { // Generate syntax tree var syntaxTree = decompiler.DecompileTypes(sourceFile); + + if (progress != null) + { + currentProgress.UnitsCompleted++; + progress.Report(currentProgress); + } + if (!syntaxTree.HasChildren) continue; diff --git a/ICSharpCode.Decompiler/DecompilationProgress.cs b/ICSharpCode.Decompiler/DecompilationProgress.cs new file mode 100644 index 000000000..3d9634998 --- /dev/null +++ b/ICSharpCode.Decompiler/DecompilationProgress.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2022 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. + +#nullable enable + +namespace ICSharpCode.Decompiler +{ + /// + /// Information used for (optional) progress reporting by the decompiler. + /// + public struct DecompilationProgress + { + /// + /// The total number of units to process. If set to a value <= 0, an indeterminate progress bar is displayed. + /// + public int TotalUnits; + + /// + /// The number of units currently completed. Should be a positive number. + /// + public int UnitsCompleted; + + /// + /// Optional information displayed alongside the progress bar. + /// + public string? Status; + + /// + /// Optional custom title for the operation. + /// + public string? Title; + } +} diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index b3b5cc7a6..25d9b0bf9 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -90,6 +90,7 @@ + diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 3af1c3b64..84bafc423 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -75,12 +75,14 @@ namespace ICSharpCode.ILSpy Docking.DockWorkspace.Instance.RunWithCancellation(ct => Task.Factory.StartNew(() => { AvalonEditTextOutput output = new AvalonEditTextOutput(); Stopwatch stopwatch = Stopwatch.StartNew(); + options.CancellationToken = ct; using (FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)) { try { var decompiler = new CSharpDecompiler(file, assembly.GetAssemblyResolver(), options.DecompilerSettings); - PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream); + decompiler.CancellationToken = ct; + PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream, progress: options.Progress); } catch (OperationCanceledException) { diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs index d241da8f1..7d6feb5c0 100644 --- a/ILSpy/DecompilationOptions.cs +++ b/ILSpy/DecompilationOptions.cs @@ -19,6 +19,7 @@ using System; using System.Threading; +using ICSharpCode.Decompiler; using ICSharpCode.ILSpy.Options; using ICSharpCode.ILSpyX; @@ -55,6 +56,14 @@ namespace ICSharpCode.ILSpy /// public CancellationToken CancellationToken { get; set; } + /// + /// Gets the progress reporter. + /// + /// + /// If decompilers do not implement progress reporting, an indeterminate wait bar is displayed. + /// + public IProgress Progress { get; set; } + /// /// Gets the settings for the decompiler. /// diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index a1d942882..492ecf220 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -404,6 +404,7 @@ namespace ICSharpCode.ILSpy options.DecompilerSettings.UseSdkStyleProjectFormat = false; } var decompiler = new ILSpyWholeProjectDecompiler(assembly, options); + decompiler.ProgressIndicator = options.Progress; return decompiler.DecompileProject(module, options.SaveAsProjectDirectory, new TextOutputWriter(output), options.CancellationToken); } else diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index cf64691d7..3ffe81d2d 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -105,7 +105,11 @@ namespace ICSharpCode.ILSpy public DisplaySettings CurrentDisplaySettings { get; internal set; } - public DecompilationOptions CreateDecompilationOptions() => new DecompilationOptions(CurrentLanguageVersion, CurrentDecompilerSettings, CurrentDisplaySettings); + public DecompilationOptions CreateDecompilationOptions() + { + var decompilerView = DockWorkspace.Instance.ActiveTabPage.Content as IProgress; + return new DecompilationOptions(CurrentLanguageVersion, CurrentDecompilerSettings, CurrentDisplaySettings) { Progress = decompilerView }; + } public MainWindow() { diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 02c00bfda..539c0b4c4 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -70,7 +70,7 @@ namespace ICSharpCode.ILSpy.TextView /// Manages the TextEditor showing the decompiled code. /// Contains all the threading logic that makes the decompiler work in the background. /// - public sealed partial class DecompilerTextView : UserControl, IDisposable, IHaveState + public sealed partial class DecompilerTextView : UserControl, IDisposable, IHaveState, IProgress { readonly ReferenceElementGenerator referenceElementGenerator; readonly UIElementGenerator uiElementGenerator; @@ -540,18 +540,26 @@ namespace ICSharpCode.ILSpy.TextView #endregion #region RunWithCancellation - /// - /// Switches the GUI into "waiting" mode, then calls to create - /// the task. - /// When the task completes without being cancelled, the - /// callback is called on the GUI thread. - /// 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) + public void Report(DecompilationProgress value) { - RunWithCancellation(taskCreation).ContinueWith(taskCompleted, CancellationToken.None, TaskContinuationOptions.NotOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); + double v = (double)value.UnitsCompleted / value.TotalUnits; + Dispatcher.BeginInvoke(DispatcherPriority.Normal, delegate { + progressBar.IsIndeterminate = !double.IsFinite(v); + progressBar.Value = v * 100.0; + progressTitle.Text = !string.IsNullOrWhiteSpace(value.Title) ? value.Title : Properties.Resources.Decompiling; + progressText.Text = value.Status; + progressText.Visibility = !string.IsNullOrWhiteSpace(progressText.Text) ? Visibility.Visible : Visibility.Collapsed; + var taskBar = MainWindow.Instance.TaskbarItemInfo; + if (taskBar != null) + { + taskBar.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal; + taskBar.ProgressValue = v; + } + if (this.DataContext is TabPageModel model) + { + model.Title = progressTitle.Text; + } + }); } /// @@ -566,7 +574,10 @@ namespace ICSharpCode.ILSpy.TextView waitAdorner.Visibility = Visibility.Visible; // Work around a WPF bug by setting IsIndeterminate only while the progress bar is visible. // https://github.com/icsharpcode/ILSpy/issues/593 + progressTitle.Text = Properties.Resources.Decompiling; progressBar.IsIndeterminate = true; + progressText.Text = null; + progressText.Visibility = Visibility.Collapsed; waitAdorner.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, new Duration(TimeSpan.FromSeconds(0.5)), FillBehavior.Stop)); var taskBar = MainWindow.Instance.TaskbarItemInfo; if (taskBar != null) @@ -605,6 +616,8 @@ namespace ICSharpCode.ILSpy.TextView currentCancellationTokenSource = null; waitAdorner.Visibility = Visibility.Collapsed; progressBar.IsIndeterminate = false; + progressText.Text = null; + progressText.Visibility = Visibility.Collapsed; var taskBar = MainWindow.Instance.TaskbarItemInfo; if (taskBar != null) { @@ -828,6 +841,7 @@ namespace ICSharpCode.ILSpy.TextView return RunWithCancellation( delegate (CancellationToken ct) { // creation of the background task context.Options.CancellationToken = ct; + context.Options.Progress = this; decompiledNodes = context.TreeNodes; return DecompileAsync(context, outputLengthLimit); }) @@ -1091,6 +1105,7 @@ namespace ICSharpCode.ILSpy.TextView { bool originalProjectFormatSetting = context.Options.DecompilerSettings.UseSdkStyleProjectFormat; context.Options.EscapeInvalidIdentifiers = true; + context.Options.Progress = this; AvalonEditTextOutput output = new AvalonEditTextOutput { EnableHyperlinks = true, Title = string.Join(", ", context.TreeNodes.Select(n => n.Text)) diff --git a/ILSpy/TextView/DecompilerTextView.xaml b/ILSpy/TextView/DecompilerTextView.xaml index 66960f102..6ff6d2130 100644 --- a/ILSpy/TextView/DecompilerTextView.xaml +++ b/ILSpy/TextView/DecompilerTextView.xaml @@ -20,8 +20,7 @@ folding:FoldingMargin.FoldingMarkerBackgroundBrush="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" folding:FoldingMargin.SelectedFoldingMarkerBackgroundBrush="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" folding:FoldingMargin.FoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" - folding:FoldingMargin.SelectedFoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" - > + folding:FoldingMargin.SelectedFoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">