Skip to content

Commit 559bb12

Browse files
Now with faster search and output
1 parent b0bdb6b commit 559bb12

File tree

5 files changed

+245
-44
lines changed

5 files changed

+245
-44
lines changed

FF/FF.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<RunCodeAnalysis>true</RunCodeAnalysis>
2626
<CodeAnalysisRuleSet>FF.ruleset</CodeAnalysisRuleSet>
2727
<UseVSHostingProcess>false</UseVSHostingProcess>
28+
<Prefer32Bit>false</Prefer32Bit>
2829
</PropertyGroup>
2930
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
3031
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -36,6 +37,8 @@
3637
<WarningLevel>4</WarningLevel>
3738
<CodeAnalysisRuleSet>FF.ruleset</CodeAnalysisRuleSet>
3839
<RunCodeAnalysis>true</RunCodeAnalysis>
40+
<UseVSHostingProcess>false</UseVSHostingProcess>
41+
<Prefer32Bit>false</Prefer32Bit>
3942
</PropertyGroup>
4043
<ItemGroup>
4144
<Reference Include="System" />
@@ -54,8 +57,10 @@
5457
<DependentUpon>Constants.resx</DependentUpon>
5558
</Compile>
5659
<Compile Include="FastFindArgumentParser.cs" />
60+
<Compile Include="NativeMethods.cs" />
5761
<Compile Include="Program.cs" />
5862
<Compile Include="Properties\AssemblyInfo.cs" />
63+
<Compile Include="SafeFindFileHandle.cs" />
5964
</ItemGroup>
6065
<ItemGroup>
6166
<None Include="FF.ruleset">

FF/FF.sln

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 14
4-
VisualStudioVersion = 14.0.23107.0
4+
VisualStudioVersion = 14.0.24720.0
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FF", "FF.csproj", "{0E90979C-C1C1-425B-86D1-B0BB9078EAAF}"
77
EndProject
88
Global
9+
GlobalSection(Performance) = preSolution
10+
HasPerformanceSessions = true
11+
EndGlobalSection
912
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1013
Debug|Any CPU = Debug|Any CPU
1114
Release|Any CPU = Release|Any CPU

FF/NativeMethods.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.IO;
3+
using System.Runtime.InteropServices;
4+
using System.Runtime.InteropServices.ComTypes;
5+
6+
namespace FastFind
7+
{
8+
/// <summary>
9+
/// Wrappers around all native Win32 API calls.
10+
/// </summary>
11+
internal static class NativeMethods
12+
{
13+
internal enum FINDEX_INFO_LEVELS
14+
{
15+
Standard = 0,
16+
Basic = 1
17+
}
18+
19+
internal enum FINDEX_SEARCH_OPS
20+
{
21+
SearchNameMatch = 0,
22+
SearchLimitToDirectories = 1,
23+
SearchLimitToDevices = 2
24+
}
25+
26+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
27+
internal struct WIN32_FIND_DATA
28+
{
29+
public FileAttributes dwFileAttributes;
30+
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
31+
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
32+
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
33+
public UInt32 nFileSizeHigh;
34+
public UInt32 nFileSizeLow;
35+
public readonly UInt32 dwReserved0;
36+
private readonly UInt32 dwReserved1;
37+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
38+
public String cFileName;
39+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
40+
public String cAlternateFileName;
41+
}
42+
internal enum FindExAdditionalFlags
43+
{
44+
None = 0,
45+
CaseSensitive = 1,
46+
LargeFetch = 2
47+
}
48+
49+
[DllImport("kernel32.dll", SetLastError = false, CharSet = CharSet.Unicode)]
50+
[return: MarshalAs(UnmanagedType.Bool)]
51+
internal static extern Boolean FindClose(IntPtr hFindFile);
52+
53+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "FindFirstFileExW")]
54+
internal static extern SafeFindFileHandle FindFirstFileEx([MarshalAs(UnmanagedType.LPWStr)] String lpFileName,
55+
FINDEX_INFO_LEVELS fInfoLevelId,
56+
out WIN32_FIND_DATA lpFindFileData,
57+
FINDEX_SEARCH_OPS fSearchOp,
58+
IntPtr lpSearchFilter,
59+
FindExAdditionalFlags dwAdditionalFlags);
60+
61+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "FindNextFileW")]
62+
[return: MarshalAs(UnmanagedType.Bool)]
63+
internal static extern Boolean FindNextFile(SafeFindFileHandle hFindFile, out WIN32_FIND_DATA lpFindFileData);
64+
65+
66+
}
67+
}

FF/Program.cs

Lines changed: 134 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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.
@@ -10,16 +10,19 @@
1010
namespace 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
}

FF/SafeFindFileHandle.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Microsoft.Win32.SafeHandles;
2+
using System;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace FastFind
6+
{
7+
/// <summary>
8+
/// Wraps up the FindFirstFileEx and FindNextFile handle.
9+
/// </summary>
10+
internal sealed class SafeFindFileHandle : SafeHandleZeroOrMinusOneIsInvalid
11+
{
12+
private SafeFindFileHandle() : base(true)
13+
{
14+
}
15+
16+
[SuppressMessage("Microsoft.Performance",
17+
"CA1811:AvoidUncalledPrivateCode",
18+
Justification = "Not called directly, but implicitly by FindFirstFileEx ")]
19+
public SafeFindFileHandle(IntPtr handle, Boolean ownsHandle = true) : base(ownsHandle)
20+
{
21+
SetHandle(handle);
22+
}
23+
24+
protected override Boolean ReleaseHandle()
25+
{
26+
Boolean retValue = true;
27+
if (!IsClosed)
28+
{
29+
retValue = NativeMethods.FindClose(handle);
30+
SetHandleAsInvalid();
31+
}
32+
return retValue;
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)