@@ -82,7 +82,14 @@ public static Task<SolutionExportResult> CreateSolutionAsync(string solutionFile
8282 readonly IProgress < DecompilationProgress > ? progress ;
8383 readonly ConcurrentBag < ProjectItem > projects ;
8484 readonly ConcurrentBag < string > statusOutput ;
85- int completedAssemblies ;
85+
86+ // How far each project has got, keyed by assembly short name -- unique, because duplicate names
87+ // abort the export before any project runs. The workers fill these in as they decompile and the
88+ // progress bar shows their sum.
89+ readonly ConcurrentDictionary < string , ProjectProgress > projectProgress ;
90+ // The projects in selection order, so the status label lists them in a stable order rather than
91+ // in whatever order the workers happen to reach them.
92+ string [ ] projectOrder ;
8693
8794 SolutionWriter ( string solutionFilePath , DecompilerSettings settings , string ? strongNameKeyFile ,
8895 IProgress < DecompilationProgress > ? progress )
@@ -94,6 +101,32 @@ public static Task<SolutionExportResult> CreateSolutionAsync(string solutionFile
94101 solutionDirectory = Path . GetDirectoryName ( solutionFilePath ) ! ;
95102 statusOutput = new ConcurrentBag < string > ( ) ;
96103 projects = new ConcurrentBag < ProjectItem > ( ) ;
104+ projectProgress = new ConcurrentDictionary < string , ProjectProgress > ( ) ;
105+ projectOrder = Array . Empty < string > ( ) ;
106+ }
107+
108+ /// <summary>How much of one project's file list has been written, and whether it is still running.</summary>
109+ sealed class ProjectProgress
110+ {
111+ public int FilesWritten ;
112+ public int FileCount ;
113+ public bool Running ;
114+ }
115+
116+ /// <summary>
117+ /// Feeds one project's file counts into the shared total. <see cref="WholeProjectDecompiler"/>
118+ /// reports its whole file count with every report, so the solution bar knows a project's size from
119+ /// its first written file rather than only once the project is done.
120+ /// </summary>
121+ sealed class ProjectProgressReporter ( SolutionWriter writer , ProjectProgress project )
122+ : IProgress < DecompilationProgress >
123+ {
124+ public void Report ( DecompilationProgress value )
125+ {
126+ project . FileCount = value . TotalUnits ;
127+ project . FilesWritten = value . UnitsCompleted ;
128+ writer . ReportProgress ( ) ;
129+ }
97130 }
98131
99132 async Task < SolutionExportResult > CreateSolutionAsync ( IReadOnlyList < LoadedAssembly > allAssemblies ,
@@ -123,13 +156,15 @@ async Task<SolutionExportResult> CreateSolutionAsync(IReadOnlyList<LoadedAssembl
123156 if ( abort )
124157 return new SolutionExportResult ( false , report . ToString ( ) ) ;
125158
159+ projectOrder = allAssemblies . Select ( a => a . ShortName ) . ToArray ( ) ;
160+
126161 try
127162 {
128163 // An explicit enumerable partitioner avoids Parallel.ForEach's list special-casing,
129164 // whose static partitioning is inefficient when assemblies decompile at different speeds.
130165 await Task . Run ( ( ) => System . Threading . Tasks . Parallel . ForEach ( Partitioner . Create ( allAssemblies ) ,
131166 new ParallelOptions { MaxDegreeOfParallelism = Environment . ProcessorCount , CancellationToken = ct } ,
132- item => WriteProject ( item , language , solutionDirectory , allAssemblies . Count , ct ) ) )
167+ item => WriteProject ( item , language , solutionDirectory , ct ) ) )
133168 . ConfigureAwait ( false ) ;
134169
135170 if ( projects . Count == 0 )
@@ -183,15 +218,68 @@ await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, project
183218 return new SolutionExportResult ( true , report . ToString ( ) ) ;
184219 }
185220
186- void WriteProject ( LoadedAssembly loadedAssembly , Language language , string targetDirectory , int totalAssemblies , CancellationToken ct )
221+ // Reports the whole solution's progress: the file counts of every project added up. The projects
222+ // are decompiled in parallel, so no single one of them can drive the bar; summing them lets it
223+ // move continuously and, because a project reports its file count as soon as it writes its first
224+ // file, turn determinate right after the export starts. Racing reads are fine here -- the worst a
225+ // report that races a worker can be is a file or two out of date.
226+ void ReportProgress ( )
187227 {
188- // Solution export decompiles assemblies in parallel, so per-file progress would race; report
189- // at the coarser assembly granularity instead -- a determinate bar over the assembly count.
190- void ReportDone ( ) => progress ? . Report ( new DecompilationProgress {
191- TotalUnits = totalAssemblies ,
192- UnitsCompleted = System . Threading . Interlocked . Increment ( ref completedAssemblies ) ,
193- Status = loadedAssembly . ShortName ,
228+ if ( progress == null )
229+ return ;
230+
231+ int filesWritten = 0 , fileCount = 0 ;
232+ foreach ( var project in projectProgress . Values )
233+ {
234+ filesWritten += project . FilesWritten ;
235+ fileCount += project . FileCount ;
236+ }
237+
238+ progress . Report ( new DecompilationProgress {
239+ TotalUnits = fileCount ,
240+ UnitsCompleted = filesWritten ,
241+ Title = "Exporting solution..." ,
242+ Status = RunningProjects ( ) ,
194243 } ) ;
244+ }
245+
246+ // The projects being written right now, so the label says what is running instead of naming
247+ // whichever project happened to report last. Long selections are cut short: the bar is not the
248+ // place to list twenty assemblies.
249+ string RunningProjects ( )
250+ {
251+ const int maxNames = 3 ;
252+ var running = projectOrder
253+ . Where ( name => projectProgress . TryGetValue ( name , out var project ) && project . Running )
254+ . ToList ( ) ;
255+ return running . Count <= maxNames
256+ ? string . Join ( ", " , running )
257+ : string . Join ( ", " , running . Take ( maxNames ) ) + $ " and { running . Count - maxNames } more";
258+ }
259+
260+ void WriteProject ( LoadedAssembly loadedAssembly , Language language , string targetDirectory , CancellationToken ct )
261+ {
262+ var project = new ProjectProgress { Running = true } ;
263+ projectProgress [ loadedAssembly . ShortName ] = project ;
264+ ReportProgress ( ) ;
265+ try
266+ {
267+ WriteProjectCore ( loadedAssembly , language , targetDirectory , project , ct ) ;
268+ }
269+ finally
270+ {
271+ // Whatever became of the project -- written, bailed out before it started, or cancelled --
272+ // it stops counting against the total here. Leaving an abandoned project's files
273+ // outstanding would strand the bar short of the end for the rest of the export.
274+ project . FilesWritten = project . FileCount ;
275+ project . Running = false ;
276+ ReportProgress ( ) ;
277+ }
278+ }
279+
280+ void WriteProjectCore ( LoadedAssembly loadedAssembly , Language language , string targetDirectory ,
281+ ProjectProgress project , CancellationToken ct )
282+ {
195283 targetDirectory = Path . Combine ( targetDirectory , loadedAssembly . ShortName ) ;
196284
197285 if ( language . ProjectFileExtension == null )
@@ -230,6 +318,7 @@ void ReportDone() => progress?.Report(new DecompilationProgress {
230318 options . CancellationToken = ct ;
231319 options . SaveAsProjectDirectory = targetDirectory ;
232320 options . StrongNameKeyFile = strongNameKeyFile ;
321+ options . ProgressIndicator = new ProjectProgressReporter ( this , project ) ;
233322
234323 // The project-export path writes the .csproj into SaveAsProjectDirectory itself; the
235324 // ITextOutput only receives a "Project written to ..." breadcrumb, which we discard here.
@@ -260,7 +349,6 @@ void ReportDone() => progress?.Report(new DecompilationProgress {
260349 statusOutput . Add ( "-------------" ) ;
261350 statusOutput . Add ( $ "Failed to decompile the assembly '{ loadedAssembly . FileName } ':{ Environment . NewLine } { e } ") ;
262351 }
263- ReportDone ( ) ;
264352 }
265353 }
266354}
0 commit comments