11// --------------------------------------------------------------------------------------------------------------------
22// <copyright file="Program.cs" company="John Robbins/Wintellect">
3- // (c) 2012 by John Robbins/Wintellect
3+ // (c) 2012-2016 by John Robbins/Wintellect
44// </copyright>
55// <summary>
66// The fast file finder program.
1010namespace FastFind
1111{
1212 using System ;
13+ using System . Collections . Concurrent ;
1314 using System . Diagnostics ;
15+ using System . Diagnostics . CodeAnalysis ;
1416 using System . Globalization ;
1517 using System . IO ;
18+ using System . Text ;
1619 using System . Threading ;
1720 using System . Threading . Tasks ;
1821
1922 /// <summary>
2023 /// The entry point to the application.
2124 /// </summary>
22- internal static class Program
25+ internal sealed class Program
2326 {
2427 /// <summary>
2528 /// Holds the command line options the user wanted.
@@ -41,6 +44,11 @@ internal static class Program
4144 /// </summary>
4245 private static Int64 totalDirectories ;
4346
47+ /// <summary>
48+ /// The collection to hold found strings so they can be printed in batch mode.
49+ /// </summary>
50+ private static readonly BlockingCollection < String > ResultsQueue = new BlockingCollection < String > ( ) ;
51+
4452 /// <summary>
4553 /// The entry point function for the program.
4654 /// </summary>
@@ -69,8 +77,18 @@ internal static Int32 Main(String[] args)
6977
7078 if ( parsed )
7179 {
80+ var canceller = new CancellationTokenSource ( ) ;
81+
82+ // Fire up the searcher and batch output threads.
7283 var task = Task . Factory . StartNew ( ( ) => RecurseFiles ( Options . Path ) ) ;
84+ var resultsTask = Task . Factory . StartNew ( ( ) => WriteResultsBatched ( canceller . Token , 200 ) ) ;
85+
7386 task . Wait ( ) ;
87+
88+ // Indicate a cancel so all remaining strings get printed out.
89+ canceller . Cancel ( ) ;
90+ resultsTask . Wait ( ) ;
91+
7492 timer . Stop ( ) ;
7593
7694 if ( false == Options . NoStatistics )
@@ -172,66 +190,139 @@ private static Boolean IsNameMatch(String name)
172190 }
173191
174192 /// <summary>
175- /// Reports all matches in a directory.
193+ /// Takes care of writing out results found in a batch manner so slow calls to
194+ /// Console.WriteLine are minimized.
176195 /// </summary>
177- /// <param name="directory">
178- /// The directory to look at.
196+ /// <param name="canceller">
197+ /// The cancellation token.
198+ /// </param>
199+ /// <param name="batchSize">
200+ /// The batch size for the number of lines to write.
179201 /// </param>
180- private static void RecurseFiles ( String directory )
202+ private static void WriteResultsBatched ( CancellationToken canceller , Int32 batchSize = 10 )
181203 {
204+ var sb = new StringBuilder ( batchSize * 260 ) ;
205+ var lineCount = 0 ;
206+
182207 try
183208 {
184- String [ ] files = Directory . GetFiles ( directory ) ;
185-
186- Interlocked . Add ( ref totalFiles , files . Length ) ;
187-
188- for ( Int32 i = 0 ; i < files . Length ; i ++ )
209+ foreach ( var line in ResultsQueue . GetConsumingEnumerable ( canceller ) )
189210 {
190- String currFile = files [ i ] ;
191- if ( false == Options . IncludeDirectories )
192- {
193- currFile = Path . GetFileName ( currFile ) ;
194- }
211+ sb . AppendLine ( line ) ;
212+ lineCount ++ ;
195213
196- if ( IsNameMatch ( currFile ) )
214+ if ( lineCount > batchSize )
197215 {
198- Interlocked . Increment ( ref totalMatches ) ;
199- Console . WriteLine ( files [ i ] ) ;
216+ Console . Write ( sb ) ;
217+ sb . Clear ( ) ;
218+ lineCount = 0 ;
200219 }
201220 }
221+ }
222+ catch ( OperationCanceledException )
223+ {
224+ //Not much to do here...
225+ }
226+ finally
227+ {
228+ if ( sb . Length > 0 )
229+ {
230+ Console . Write ( sb ) ;
231+ }
232+ }
233+ }
202234
203- // Lets look for the directories.
204- String [ ] dirs = Directory . GetDirectories ( directory ) ;
205- Interlocked . Add ( ref totalDirectories , dirs . Length ) ;
235+ /// <summary>
236+ /// The method to call when a matching file/directory is found.
237+ /// </summary>
238+ /// <param name="line">
239+ /// The matching item to add to the output queue.
240+ /// </param>
241+ private static void QueueConsoleWriteLine ( String line )
242+ {
243+ ResultsQueue . Add ( line ) ;
244+ }
206245
207- for ( Int32 i = 0 ; i < dirs . Length ; i ++ )
246+
247+ /// <summary>
248+ /// The main method that does the recursive file matching.
249+ /// </summary>
250+ /// <param name="directory">
251+ /// The file directory to search.
252+ /// </param>
253+ /// <remarks>
254+ /// This method calls the low level Windows API because the built in .NET APIs do not
255+ /// support long file names. (Those greater than 260 characters).
256+ /// </remarks>
257+ static private void RecurseFiles ( String directory )
258+ {
259+ String lookUpdirectory = "\\ \\ ?\\ " + directory + "\\ *" ;
260+ NativeMethods . WIN32_FIND_DATA w32FindData ;
261+
262+ using ( SafeFindFileHandle fileHandle = NativeMethods . FindFirstFileEx ( lookUpdirectory ,
263+ NativeMethods . FINDEX_INFO_LEVELS . Basic ,
264+ out w32FindData ,
265+ NativeMethods . FINDEX_SEARCH_OPS . SearchNameMatch ,
266+ IntPtr . Zero ,
267+ NativeMethods . FindExAdditionalFlags . LargeFetch ) )
268+ {
269+ if ( ! fileHandle . IsInvalid )
208270 {
209- String currDir = dirs [ i ] ;
210- if ( Options . IncludeDirectories )
271+ do
211272 {
212- if ( IsNameMatch ( currDir ) )
273+ // Does this match "." or ".."? If so get out.
274+ if ( ( w32FindData . cFileName . Equals ( "." , StringComparison . OrdinalIgnoreCase ) ||
275+ ( w32FindData . cFileName . Equals ( ".." , StringComparison . OrdinalIgnoreCase ) ) ) )
213276 {
214- Interlocked . Increment ( ref totalMatches ) ;
215- Console . WriteLine ( currDir ) ;
277+ continue ;
216278 }
217- }
218279
219- // Recurse our way to happiness....
220- Task . Factory . StartNew ( ( ) => RecurseFiles ( currDir ) , TaskCreationOptions . AttachedToParent ) ;
280+ // Is this a directory? If so, queue up another task.
281+ if ( ( w32FindData . dwFileAttributes & FileAttributes . Directory ) == FileAttributes . Directory )
282+ {
283+ Interlocked . Increment ( ref totalDirectories ) ;
284+
285+ String subDirectory = directory + "\\ " + w32FindData . cFileName ;
286+ if ( Options . IncludeDirectories )
287+ {
288+ if ( IsNameMatch ( subDirectory ) )
289+ {
290+ Interlocked . Increment ( ref totalMatches ) ;
291+ QueueConsoleWriteLine ( subDirectory ) ;
292+ }
293+ }
294+
295+ // Recurse our way to happiness....
296+ Task . Factory . StartNew ( ( ) => RecurseFiles ( subDirectory ) , TaskCreationOptions . AttachedToParent ) ;
297+ }
298+ else
299+ {
300+ // It's a file so look at it.
301+ Interlocked . Increment ( ref totalFiles ) ;
302+
303+ String fullFile = directory ;
304+ if ( ! directory . EndsWith ( "\\ " , StringComparison . OrdinalIgnoreCase ) )
305+ {
306+ fullFile += "\\ " ;
307+ }
308+ fullFile += w32FindData . cFileName ;
309+
310+ String matchName = fullFile ;
311+
312+ if ( false == Options . IncludeDirectories )
313+ {
314+ matchName = w32FindData . cFileName ;
315+ }
316+
317+ if ( IsNameMatch ( matchName ) )
318+ {
319+ Interlocked . Increment ( ref totalMatches ) ;
320+ QueueConsoleWriteLine ( fullFile ) ;
321+ }
322+ }
323+ } while ( NativeMethods . FindNextFile ( fileHandle , out w32FindData ) ) ;
221324 }
222325 }
223- catch ( UnauthorizedAccessException )
224- {
225- // I guess I could dump out the fact that there was a directory
226- // the caller doesn't have access to but it seems like overkill
227- // and noise.
228- }
229- catch ( PathTooLongException )
230- {
231- // Not much I can do here. The BCL methods do not support the \\?\c:\ format
232- // like the raw Win32 API. I guess I could thunk down and do this on my own by
233- // pInvoke.
234- }
235326 }
236327 }
237328}
0 commit comments