├── .gitignore ├── Screenshot.png ├── Source ├── App.ico ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── Configuration.cs ├── ControlLog.xaml ├── ControlLog.xaml.cs ├── Global.cs ├── Icons │ ├── App.png │ ├── Line.png │ ├── Search.png │ └── SearchTerms.png ├── LogFile.cs ├── LogLine.cs ├── LogViewer.toml ├── LogViewer2.csproj ├── LogViewer2.sln ├── Methods.cs ├── SearchCriteria.cs ├── Searches.cs ├── WaitCursor.cs ├── WindowConfiguration.xaml ├── WindowConfiguration.xaml.cs ├── WindowGoToLine.xaml ├── WindowGoToLine.xaml.cs ├── WindowLine.xaml ├── WindowLine.xaml.cs ├── WindowMain.xaml ├── WindowMain.xaml.cs ├── WindowMultiSearch.xaml ├── WindowMultiSearch.xaml.cs ├── WindowSearchTerms.xaml └── WindowSearchTerms.xaml.cs ├── help.md ├── help.pdf ├── readme.md └── readme.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/Screenshot.png -------------------------------------------------------------------------------- /Source/App.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/Source/App.ico -------------------------------------------------------------------------------- /Source/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Source/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace LogViewer2 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /Source/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Drawing; 4 | using Nett; 5 | 6 | namespace LogViewer2 7 | { 8 | /// 9 | /// Allows us to save/load the configuration file to/from TOML 10 | /// 11 | public class Configuration 12 | { 13 | #region Member Variables 14 | public string HighlightColour { get; set; } = "Lime"; 15 | public string ContextColour { get; set; } = "LightGray"; 16 | public int MultiSelectLimit { get; set; } = 1000; 17 | public int NumContextLines { get; set; } = 0; 18 | private const string FILENAME = "LogViewer.toml"; 19 | #endregion 20 | 21 | #region Public Methods 22 | /// 23 | /// 24 | /// 25 | /// 26 | public string Load() 27 | { 28 | try 29 | { 30 | if (File.Exists(this.GetPath()) == false) 31 | { 32 | return string.Empty; 33 | } 34 | 35 | Configuration c = Toml.ReadFile(this.GetPath()); 36 | this.HighlightColour = c.HighlightColour; 37 | this.ContextColour = c.ContextColour; 38 | this.MultiSelectLimit = c.MultiSelectLimit; 39 | this.NumContextLines = c.NumContextLines; 40 | 41 | if (this.MultiSelectLimit > 10000) 42 | { 43 | this.MultiSelectLimit = 10000; 44 | return "The multiselect limit is 10000"; 45 | } 46 | 47 | if (this.NumContextLines > 10) 48 | { 49 | this.NumContextLines = 10; 50 | return "The maximum number of context lines is 10"; 51 | } 52 | return string.Empty; 53 | } 54 | catch (FileNotFoundException fileNotFoundEx) 55 | { 56 | return fileNotFoundEx.Message; 57 | } 58 | catch (UnauthorizedAccessException unauthAccessEx) 59 | { 60 | return unauthAccessEx.Message; 61 | } 62 | catch (IOException ioEx) 63 | { 64 | return ioEx.Message; 65 | } 66 | catch (Exception ex) 67 | { 68 | return ex.Message; 69 | } 70 | } 71 | 72 | /// 73 | /// 74 | /// 75 | /// 76 | public string Save() 77 | { 78 | try 79 | { 80 | Toml.WriteFile(this, this.GetPath()); 81 | return string.Empty; 82 | } 83 | catch (FileNotFoundException fileNotFoundEx) 84 | { 85 | return fileNotFoundEx.Message; 86 | } 87 | catch (UnauthorizedAccessException unauthAccessEx) 88 | { 89 | return unauthAccessEx.Message; 90 | } 91 | catch (IOException ioEx) 92 | { 93 | return ioEx.Message; 94 | } 95 | catch (Exception ex) 96 | { 97 | return ex.Message; 98 | } 99 | } 100 | 101 | /// 102 | /// 103 | /// 104 | /// 105 | public Color GetHighlightColour() 106 | { 107 | Color temp = Color.FromName(this.HighlightColour); 108 | if (temp.IsKnownColor == false) 109 | { 110 | return Color.Lime; 111 | } 112 | 113 | return temp; 114 | } 115 | 116 | /// 117 | /// 118 | /// 119 | /// 120 | public Color GetContextColour() 121 | { 122 | Color temp = Color.FromName(this.ContextColour); 123 | if (temp.IsKnownColor == false) 124 | { 125 | return Color.LightGray; 126 | } 127 | 128 | return temp; 129 | } 130 | #endregion 131 | 132 | #region Misc Methods 133 | /// 134 | /// 135 | /// 136 | /// 137 | private string GetPath() 138 | { 139 | return System.IO.Path.Combine(Methods.GetApplicationDirectory(), FILENAME); 140 | } 141 | #endregion 142 | } 143 | } -------------------------------------------------------------------------------- /Source/ControlLog.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Source/ControlLog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Data; 11 | using System.Windows.Interop; 12 | 13 | namespace LogViewer2 14 | { 15 | /// 16 | /// 17 | /// 18 | public partial class ControlLog : System.Windows.Controls.UserControl 19 | { 20 | #region Delegates 21 | public delegate void SearchCompleteEvent(ControlLog cl, string fileName, TimeSpan duration, long matches, int numSearchTerms, bool cancelled); 22 | public delegate void CompleteEvent(ControlLog cl, string fileName, TimeSpan duration, bool cancelled); 23 | public delegate void BoolEvent(string fileName, bool val); 24 | public delegate void MessageEvent(string fileName, string message); 25 | public delegate void ProgressUpdateEvent(int percent); 26 | public delegate void ExportInitiatedEvent(ControlLog cl, bool all); 27 | public delegate void MultiSearchInitiatedEvent(ControlLog cl, List searches); 28 | #endregion 29 | 30 | #region Events 31 | public event SearchCompleteEvent SearchComplete; 32 | public event CompleteEvent LoadComplete; 33 | public event CompleteEvent ExportComplete; 34 | public event ProgressUpdateEvent ProgressUpdate; 35 | public event MessageEvent LoadError; 36 | public event ExportInitiatedEvent ExportInitiated; 37 | public event MultiSearchInitiatedEvent MultiSearchInitiated; 38 | #endregion 39 | 40 | #region Member Variables 41 | //private Color highlightColour { get; set; } = Color.Lime; 42 | // private Color contextColour { get; set; } = Color.LightGray; 43 | private Configuration config; 44 | public Searches Searches { get; set; } 45 | public Global.ViewMode ViewMode { get; set; } = Global.ViewMode.Standard; 46 | public List Lines { get; private set; } = new List(); 47 | public LogLine LongestLine { get; private set; } = new LogLine(); 48 | public int LineCount { get; private set; } = 0; 49 | private FileStream fileStream; 50 | private Mutex readMutex = new Mutex(); 51 | public string FileName { get; private set; } 52 | public List FilterIds { get; private set; } = new List(); 53 | public string Guid { get; private set; } 54 | #endregion 55 | 56 | #region Constructor 57 | /// 58 | /// 59 | /// 60 | /// 61 | public ControlLog(Configuration config) 62 | { 63 | InitializeComponent(); 64 | 65 | this.AllowDrop = true; 66 | this.config = config; 67 | this.Guid = System.Guid.NewGuid().ToString(); 68 | this.Searches = new Searches(); 69 | } 70 | #endregion 71 | 72 | #region Public Methods 73 | /// 74 | /// 75 | /// 76 | /// 77 | /// 78 | public void Load(string filePath, CancellationToken ct) 79 | { 80 | this.Dispose(); 81 | this.FileName = Path.GetFileName(filePath); 82 | 83 | Task.Run(() => { 84 | 85 | DateTime start = DateTime.Now; 86 | bool cancelled = false; 87 | bool error = false; 88 | try 89 | { 90 | byte[] tempBuffer = new byte[1024 * 1024]; 91 | 92 | this.fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); 93 | FileInfo fileInfo = new FileInfo(filePath); 94 | 95 | // Calcs and finally point the position to the end of the line 96 | long position = 0; 97 | // Holds the offset to the start of the next line 98 | long lineStartOffset = 0; 99 | // Checks if we have read less than requested e.g. buffer not filled/end of file 100 | bool lastSection = false; 101 | // Counter for process reporting 102 | int counter = 0; 103 | // Holds a counter to start checking for the next indexOf('\r') 104 | int startIndex = 0; 105 | // Once all of the \r (lines) have been emnumerated, there might still be data left in the 106 | // buffer, so this holds the number of bytes that need to be added onto the next line 107 | int bufferRemainder = 0; 108 | // Holds how many bytes were read from the last file stream read 109 | int numBytesRead = 0; 110 | // Holds the temporary string generated from the file stream buffer 111 | string tempStr = string.Empty; 112 | // Length of the current line 113 | int charCount; 114 | // Return value from IndexOf function 115 | int indexOf; 116 | 117 | while (position < this.fileStream.Length) 118 | { 119 | numBytesRead = this.fileStream.Read(tempBuffer, 0, 1024 * 1024); 120 | if (numBytesRead < 1048576) 121 | 122 | { 123 | lastSection = true; 124 | } 125 | 126 | tempStr = Encoding.ASCII.GetString(tempBuffer).Substring(0, numBytesRead); 127 | startIndex = 0; 128 | 129 | // Does the buffer contain at least one "\n", so now enumerate all instances of "\n" 130 | if (tempStr.IndexOf('\n') != -1) 131 | { 132 | while ((indexOf = tempStr.IndexOf('\n', startIndex)) != -1 && startIndex < numBytesRead) 133 | { 134 | if (indexOf != -1) 135 | { 136 | charCount = 0; 137 | 138 | // Check if the line contains a CR as well, if it does then we remove the last char as the char count 139 | if (indexOf != 0 && (int)tempBuffer[Math.Max(0, indexOf - 1)] == 13) 140 | { 141 | charCount = bufferRemainder + (indexOf - startIndex - 1); 142 | position += (long)charCount + 2L; 143 | } 144 | else 145 | { 146 | charCount = bufferRemainder + (indexOf - startIndex); 147 | position += (long)charCount + 1L; 148 | } 149 | 150 | AddLine(lineStartOffset, charCount); 151 | 152 | // The remaining number in the buffer gets set to 0 e.g. after 153 | //the first iteration as it would add onto the first line 154 | bufferRemainder = 0; 155 | 156 | // Set the offset to the end of the last line that has just been added 157 | lineStartOffset = position; 158 | startIndex = indexOf + 1; 159 | } 160 | } 161 | 162 | // We had some '\r' in the last buffer read, now they are processing, so just add the rest as the last line 163 | if (lastSection == true) 164 | { 165 | AddLine(lineStartOffset, bufferRemainder + (numBytesRead - startIndex)); 166 | return; 167 | } 168 | 169 | bufferRemainder += numBytesRead - startIndex; 170 | } 171 | else 172 | { 173 | // The entire content of the buffer doesn't contain \r so just add the rest of content as the last line 174 | if (lastSection == true) 175 | { 176 | AddLine(lineStartOffset, bufferRemainder + (numBytesRead - startIndex)); 177 | return; 178 | } 179 | 180 | bufferRemainder += numBytesRead; 181 | } 182 | 183 | if (counter++ % 50 == 0) 184 | { 185 | OnProgressUpdate((int)((double)position / (double)fileInfo.Length * 100)); 186 | 187 | if (ct.IsCancellationRequested) 188 | { 189 | cancelled = true; 190 | return; 191 | } 192 | } 193 | } // WHILE 194 | } 195 | catch (IOException ex) 196 | { 197 | OnLoadError(ex.Message); 198 | error = true; 199 | } 200 | finally 201 | { 202 | if (error == false) 203 | { 204 | DateTime end = DateTime.Now; 205 | 206 | OnProgressUpdate(100); 207 | OnLoadComplete(end - start, cancelled); 208 | } 209 | } 210 | }); 211 | } 212 | 213 | /// 214 | /// 215 | /// 216 | public void Dispose() 217 | { 218 | this.Searches = new Searches(); 219 | this.Lines.Clear(); 220 | this.LongestLine = new LogLine(); 221 | this.LineCount = 0; 222 | this.FileName = String.Empty; 223 | this.FilterIds = new List(); 224 | this.FilterIds.Clear(); 225 | this.ItemsSource = null; 226 | 227 | if (this.fileStream != null) 228 | { 229 | this.fileStream.Dispose(); 230 | } 231 | } 232 | 233 | /// 234 | /// 235 | /// 236 | /// 237 | /// 238 | public void SearchMulti(List scs, CancellationToken ct, int numContextLines) 239 | { 240 | Task.Run(() => { 241 | 242 | DateTime start = DateTime.Now; 243 | bool cancelled = false; 244 | long matches = 0; 245 | try 246 | { 247 | long counter = 0; 248 | string line = string.Empty; 249 | bool located = false; 250 | 251 | foreach (LogLine ll in this.Lines) 252 | { 253 | // Reset the match flag 254 | ll.SearchMatches.Clear(); 255 | ClearContextLine(ll.LineNumber, numContextLines); 256 | 257 | foreach (SearchCriteria sc in scs) 258 | { 259 | line = this.GetLine(ll.LineNumber); 260 | 261 | located = false; 262 | switch (sc.Type) 263 | { 264 | case Global.SearchType.SubStringCaseInsensitive: 265 | if (line.IndexOf(sc.Pattern, 0, StringComparison.OrdinalIgnoreCase) > -1) 266 | { 267 | located = true; 268 | } 269 | break; 270 | 271 | case Global.SearchType.SubStringCaseSensitive: 272 | if (line.IndexOf(sc.Pattern, 0, StringComparison.Ordinal) > -1) 273 | { 274 | located = true; 275 | } 276 | break; 277 | 278 | case Global.SearchType.RegexCaseInsensitive: 279 | if (Regex.Match(line, sc.Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled) != Match.Empty) 280 | { 281 | located = true; 282 | } 283 | break; 284 | 285 | case Global.SearchType.RegexCaseSensitive: 286 | if (Regex.Match(line, sc.Pattern, RegexOptions.Compiled) != Match.Empty) 287 | { 288 | located = true; 289 | } 290 | break; 291 | 292 | default: 293 | break; 294 | } 295 | 296 | if (located == true) 297 | { 298 | matches++; 299 | ll.SearchMatches.Add(sc.Id); 300 | 301 | if (numContextLines > 0) 302 | { 303 | this.SetContextLines(ll.LineNumber, numContextLines); 304 | } 305 | } 306 | } 307 | 308 | if (counter++ % 50 == 0) 309 | { 310 | OnProgressUpdate((int)((double)counter / (double)this.Lines.Count * 100)); 311 | 312 | if (ct.IsCancellationRequested) 313 | { 314 | cancelled = true; 315 | return; 316 | } 317 | } 318 | } 319 | } 320 | finally 321 | { 322 | DateTime end = DateTime.Now; 323 | 324 | OnProgressUpdate(100); 325 | OnSearchComplete(end - start, matches, scs.Count, cancelled); 326 | } 327 | }); 328 | } 329 | 330 | /// 331 | /// 332 | /// 333 | /// 334 | /// 335 | public void Search(SearchCriteria sc, bool cumulative, CancellationToken ct, int numContextLines) 336 | { 337 | Task.Run(() => { 338 | 339 | DateTime start = DateTime.Now; 340 | bool cancelled = false; 341 | long matches = 0; 342 | try 343 | { 344 | long counter = 0; 345 | string line = string.Empty; 346 | bool located = false; 347 | 348 | foreach (LogLine ll in this.Lines) 349 | { 350 | if (cumulative == false) 351 | { 352 | // Reset the match flag 353 | ll.SearchMatches.Clear(); 354 | //ll.IsContextLine = false; 355 | 356 | ClearContextLine(ll.LineNumber, numContextLines); 357 | } 358 | else 359 | { 360 | if (ll.SearchMatches.Count > 0) 361 | { 362 | continue; 363 | } 364 | } 365 | 366 | line = this.GetLine(ll.LineNumber); 367 | 368 | located = false; 369 | switch (sc.Type) 370 | { 371 | case Global.SearchType.SubStringCaseInsensitive: 372 | if (line.IndexOf(sc.Pattern, 0, StringComparison.OrdinalIgnoreCase) > -1) 373 | { 374 | located = true; 375 | } 376 | break; 377 | 378 | case Global.SearchType.SubStringCaseSensitive: 379 | if (line.IndexOf(sc.Pattern, 0, StringComparison.Ordinal) > -1) 380 | { 381 | located = true; 382 | } 383 | break; 384 | 385 | case Global.SearchType.RegexCaseInsensitive: 386 | if (Regex.Match(line, sc.Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled) != Match.Empty) 387 | { 388 | located = true; 389 | } 390 | break; 391 | 392 | case Global.SearchType.RegexCaseSensitive: 393 | if (Regex.Match(line, sc.Pattern, RegexOptions.Compiled) != Match.Empty) 394 | { 395 | located = true; 396 | } 397 | break; 398 | 399 | default: 400 | break; 401 | } 402 | 403 | if (located == false) 404 | { 405 | ll.SearchMatches.Remove(sc.Id); 406 | } 407 | else 408 | { 409 | matches++; 410 | ll.SearchMatches.Add(sc.Id); 411 | 412 | if (numContextLines > 0) 413 | { 414 | this.SetContextLines(ll.LineNumber, numContextLines); 415 | } 416 | } 417 | 418 | if (counter++ % 50 == 0) 419 | { 420 | OnProgressUpdate((int)((double)counter / (double)this.Lines.Count * 100)); 421 | 422 | if (ct.IsCancellationRequested) 423 | { 424 | cancelled = true; 425 | return; 426 | } 427 | } 428 | } 429 | } 430 | finally 431 | { 432 | DateTime end = DateTime.Now; 433 | 434 | OnProgressUpdate(100); 435 | OnSearchComplete(end - start, matches, 1, cancelled); 436 | } 437 | }); 438 | } 439 | 440 | /// 441 | /// 442 | /// 443 | /// 444 | public void Export(string filePath, CancellationToken ct) 445 | { 446 | this.ExportToFile((IEnumerable)this.ItemsSource, filePath, ct); 447 | } 448 | 449 | /// 450 | /// 451 | /// 452 | /// 453 | public void ExportSelected(string filePath, CancellationToken ct) 454 | { 455 | this.ExportToFile(listLines.SelectedItems.Cast(), filePath, ct); 456 | } 457 | 458 | /// 459 | /// 460 | /// 461 | public void ShowMultiSearch() 462 | { 463 | WindowMultiSearch wms = new WindowMultiSearch(this.Searches); 464 | // Hack to set owner from a user control?! 465 | HwndSource source = HwndSource.FromVisual(this) as HwndSource; 466 | if (source != null) 467 | { 468 | WindowInteropHelper helper = new WindowInteropHelper(wms); 469 | helper.Owner = source.Handle; 470 | } 471 | if (wms.ShowDialog() == false) 472 | { 473 | return; 474 | } 475 | 476 | // Clear any existing filter ID's as we will only show the multi-string search 477 | this.FilterIds.Clear(); 478 | this.Searches.Reset(); 479 | foreach (SearchCriteria sc in wms.NewSearches) 480 | { 481 | // Add the ID so that any matches show up straight away 482 | this.FilterIds.Add(sc.Id); 483 | this.Searches.Add(sc); 484 | } 485 | 486 | OnMultiSearchInitiated(wms.NewSearches); 487 | } 488 | 489 | /// 490 | /// 491 | /// 492 | public void UpdateColumnWidths() 493 | { 494 | if (double.IsNaN(colData.Width)) 495 | { 496 | colData.Width = colData.ActualWidth; 497 | colData.Width = double.NaN; 498 | } 499 | 500 | if (double.IsNaN(colLineNum.Width)) 501 | { 502 | colLineNum.Width = colLineNum.ActualWidth + 50; 503 | colLineNum.Width = double.NaN; 504 | } 505 | } 506 | 507 | /// 508 | /// 509 | /// 510 | /// 511 | /// 512 | public string GetLine(int lineNumber) 513 | { 514 | if (lineNumber >= this.Lines.Count) 515 | { 516 | return string.Empty; 517 | } 518 | 519 | byte[] buffer = new byte[this.Lines[lineNumber].CharCount + 1]; 520 | try 521 | { 522 | this.readMutex.WaitOne(); 523 | this.fileStream.Seek(this.Lines[lineNumber].Offset, SeekOrigin.Begin); 524 | this.fileStream.Read(buffer, 0, this.Lines[lineNumber].CharCount); 525 | this.readMutex.ReleaseMutex(); 526 | } 527 | catch (Exception) { } 528 | 529 | return Regex.Replace(Encoding.ASCII.GetString(buffer), "[\0-\b\n\v\f\x000E-\x001F\x007F-ÿ]", "", RegexOptions.Compiled); 530 | } 531 | #endregion 532 | 533 | #region Properties 534 | /// 535 | /// 536 | /// 537 | public System.Collections.IEnumerable ItemsSource 538 | { 539 | get 540 | { 541 | return listLines.ItemsSource; 542 | } 543 | set 544 | { 545 | listLines.ItemsSource = value; 546 | } 547 | } 548 | #endregion 549 | 550 | #region Private Methods 551 | /// 552 | /// 553 | /// 554 | /// 555 | /// 556 | private void SetContextLines(long lineNumber, int numLines) 557 | { 558 | long temp = numLines; 559 | if (lineNumber < this.Lines.Count) 560 | { 561 | if (numLines + lineNumber > this.Lines.Count - 1) 562 | { 563 | temp = this.Lines.Count - lineNumber - 1; 564 | } 565 | for (int index = 1; index <= temp; index++) 566 | { 567 | this.Lines[(int)lineNumber + index].IsContextLine = true; 568 | } 569 | } 570 | 571 | if (lineNumber > 0) 572 | { 573 | if (lineNumber - numLines < 0) 574 | { 575 | temp = lineNumber; 576 | } 577 | for (int index = 1; index <= temp; index++) 578 | { 579 | this.Lines[(int)lineNumber - index].IsContextLine = true; 580 | } 581 | } 582 | } 583 | 584 | /// 585 | /// Clear the line that is the next after the farthest context 586 | /// line, so the flag is reset and we won't overwrite 587 | /// 588 | /// 589 | /// 590 | private void ClearContextLine(long lineNumber, int numLines) 591 | { 592 | if ((int)lineNumber + numLines + 1 < this.Lines.Count - 1) 593 | { 594 | this.Lines[(int)lineNumber + numLines + 1].IsContextLine = false; 595 | } 596 | } 597 | 598 | /// 599 | /// 600 | /// 601 | /// 602 | /// 603 | private void ExportToFile(IEnumerable lines, string filePath, CancellationToken ct) 604 | { 605 | Task.Run(() => 606 | { 607 | DateTime start = DateTime.Now; 608 | bool cancelled = false; 609 | try 610 | { 611 | using (FileStream fs = new FileStream(filePath, FileMode.Create)) 612 | { 613 | string lineStr = string.Empty; 614 | byte[] lineBytes; 615 | byte[] endLine = new byte[2] { 13, 10 }; 616 | 617 | long counter = 0; 618 | foreach (LogLine ll in lines) 619 | { 620 | lineStr = this.GetLine(ll.LineNumber); 621 | lineBytes = Encoding.ASCII.GetBytes(lineStr); 622 | fs.Write(lineBytes, 0, lineBytes.Length); 623 | // Add \r\n 624 | fs.Write(endLine, 0, 2); 625 | 626 | if (counter++ % 50 == 0) 627 | { 628 | OnProgressUpdate((int)((double)counter / (double)Lines.Count * 100)); 629 | 630 | if (ct.IsCancellationRequested) 631 | { 632 | cancelled = true; 633 | return; 634 | } 635 | } 636 | } 637 | 638 | } 639 | } 640 | finally 641 | { 642 | DateTime end = DateTime.Now; 643 | 644 | OnProgressUpdate(100); 645 | OnExportComplete(end - start, cancelled); 646 | } 647 | }); 648 | } 649 | 650 | /// 651 | /// 652 | /// 653 | /// 654 | /// 655 | private void AddLine(long offset, int charCount) 656 | { 657 | LogLine ll = new LogLine 658 | { 659 | Control = this, 660 | Offset = offset, 661 | CharCount = charCount, 662 | LineNumber = this.LineCount 663 | }; 664 | 665 | this.Lines.Add(ll); 666 | 667 | if (charCount > this.LongestLine.CharCount) 668 | { 669 | this.LongestLine.CharCount = charCount; 670 | this.LongestLine.LineNumber = ll.LineNumber; 671 | } 672 | 673 | this.LineCount++; 674 | } 675 | #endregion 676 | 677 | #region Event Methods 678 | /// 679 | /// 680 | /// 681 | private void OnExportInitiated(bool all) 682 | { 683 | ExportInitiated?.Invoke(this, all); 684 | } 685 | 686 | /// 687 | /// 688 | /// 689 | private void OnMultiSearchInitiated(List searches) 690 | { 691 | MultiSearchInitiated?.Invoke(this, searches); 692 | } 693 | 694 | /// 695 | /// 696 | /// 697 | private void OnLoadError(string message) 698 | { 699 | LoadError?.Invoke(this.FileName, message); 700 | } 701 | 702 | /// 703 | /// 704 | /// 705 | private void OnProgressUpdate(int progress) 706 | { 707 | ProgressUpdate?.Invoke(progress); 708 | } 709 | 710 | /// 711 | /// 712 | /// 713 | private void OnLoadComplete(TimeSpan duration, bool cancelled) 714 | { 715 | LoadComplete?.Invoke(this, this.FileName, duration, cancelled); 716 | } 717 | 718 | /// 719 | /// 720 | /// 721 | private void OnExportComplete(TimeSpan duration, bool cancelled) 722 | { 723 | ExportComplete?.Invoke(this, this.FileName, duration, cancelled); 724 | } 725 | 726 | /// 727 | /// 728 | /// 729 | private void OnSearchComplete(TimeSpan duration, long matches, int numTerms, bool cancelled) 730 | { 731 | SearchComplete?.Invoke(this, this.FileName, duration, matches, numTerms, cancelled); 732 | } 733 | #endregion 734 | 735 | #region Filter Methods 736 | /// 737 | /// 738 | /// 739 | /// 740 | /// 741 | private bool ShowFilter(object item) 742 | { 743 | var ll = item as LogLine; 744 | return ll != null && (ll.SearchMatches.Intersect(this.FilterIds).Any() == true || (ll.IsContextLine == true)); 745 | } 746 | 747 | /// 748 | /// 749 | /// 750 | /// 751 | /// 752 | private bool HideFilter(object item) 753 | { 754 | var ll = item as LogLine; 755 | return ll != null && (ll.SearchMatches.Intersect(this.FilterIds).Any() == false); 756 | } 757 | #endregion 758 | 759 | #region Context Menu Event Handlers 760 | /// 761 | /// 762 | /// 763 | /// 764 | /// 765 | private void CtxMenuFilteringShowMatched_Click(object sender, System.Windows.RoutedEventArgs e) 766 | { 767 | CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.ItemsSource); 768 | view.Filter = ShowFilter; 769 | } 770 | 771 | /// 772 | /// 773 | /// 774 | /// 775 | /// 776 | private void CtxMenuFilteringHideMatched_Click(object sender, System.Windows.RoutedEventArgs e) 777 | { 778 | CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.ItemsSource); 779 | view.Filter = HideFilter; 780 | } 781 | 782 | /// 783 | /// 784 | /// 785 | /// 786 | /// 787 | private void CtxMenuFilteringClear_Click(object sender, System.Windows.RoutedEventArgs e) 788 | { 789 | CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.ItemsSource); 790 | view.Filter = null; 791 | } 792 | 793 | /// 794 | /// 795 | /// 796 | /// 797 | /// 798 | private void CtxMenuSearchViewTerms_Click(object sender, System.Windows.RoutedEventArgs e) 799 | { 800 | WindowSearchTerms wst = new WindowSearchTerms(this.Searches); 801 | // Hack to set owner from a user control?! 802 | HwndSource source = HwndSource.FromVisual(this) as HwndSource; 803 | if (source != null) 804 | { 805 | WindowInteropHelper helper = new WindowInteropHelper(wst); 806 | helper.Owner = source.Handle; 807 | } 808 | if (wst.ShowDialog() == false) 809 | { 810 | return; 811 | } 812 | 813 | this.Searches = wst.Searches; 814 | this.FilterIds.Clear(); 815 | foreach (SearchCriteria sc in this.Searches.Items) 816 | { 817 | if (sc.Enabled == false) 818 | { 819 | continue; 820 | } 821 | 822 | this.FilterIds.Add(sc.Id); 823 | } 824 | 825 | listLines.ItemsSource = null; 826 | listLines.ItemsSource = this.Lines; 827 | } 828 | 829 | /// 830 | /// 831 | /// 832 | /// 833 | /// 834 | private void CtxMenuExportAll_Click(object sender, System.Windows.RoutedEventArgs e) 835 | { 836 | OnExportInitiated(true); 837 | } 838 | 839 | /// 840 | /// 841 | /// 842 | /// 843 | /// 844 | private void CtxMenuExportSelected_Click(object sender, System.Windows.RoutedEventArgs e) 845 | { 846 | OnExportInitiated(false); 847 | } 848 | 849 | /// 850 | /// 851 | /// 852 | /// 853 | /// 854 | private void CtxMenuCopy_Click(object sender, System.Windows.RoutedEventArgs e) 855 | { 856 | StringBuilder sb = new StringBuilder(); 857 | //LogFile lf = logs[tabControl.SelectedTab.Tag.ToString()]; 858 | 859 | foreach (LogLine ll in listLines.SelectedItems) 860 | { 861 | sb.AppendLine(this.GetLine(ll.LineNumber)); 862 | } 863 | 864 | Clipboard.SetText(sb.ToString()); 865 | } 866 | 867 | /// 868 | /// 869 | /// 870 | /// 871 | /// 872 | private void CtxMenu_ContextMenuOpening(object sender, System.Windows.Controls.ContextMenuEventArgs e) 873 | { 874 | bool enableLineOps = true; 875 | if (this.LineCount == 0) 876 | { 877 | enableLineOps = false; 878 | } 879 | 880 | ctxMenuLinesFirst.IsEnabled = enableLineOps; 881 | ctxMenuLinesLast.IsEnabled = enableLineOps; 882 | ctxMenuLinesGoTo.IsEnabled = enableLineOps; 883 | 884 | if (listLines.SelectedItems.Count > this.config.MultiSelectLimit) 885 | { 886 | ctxMenuCopy.IsEnabled = false; 887 | ctxMenuExportSelected.IsEnabled = false; 888 | return; 889 | } 890 | 891 | ctxMenuCopy.IsEnabled = true; 892 | ctxMenuExportSelected.IsEnabled = true; 893 | } 894 | 895 | /// 896 | /// 897 | /// 898 | /// 899 | /// 900 | private void CtxMenuLinesGoTo_Click(object sender, System.Windows.RoutedEventArgs e) 901 | { 902 | WindowGoToLine wgtl = new WindowGoToLine(this.LineCount); 903 | // Hack to set owner from a user control?! 904 | HwndSource source = HwndSource.FromVisual(this) as HwndSource; 905 | if (source != null) 906 | { 907 | WindowInteropHelper helper = new WindowInteropHelper(wgtl); 908 | helper.Owner = source.Handle; 909 | } 910 | 911 | if (wgtl.ShowDialog() == false) 912 | { 913 | return; 914 | } 915 | 916 | this.listLines.ScrollIntoView(this.listLines.Items[wgtl.LineNo]); 917 | this.listLines.SelectedItem = this.listLines.Items[wgtl.LineNo]; 918 | } 919 | 920 | /// 921 | /// 922 | /// 923 | /// 924 | /// 925 | private void CtxMenuLinesFirst_Click(object sender, System.Windows.RoutedEventArgs e) 926 | { 927 | this.listLines.ScrollIntoView(this.listLines.Items[0]); 928 | this.listLines.SelectedItem = this.listLines.Items[0]; 929 | } 930 | 931 | /// 932 | /// 933 | /// 934 | /// 935 | /// 936 | private void CtxMenuLinesLast_Click(object sender, System.Windows.RoutedEventArgs e) 937 | { 938 | this.listLines.ScrollIntoView(this.listLines.Items[this.LineCount - 1]); 939 | this.listLines.SelectedItem = this.listLines.Items[this.LineCount - 1]; 940 | } 941 | #endregion 942 | 943 | #region Listview Event Handlers 944 | /// 945 | /// 946 | /// 947 | /// 948 | /// 949 | private void ListLines_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) 950 | { 951 | if (listLines.SelectedItems.Count != 1) 952 | { 953 | return; 954 | } 955 | 956 | LogLine ll = (LogLine)listLines.SelectedItem; 957 | 958 | WindowLine wl = new WindowLine(this.GetLine(ll.LineNumber)); 959 | // Hack to set owner from a user control?! 960 | HwndSource source = HwndSource.FromVisual(this) as HwndSource; 961 | if (source != null) 962 | { 963 | WindowInteropHelper helper = new WindowInteropHelper(wl); 964 | helper.Owner = source.Handle; 965 | } 966 | 967 | wl.ShowDialog(); 968 | } 969 | #endregion 970 | } 971 | } 972 | -------------------------------------------------------------------------------- /Source/Global.cs: -------------------------------------------------------------------------------- 1 | namespace LogViewer2 2 | { 3 | public class Global 4 | { 5 | #region Enums 6 | /// 7 | /// 8 | /// 9 | public enum SearchType 10 | { 11 | SubStringCaseInsensitive = 0, 12 | SubStringCaseSensitive = 1, 13 | RegexCaseInsensitive = 2, 14 | RegexCaseSensitive = 3, 15 | } 16 | 17 | /// 18 | /// 19 | /// 20 | public enum ViewMode 21 | { 22 | Standard = 1, 23 | FilterShow = 2, 24 | FilterHide = 3 25 | } 26 | #endregion 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Icons/App.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/Source/Icons/App.png -------------------------------------------------------------------------------- /Source/Icons/Line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/Source/Icons/Line.png -------------------------------------------------------------------------------- /Source/Icons/Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/Source/Icons/Search.png -------------------------------------------------------------------------------- /Source/Icons/SearchTerms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/Source/Icons/SearchTerms.png -------------------------------------------------------------------------------- /Source/LogFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using System.Windows.Controls; 12 | 13 | namespace LogViewer2 14 | { 15 | /// 16 | /// 17 | /// 18 | internal class LogFile 19 | { 20 | #region Delegates 21 | public delegate void SearchCompleteEvent(LogFile lf, string fileName, TimeSpan duration, long matches, int numSearchTerms, bool cancelled); 22 | public delegate void CompleteEvent(LogFile lf, string fileName, TimeSpan duration, bool cancelled); 23 | public delegate void BoolEvent(string fileName, bool val); 24 | public delegate void MessageEvent(string fileName, string message); 25 | public delegate void ProgressUpdateEvent(int percent); 26 | #endregion 27 | 28 | #region Events 29 | public event SearchCompleteEvent SearchComplete; 30 | public event CompleteEvent LoadComplete; 31 | public event CompleteEvent ExportComplete; 32 | public event ProgressUpdateEvent ProgressUpdate; 33 | public event MessageEvent LoadError; 34 | #endregion 35 | 36 | #region Member Variables 37 | //private Color highlightColour { get; set; } = Color.Lime; 38 | // private Color contextColour { get; set; } = Color.LightGray; 39 | public Searches Searches { get; set; } 40 | public Global.ViewMode ViewMode { get; set; } = Global.ViewMode.Standard; 41 | public List Lines { get; private set; } = new List(); 42 | public LogLine LongestLine { get; private set; } = new LogLine(); 43 | public int LineCount { get; private set; } = 0; 44 | private FileStream fileStream; 45 | private Mutex readMutex = new Mutex(); 46 | public string FileName { get; private set; } 47 | public List FilterIds { get; private set; } = new List(); 48 | public ControlLog List { get; set; } 49 | public string Guid { get; private set; } 50 | #endregion 51 | 52 | /// 53 | /// 54 | /// 55 | public LogFile() 56 | { 57 | this.Guid = System.Guid.NewGuid().ToString(); 58 | this.Searches = new Searches(); 59 | } 60 | 61 | #region Public Methods 62 | /// 63 | /// 64 | /// 65 | /// 66 | /// 67 | public void Load(string filePath, SynchronizationContext st, CancellationToken ct) 68 | { 69 | this.Dispose(); 70 | this.FileName = Path.GetFileName(filePath); 71 | 72 | Task.Run(() => { 73 | 74 | DateTime start = DateTime.Now; 75 | bool cancelled = false; 76 | bool error = false; 77 | try 78 | { 79 | byte[] tempBuffer = new byte[1024 * 1024]; 80 | 81 | this.fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); 82 | FileInfo fileInfo = new FileInfo(filePath); 83 | 84 | // Calcs and finally point the position to the end of the line 85 | long position = 0; 86 | // Holds the offset to the start of the next line 87 | long lineStartOffset = 0; 88 | // Checks if we have read less than requested e.g. buffer not filled/end of file 89 | bool lastSection = false; 90 | // Counter for process reporting 91 | int counter = 0; 92 | // Holds a counter to start checking for the next indexOf('\r') 93 | int startIndex = 0; 94 | // Once all of the \r (lines) have been emnumerated, there might still be data left in the 95 | // buffer, so this holds the number of bytes that need to be added onto the next line 96 | int bufferRemainder = 0; 97 | // Holds how many bytes were read from the last file stream read 98 | int numBytesRead = 0; 99 | // Holds the temporary string generated from the file stream buffer 100 | string tempStr = string.Empty; 101 | // Length of the current line 102 | int charCount; 103 | // Return value from IndexOf function 104 | int indexOf; 105 | 106 | while (position < this.fileStream.Length) 107 | { 108 | numBytesRead = this.fileStream.Read(tempBuffer, 0, 1024 * 1024); 109 | if (numBytesRead < 1048576) 110 | 111 | { 112 | lastSection = true; 113 | } 114 | 115 | tempStr = Encoding.ASCII.GetString(tempBuffer).Substring(0, numBytesRead); 116 | startIndex = 0; 117 | 118 | // Does the buffer contain at least one "\n", so now enumerate all instances of "\n" 119 | if (tempStr.IndexOf('\n') != -1) 120 | { 121 | while ((indexOf = tempStr.IndexOf('\n', startIndex)) != -1 && startIndex < numBytesRead) 122 | { 123 | if (indexOf != -1) 124 | { 125 | charCount = 0; 126 | 127 | // Check if the line contains a CR as well, if it does then we remove the last char as the char count 128 | if (indexOf != 0 && (int)tempBuffer[Math.Max(0, indexOf - 1)] == 13) 129 | { 130 | charCount = bufferRemainder + (indexOf - startIndex - 1); 131 | position += (long)charCount + 2L; 132 | } 133 | else 134 | { 135 | charCount = bufferRemainder + (indexOf - startIndex); 136 | position += (long)charCount + 1L; 137 | } 138 | 139 | AddLine(lineStartOffset, charCount); 140 | 141 | // The remaining number in the buffer gets set to 0 e.g. after 142 | //the first iteration as it would add onto the first line 143 | bufferRemainder = 0; 144 | 145 | // Set the offset to the end of the last line that has just been added 146 | lineStartOffset = position; 147 | startIndex = indexOf + 1; 148 | } 149 | } 150 | 151 | // We had some '\r' in the last buffer read, now they are processing, so just add the rest as the last line 152 | if (lastSection == true) 153 | { 154 | AddLine(lineStartOffset, bufferRemainder + (numBytesRead - startIndex)); 155 | return; 156 | } 157 | 158 | bufferRemainder += numBytesRead - startIndex; 159 | } 160 | else 161 | { 162 | // The entire content of the buffer doesn't contain \r so just add the rest of content as the last line 163 | if (lastSection == true) 164 | { 165 | AddLine(lineStartOffset, bufferRemainder + (numBytesRead - startIndex)); 166 | return; 167 | } 168 | 169 | bufferRemainder += numBytesRead; 170 | } 171 | 172 | if (counter++ % 50 == 0) 173 | { 174 | OnProgressUpdate((int)((double)position / (double)fileInfo.Length * 100)); 175 | 176 | if (ct.IsCancellationRequested) 177 | { 178 | cancelled = true; 179 | return; 180 | } 181 | } 182 | } // WHILE 183 | } 184 | catch (IOException ex) 185 | { 186 | OnLoadError(ex.Message); 187 | error = true; 188 | } 189 | finally 190 | { 191 | if (error == false) 192 | { 193 | DateTime end = DateTime.Now; 194 | 195 | OnProgressUpdate(100); 196 | OnLoadComplete(end - start, cancelled); 197 | } 198 | } 199 | }); 200 | } 201 | 202 | /// 203 | /// 204 | /// 205 | public void Dispose() 206 | { 207 | this.Searches = new Searches(); 208 | this.Lines.Clear(); 209 | this.LongestLine = new LogLine(); 210 | this.LineCount = 0; 211 | this.FileName = String.Empty; 212 | this.FilterIds = new List(); 213 | //this.List.ModelFilter = null; 214 | this.FilterIds.Clear(); 215 | //this.List.ClearObjects(); 216 | 217 | if (this.fileStream != null) 218 | { 219 | this.fileStream.Dispose(); 220 | } 221 | } 222 | 223 | /// 224 | /// 225 | /// 226 | /// 227 | /// 228 | public void SearchMulti(List scs, CancellationToken ct, int numContextLines) 229 | { 230 | Task.Run(() => { 231 | 232 | DateTime start = DateTime.Now; 233 | bool cancelled = false; 234 | long matches = 0; 235 | try 236 | { 237 | long counter = 0; 238 | string line = string.Empty; 239 | bool located = false; 240 | 241 | foreach (LogLine ll in this.Lines) 242 | { 243 | // Reset the match flag 244 | ll.SearchMatches.Clear(); 245 | ClearContextLine(ll.LineNumber, numContextLines); 246 | 247 | foreach (SearchCriteria sc in scs) 248 | { 249 | line = this.GetLine(ll.LineNumber); 250 | 251 | located = false; 252 | switch (sc.Type) 253 | { 254 | case Global.SearchType.SubStringCaseInsensitive: 255 | if (line.IndexOf(sc.Pattern, 0, StringComparison.OrdinalIgnoreCase) > -1) 256 | { 257 | located = true; 258 | } 259 | break; 260 | 261 | case Global.SearchType.SubStringCaseSensitive: 262 | if (line.IndexOf(sc.Pattern, 0, StringComparison.Ordinal) > -1) 263 | { 264 | located = true; 265 | } 266 | break; 267 | 268 | case Global.SearchType.RegexCaseInsensitive: 269 | if (Regex.Match(line, sc.Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled) != Match.Empty) 270 | { 271 | located = true; 272 | } 273 | break; 274 | 275 | case Global.SearchType.RegexCaseSensitive: 276 | if (Regex.Match(line, sc.Pattern, RegexOptions.Compiled) != Match.Empty) 277 | { 278 | located = true; 279 | } 280 | break; 281 | 282 | default: 283 | break; 284 | } 285 | 286 | if (located == true) 287 | { 288 | matches++; 289 | ll.SearchMatches.Add(sc.Id); 290 | 291 | if (numContextLines > 0) 292 | { 293 | this.SetContextLines(ll.LineNumber, numContextLines); 294 | } 295 | } 296 | } 297 | 298 | if (counter++ % 50 == 0) 299 | { 300 | OnProgressUpdate((int)((double)counter / (double)this.Lines.Count * 100)); 301 | 302 | if (ct.IsCancellationRequested) 303 | { 304 | cancelled = true; 305 | return; 306 | } 307 | } 308 | } 309 | } 310 | finally 311 | { 312 | DateTime end = DateTime.Now; 313 | 314 | OnProgressUpdate(100); 315 | OnSearchComplete(end - start, matches, scs.Count, cancelled); 316 | } 317 | }); 318 | } 319 | 320 | /// 321 | /// 322 | /// 323 | /// 324 | /// 325 | public void Search(SearchCriteria sc, bool cumulative, CancellationToken ct, int numContextLines) 326 | { 327 | Task.Run(() => { 328 | 329 | DateTime start = DateTime.Now; 330 | bool cancelled = false; 331 | long matches = 0; 332 | try 333 | { 334 | long counter = 0; 335 | string line = string.Empty; 336 | bool located = false; 337 | 338 | foreach (LogLine ll in this.Lines) 339 | { 340 | if (cumulative == false) 341 | { 342 | // Reset the match flag 343 | ll.SearchMatches.Clear(); 344 | //ll.IsContextLine = false; 345 | 346 | ClearContextLine(ll.LineNumber, numContextLines); 347 | } 348 | else 349 | { 350 | if (ll.SearchMatches.Count > 0) { 351 | continue; 352 | } 353 | } 354 | 355 | line = this.GetLine(ll.LineNumber); 356 | 357 | located = false; 358 | switch (sc.Type) 359 | { 360 | case Global.SearchType.SubStringCaseInsensitive: 361 | if (line.IndexOf(sc.Pattern, 0, StringComparison.OrdinalIgnoreCase) > -1) 362 | { 363 | located = true; 364 | } 365 | break; 366 | 367 | case Global.SearchType.SubStringCaseSensitive: 368 | if (line.IndexOf(sc.Pattern, 0, StringComparison.Ordinal) > -1) 369 | { 370 | located = true; 371 | } 372 | break; 373 | 374 | case Global.SearchType.RegexCaseInsensitive: 375 | if (Regex.Match(line, sc.Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled) != Match.Empty) 376 | { 377 | located = true; 378 | } 379 | break; 380 | 381 | case Global.SearchType.RegexCaseSensitive: 382 | if (Regex.Match(line, sc.Pattern, RegexOptions.Compiled) != Match.Empty) 383 | { 384 | located = true; 385 | } 386 | break; 387 | 388 | default: 389 | break; 390 | } 391 | 392 | if (located == false) 393 | { 394 | ll.SearchMatches.Remove(sc.Id); 395 | } 396 | else 397 | { 398 | matches++; 399 | ll.SearchMatches.Add(sc.Id); 400 | 401 | if (numContextLines > 0) 402 | { 403 | this.SetContextLines(ll.LineNumber, numContextLines); 404 | } 405 | } 406 | 407 | if (counter++ % 50 == 0) 408 | { 409 | OnProgressUpdate((int)((double)counter / (double)this.Lines.Count * 100)); 410 | 411 | if (ct.IsCancellationRequested) 412 | { 413 | cancelled = true; 414 | return; 415 | } 416 | } 417 | } 418 | } 419 | finally 420 | { 421 | DateTime end = DateTime.Now; 422 | 423 | OnProgressUpdate(100); 424 | OnSearchComplete(end - start, matches, 1, cancelled); 425 | } 426 | }); 427 | } 428 | 429 | /// 430 | /// 431 | /// 432 | /// 433 | /// 434 | public void Export(string filePath, CancellationToken ct) 435 | { 436 | this.ExportToFile(this.Lines, filePath, ct); 437 | } 438 | 439 | /// 440 | /// 441 | /// 442 | /// 443 | /// 444 | /// 445 | public void Export(IEnumerable lines, string filePath, CancellationToken ct) 446 | { 447 | this.ExportToFile(lines, filePath, ct); 448 | } 449 | #endregion 450 | 451 | //public TabItem Initialise(string filePath) 452 | //{ 453 | // //OLVColumn colLineNumber = ((OLVColumn)(new OLVColumn())); 454 | // //OLVColumn colText = ((OLVColumn)(new OLVColumn())); 455 | 456 | // //colLineNumber.Text = "Line No."; 457 | // //colLineNumber.Width = 95; 458 | // //colText.Text = "Data"; 459 | 460 | // //colLineNumber.AspectGetter = delegate (object x) 461 | // //{ 462 | // // if (((LogLine)x) == null) 463 | // // { 464 | // // return ""; 465 | // // } 466 | 467 | // // return (((LogLine)x).LineNumber + 1); 468 | // //}; 469 | 470 | // //colText.AspectGetter = delegate (object x) 471 | // //{ 472 | // // if (((LogLine)x) == null) 473 | // // { 474 | // // return ""; 475 | // // } 476 | 477 | // // return (this.GetLine(((LogLine)x).LineNumber)); 478 | // //}; 479 | 480 | // //FastObjectListView lv = new FastObjectListView(); 481 | 482 | // //lv.AllColumns.Add(colLineNumber); 483 | // //lv.AllColumns.Add(colText); 484 | // //lv.AllowDrop = true; 485 | // //lv.AutoArrange = false; 486 | // //lv.CausesValidation = false; 487 | // //lv.CellEditUseWholeCell = false; 488 | // //lv.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { 489 | // //colLineNumber, 490 | // //colText}); 491 | // ////lv.ContextMenuStrip = this.contextMenu; 492 | // //lv.Dock = System.Windows.Forms.DockStyle.Fill; 493 | // //lv.Font = new System.Drawing.Font("Consolas", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 494 | // //lv.FullRowSelect = true; 495 | // //lv.GridLines = true; 496 | // //lv.HasCollapsibleGroups = false; 497 | // //lv.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable; 498 | // //lv.HideSelection = false; 499 | // //lv.IsSearchOnSortColumn = false; 500 | // //lv.Location = new System.Drawing.Point(3, 3); 501 | // //lv.Margin = new System.Windows.Forms.Padding(4); 502 | // //lv.Name = "listLines0"; 503 | // //lv.SelectColumnsMenuStaysOpen = false; 504 | // //lv.SelectColumnsOnRightClick = false; 505 | // //lv.SelectColumnsOnRightClickBehaviour = BrightIdeasSoftware.ObjectListView.ColumnSelectBehaviour.None; 506 | // //lv.ShowFilterMenuOnRightClick = false; 507 | // //lv.ShowGroups = false; 508 | // //lv.ShowSortIndicators = false; 509 | // //lv.Size = new System.Drawing.Size(1679, 940); 510 | // //lv.TabIndex = 1; 511 | // //lv.TriggerCellOverEventsWhenOverHeader = false; 512 | // //lv.UseCompatibleStateImageBehavior = false; 513 | // //lv.UseFiltering = true; 514 | // //lv.View = System.Windows.Forms.View.Details; 515 | // //lv.VirtualMode = true; 516 | // //lv.Tag = this.Guid; 517 | // //lv.FormatRow += new System.EventHandler(this.FormatRow); 518 | 519 | // ControlLog cl = new ControlLog(); 520 | 521 | // this.List = cl; 522 | 523 | // TabItem tp = new TabItem 524 | // { 525 | // Content = cl, 526 | // //tp.Location = new System.Drawing.Point(4, 33); 527 | // //tp.Name = "tabPage" + this.Guid; 528 | // //tp.Padding = new System.Windows.Forms.Padding(3); 529 | // //tp.Size = new System.Drawing.Size(1685, 946); 530 | // //tp.TabIndex = 0; 531 | // Header = "Loading...", 532 | // //tp.UseVisualStyleBackColor = true; 533 | // //tp.Tag = this.Guid; 534 | // // 535 | 536 | // ToolTip = filePath 537 | // }; 538 | 539 | // return tp; 540 | //} 541 | 542 | ///// 543 | ///// 544 | ///// 545 | //public void SetContextMenu(ContextMenuStrip ctx) 546 | //{ 547 | // this.List.ContextMenuStrip = ctx; 548 | //} 549 | 550 | ///// 551 | ///// 552 | ///// 553 | ///// 554 | ///// 555 | //public void FormatRow(object sender, FormatRowEventArgs e) 556 | //{ 557 | // //if (this.viewMode != Global.ViewMode.FilterHide) 558 | // //{ 559 | // if ((LogLine)e.Model == null) 560 | // { 561 | // return; 562 | // } 563 | 564 | // if (((LogLine)e.Model).SearchMatches.Intersect(this.FilterIds).Any() == true) 565 | // { 566 | // e.Item.BackColor = highlightColour; 567 | // } 568 | // else if (((LogLine)e.Model).IsContextLine == true) 569 | // { 570 | // e.Item.BackColor = contextColour; 571 | // } 572 | // //} 573 | //} 574 | 575 | /// 576 | /// 577 | /// 578 | /// 579 | /// 580 | private void SetContextLines(long lineNumber, int numLines) 581 | { 582 | long temp = numLines; 583 | if (lineNumber < this.Lines.Count) 584 | { 585 | if (numLines + lineNumber > this.Lines.Count - 1) 586 | { 587 | temp = this.Lines.Count - lineNumber - 1; 588 | } 589 | for (int index = 1; index <= temp; index++) 590 | { 591 | this.Lines[(int)lineNumber + index].IsContextLine = true; 592 | } 593 | } 594 | 595 | if (lineNumber > 0) 596 | { 597 | if (lineNumber - numLines < 0) 598 | { 599 | temp = lineNumber; 600 | } 601 | for (int index = 1; index <= temp; index++) 602 | { 603 | this.Lines[(int)lineNumber - index].IsContextLine = true; 604 | } 605 | } 606 | } 607 | 608 | /// 609 | /// Clear the line that is the next after the farthest context 610 | /// line, so the flag is reset and we won't overwrite 611 | /// 612 | /// 613 | /// 614 | private void ClearContextLine(long lineNumber, int numLines) 615 | { 616 | long temp = numLines; 617 | if ((int)lineNumber + numLines + 1 < this.Lines.Count - 1) 618 | { 619 | this.Lines[(int)lineNumber + numLines + 1].IsContextLine = false; 620 | } 621 | } 622 | 623 | /// 624 | /// 625 | /// 626 | /// 627 | /// 628 | private void ExportToFile(IEnumerable lines, string filePath, CancellationToken ct) 629 | { 630 | Task.Run(() => 631 | { 632 | DateTime start = DateTime.Now; 633 | bool cancelled = false; 634 | try 635 | { 636 | using (FileStream fs = new FileStream(filePath, FileMode.Create)) 637 | { 638 | string lineStr = string.Empty; 639 | byte[] lineBytes; 640 | byte[] endLine = new byte[2] { 13, 10 }; 641 | 642 | long counter = 0; 643 | foreach (LogLine ll in lines) 644 | { 645 | lineStr = this.GetLine(ll.LineNumber); 646 | lineBytes = Encoding.ASCII.GetBytes(lineStr); 647 | fs.Write(lineBytes, 0, lineBytes.Length); 648 | // Add \r\n 649 | fs.Write(endLine, 0, 2); 650 | 651 | if (counter++ % 50 == 0) 652 | { 653 | OnProgressUpdate((int)((double)counter / (double)Lines.Count * 100)); 654 | 655 | if (ct.IsCancellationRequested) 656 | { 657 | cancelled = true; 658 | return; 659 | } 660 | } 661 | } 662 | 663 | } 664 | } 665 | finally 666 | { 667 | DateTime end = DateTime.Now; 668 | 669 | OnProgressUpdate(100); 670 | OnExportComplete(end - start, cancelled); 671 | } 672 | }); 673 | } 674 | 675 | /// 676 | /// 677 | /// 678 | /// 679 | /// 680 | private void AddLine(long offset, int charCount) 681 | { 682 | LogLine ll = new LogLine(); 683 | // ll.lf = this; 684 | ll.Offset = offset; 685 | ll.CharCount = charCount; 686 | ll.LineNumber = this.LineCount; 687 | this.Lines.Add(ll); 688 | if (charCount > this.LongestLine.CharCount) 689 | { 690 | this.LongestLine.CharCount = charCount; 691 | this.LongestLine.LineNumber = ll.LineNumber; 692 | } 693 | 694 | this.LineCount++; 695 | } 696 | 697 | /// 698 | /// 699 | /// 700 | /// 701 | /// 702 | public string GetLine(int lineNumber) 703 | { 704 | if (lineNumber >= this.Lines.Count) 705 | { 706 | return string.Empty; 707 | } 708 | 709 | byte[] buffer = new byte[this.Lines[lineNumber].CharCount + 1]; 710 | try 711 | { 712 | this.readMutex.WaitOne(); 713 | this.fileStream.Seek(this.Lines[lineNumber].Offset, SeekOrigin.Begin); 714 | this.fileStream.Read(buffer, 0, this.Lines[lineNumber].CharCount); 715 | this.readMutex.ReleaseMutex(); 716 | } 717 | catch (Exception){} 718 | 719 | return Regex.Replace(Encoding.ASCII.GetString(buffer), "[\0-\b\n\v\f\x000E-\x001F\x007F-ÿ]", "", RegexOptions.Compiled); 720 | } 721 | 722 | #region Event Methods 723 | /// 724 | /// 725 | /// 726 | private void OnLoadError(string message) 727 | { 728 | LoadError?.Invoke(this.FileName, message); 729 | } 730 | 731 | /// 732 | /// 733 | /// 734 | private void OnProgressUpdate(int progress) 735 | { 736 | ProgressUpdate?.Invoke(progress); 737 | } 738 | 739 | /// 740 | /// 741 | /// 742 | private void OnLoadComplete(TimeSpan duration, bool cancelled) 743 | { 744 | LoadComplete?.Invoke(this, this.FileName, duration, cancelled); 745 | } 746 | 747 | /// 748 | /// 749 | /// 750 | private void OnExportComplete(TimeSpan duration, bool cancelled) 751 | { 752 | ExportComplete?.Invoke(this, this.FileName, duration, cancelled); 753 | } 754 | 755 | /// 756 | /// 757 | /// 758 | private void OnSearchComplete(TimeSpan duration, long matches, int numTerms, bool cancelled) 759 | { 760 | SearchComplete?.Invoke(this, this.FileName, duration, matches, numTerms, cancelled); 761 | } 762 | #endregion 763 | } 764 | } 765 | -------------------------------------------------------------------------------- /Source/LogLine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace LogViewer2 5 | { 6 | /// 7 | /// 8 | /// 9 | public class LogLine 10 | { 11 | #region Member Variables/Properties 12 | public ControlLog Control { get; set; } 13 | public int LineNumber { get; set; } = 0; 14 | public int CharCount { get; set; } = 0; 15 | public long Offset { get; set; } = 0; 16 | public List SearchMatches { get; set; } = new List(); 17 | public bool IsContextLine { get; set; } = false; 18 | #endregion 19 | 20 | #region Properties 21 | /// 22 | /// 23 | /// 24 | public string Data 25 | { 26 | get 27 | { 28 | return this.Control.GetLine(this.LineNumber); 29 | } 30 | } 31 | 32 | /// 33 | /// 34 | /// 35 | public string BackgroundColor 36 | { 37 | get 38 | { 39 | if (this.SearchMatches.Intersect(Control.FilterIds).Any() == true) 40 | { 41 | return "LimeGreen";//System.Windows.Media.Brush. .FromRgb(66, 245, 105); 42 | } 43 | else if (this.IsContextLine == true) 44 | { 45 | // return System.Windows.Media.Color.FromRgb(142, 145, 143); 46 | return "Gray"; 47 | } 48 | 49 | return "White";//System.Windows.Media.Color.FromRgb(255, 255, 255); 50 | } 51 | } 52 | #endregion 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/LogViewer.toml: -------------------------------------------------------------------------------- 1 | HighlightColour = "Lime" 2 | ContextColour = "LightGray" 3 | MultiSelectLimit = 1000 4 | NumContextLines = 0 -------------------------------------------------------------------------------- /Source/LogViewer2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net7.0-windows 6 | enable 7 | true 8 | x64 9 | woanware ™️ 10 | 11 | App.ico 12 | 1.0.1 13 | 1.0.1 14 | woanware 15 | https://github.com/woanware/LogViewer2 16 | readme.md 17 | 1.0.1 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | True 30 | \ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | True 48 | \ 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Source/LogViewer2.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33103.184 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogViewer2", "LogViewer2.csproj", "{3CA6864B-0F21-4842-87AD-7D9EBC284E8F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {3CA6864B-0F21-4842-87AD-7D9EBC284E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {3CA6864B-0F21-4842-87AD-7D9EBC284E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {3CA6864B-0F21-4842-87AD-7D9EBC284E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {3CA6864B-0F21-4842-87AD-7D9EBC284E8F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {C30CCCEE-4724-4381-875B-2AA0CCDC11BA} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Source/Methods.cs: -------------------------------------------------------------------------------- 1 | namespace LogViewer2 2 | { 3 | class Methods 4 | { 5 | public static string GetApplicationDirectory() 6 | { 7 | return System.IO.Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Source/SearchCriteria.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace LogViewer2 3 | { 4 | /// 5 | /// 6 | /// 7 | public class SearchCriteria 8 | { 9 | public bool Enabled { get; set; } = true; 10 | public ushort Id { get; set; } = 0; 11 | public Global.SearchType Type { get; set; } 12 | public string Pattern { get; set; } = ""; 13 | public long Matches { get; set; } = 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/Searches.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace LogViewer2 5 | { 6 | /// 7 | /// 8 | /// 9 | public class Searches 10 | { 11 | #region Member Variables 12 | public List Items { get; private set; } 13 | private ushort counter = 0; // Basically a counter 14 | #endregion 15 | 16 | /// 17 | /// 18 | /// 19 | public Searches() 20 | { 21 | Reset(); 22 | } 23 | 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// The Id of the search criteria object 29 | public ushort Add(SearchCriteria sc, bool cumulative) 30 | { 31 | if (cumulative == false) 32 | { 33 | Reset(); 34 | } 35 | 36 | if (sc.Type == Global.SearchType.RegexCaseInsensitive || sc.Type == Global.SearchType.SubStringCaseInsensitive) 37 | { 38 | var ret = this.Items.SingleOrDefault(x => x.Pattern.ToLower() == sc.Pattern.ToLower()); 39 | if (ret != null) 40 | { 41 | return 0; 42 | } 43 | } 44 | else 45 | { 46 | var ret = this.Items.SingleOrDefault(x => x.Pattern == sc.Pattern); 47 | if (ret != null) 48 | { 49 | return 0; 50 | } 51 | } 52 | 53 | counter++; 54 | sc.Enabled = true; 55 | sc.Id = counter; 56 | this.Items.Add(sc); 57 | 58 | return sc.Id; 59 | } 60 | 61 | /// 62 | /// 63 | /// 64 | /// 65 | /// The Id of the search criteria object 66 | public ushort Add(SearchCriteria sc) 67 | { 68 | // If the SearchCriteria already exists then just enable it 69 | if (sc.Type == Global.SearchType.RegexCaseInsensitive || sc.Type == Global.SearchType.SubStringCaseInsensitive) 70 | { 71 | var ret = this.Items.SingleOrDefault(x => x.Pattern.ToLower() == sc.Pattern.ToLower()); 72 | if (ret != null) 73 | { 74 | ret.Enabled = true; 75 | return 0; 76 | } 77 | } 78 | else 79 | { 80 | var ret = this.Items.SingleOrDefault(x => x.Pattern == sc.Pattern); 81 | if (ret != null) 82 | { 83 | ret.Enabled = true; 84 | return 0; 85 | } 86 | } 87 | 88 | counter++; 89 | sc.Enabled = true; 90 | sc.Id = counter; 91 | this.Items.Add(sc); 92 | 93 | return sc.Id; 94 | } 95 | 96 | /// 97 | /// 98 | /// 99 | /// 100 | public void Reset() 101 | { 102 | counter = 0; 103 | this.Items = new List(); 104 | } 105 | 106 | /// 107 | /// 108 | /// 109 | public int Count 110 | { 111 | get 112 | { 113 | return this.Items.Count; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Source/WaitCursor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace LogViewer2 5 | { 6 | /// 7 | /// 8 | /// 9 | public class WaitCursor : IDisposable 10 | { 11 | #region Member Variables 12 | private Cursor previousCursor; 13 | #endregion 14 | 15 | #region Constructor 16 | /// 17 | /// 18 | /// 19 | public WaitCursor() 20 | { 21 | this.previousCursor = Mouse.OverrideCursor; 22 | Mouse.OverrideCursor = Cursors.Wait; 23 | } 24 | #endregion 25 | 26 | #region IDisposable Members 27 | /// 28 | /// 29 | /// 30 | public void Dispose() 31 | { 32 | Mouse.OverrideCursor = previousCursor; 33 | } 34 | 35 | #endregion 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/WindowConfiguration.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 43 | 44 | -------------------------------------------------------------------------------- /Source/WindowConfiguration.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace LogViewer2 4 | { 5 | /// 6 | /// Interaction logic for WindowConfiguration.xaml 7 | /// 8 | public partial class WindowConfiguration : Window 9 | { 10 | #region Properties 11 | public Configuration Config { get; private set; } 12 | #endregion 13 | 14 | #region Constructor 15 | /// 16 | /// 17 | /// 18 | public WindowConfiguration(Configuration config) 19 | { 20 | InitializeComponent(); 21 | 22 | this.Config = config; 23 | if (this.Config.NumContextLines > 0) 24 | { 25 | chkShowContext.IsChecked = true; 26 | cmbContextLines.SelectedIndex = this.Config.NumContextLines - 1; 27 | ChkShowContext_Checked(this, null); 28 | } 29 | else 30 | { 31 | chkShowContext.IsChecked = false; 32 | cmbContextLines.SelectedIndex = 0; 33 | ChkShowContext_Unchecked(this, null); 34 | } 35 | } 36 | #endregion 37 | 38 | #region Button Event Handlers 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | private void BtnOk_Click(object sender, RoutedEventArgs e) 45 | { 46 | if (chkShowContext.IsChecked == true) 47 | { 48 | Config.NumContextLines = cmbContextLines.SelectedIndex + 1; 49 | } 50 | else 51 | { 52 | Config.NumContextLines = 0; 53 | } 54 | 55 | DialogResult = true; 56 | Close(); 57 | } 58 | #endregion 59 | 60 | #region Checkbox Event Handlers 61 | /// 62 | /// 63 | /// 64 | /// 65 | /// 66 | private void ChkShowContext_Checked(object sender, RoutedEventArgs e) 67 | { 68 | cmbContextLines.IsEnabled = true; 69 | } 70 | 71 | /// 72 | /// 73 | /// 74 | /// 75 | /// 76 | private void ChkShowContext_Unchecked(object sender, RoutedEventArgs e) 77 | { 78 | cmbContextLines.IsEnabled = false; 79 | } 80 | #endregion 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Source/WindowGoToLine.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Source/WindowGoToLine.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace LogViewer2 4 | { 5 | /// 6 | /// Interaction logic for WindowGoToLine.xaml 7 | /// 8 | public partial class WindowGoToLine : Window 9 | { 10 | #region Properties 11 | public int LineNo { get; private set; } 12 | private int maxLines = 0; 13 | #endregion 14 | 15 | #region Constructor 16 | /// 17 | /// 18 | /// 19 | public WindowGoToLine(int maxLines) 20 | { 21 | InitializeComponent(); 22 | 23 | textLine.Focus(); 24 | this.maxLines = maxLines; 25 | } 26 | #endregion 27 | 28 | #region Button Event Handlers 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | private void BtnOk_Click(object sender, RoutedEventArgs e) 35 | { 36 | if (textLine.Text.Trim().Length == 0) 37 | { 38 | MessageBox.Show("Line number must be entered", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 39 | textLine.Focus(); 40 | return; 41 | } 42 | 43 | var isNumeric = int.TryParse(textLine.Text, out int lineNo); 44 | 45 | if (isNumeric == false) 46 | { 47 | MessageBox.Show("Line number value is invalid", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 48 | textLine.Focus(); 49 | return; 50 | } 51 | 52 | if (lineNo > this.maxLines) 53 | { 54 | MessageBox.Show("Line number value is greater than the number of lines available", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 55 | textLine.Focus(); 56 | return; 57 | } 58 | 59 | this.LineNo = lineNo; 60 | DialogResult = true; 61 | Close(); 62 | } 63 | #endregion 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Source/WindowLine.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Source/WindowLine.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace LogViewer2 4 | { 5 | /// 6 | /// Interaction logic for WindowSearchTerms.xaml 7 | /// 8 | public partial class WindowLine : Window 9 | { 10 | #region Constructor 11 | /// 12 | /// 13 | /// 14 | public WindowLine(string line) 15 | { 16 | InitializeComponent(); 17 | 18 | textLine.Text = line; 19 | } 20 | #endregion 21 | 22 | #region Button Event Handlers 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | private void BtnOk_Click(object sender, RoutedEventArgs e) 29 | { 30 | DialogResult = true; 31 | Close(); 32 | } 33 | #endregion 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/WindowMain.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Source/WindowMain.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Interop; 9 | using Microsoft.Win32; 10 | 11 | namespace LogViewer2 12 | { 13 | /// 14 | /// Interaction logic for MainWindow.xaml 15 | /// 16 | public partial class WindowMain : Window 17 | { 18 | private CancellationTokenSource cancellationTokenSource; 19 | private WaitCursor waitCursor; 20 | private bool processing; 21 | private Color highlightColour = Color.Lime; 22 | private Color contextColour = Color.LightGray; 23 | private Configuration config; 24 | 25 | #region Window Event Handlers 26 | /// 27 | /// Constructor 28 | /// 29 | public WindowMain() 30 | { 31 | InitializeComponent(); 32 | } 33 | 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | private void Window_Loaded(object sender, RoutedEventArgs e) 40 | { 41 | this.config = new Configuration(); 42 | string ret = this.config.Load(); 43 | if (ret.Length > 0) 44 | { 45 | MessageBox.Show(ret, Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Error); 46 | } 47 | 48 | this.highlightColour = config.GetHighlightColour(); 49 | this.contextColour = config.GetContextColour(); 50 | 51 | mnuFileOpen.IsEnabled = false; 52 | mnuFileClose.IsEnabled = false; 53 | toolBtnSearch.IsEnabled = false; 54 | } 55 | #endregion 56 | 57 | #region Menu Event Handlers 58 | /// 59 | /// 60 | /// 61 | /// 62 | /// 63 | private void MnuFileOpenNew_Click(object sender, RoutedEventArgs e) 64 | { 65 | //LoadFile("access.log.txt", true); 66 | //LoadFile("access_log2", true); 67 | 68 | OpenFileDialog openFileDialog = new OpenFileDialog(); 69 | openFileDialog.Filter = "All Files|*.*"; 70 | openFileDialog.FileName = "*.*"; 71 | openFileDialog.Title = "Select log file"; 72 | 73 | if (openFileDialog.ShowDialog(this) == false) 74 | { 75 | return; 76 | } 77 | 78 | LoadFile(openFileDialog.FileName, true); 79 | } 80 | 81 | /// 82 | /// 83 | /// 84 | /// 85 | /// 86 | private void MnuFileOpen_Click(object sender, RoutedEventArgs e) 87 | { 88 | //LoadFile("access.log.txt", false); 89 | 90 | OpenFileDialog openFileDialog = new OpenFileDialog(); 91 | openFileDialog.Filter = "All Files|*.*"; 92 | openFileDialog.FileName = "*.*"; 93 | openFileDialog.Title = "Select log file"; 94 | 95 | if (openFileDialog.ShowDialog(this) == false) 96 | { 97 | return; 98 | } 99 | 100 | LoadFile(openFileDialog.FileName, false); 101 | } 102 | 103 | /// 104 | /// 105 | /// 106 | /// 107 | /// 108 | private void MnuFileClose_Click(object sender, RoutedEventArgs e) 109 | { 110 | if (tabMain.SelectedItem == null) 111 | { 112 | MessageBox.Show("Cannot identify current tab", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 113 | return; 114 | } 115 | 116 | TabItem ti = (TabItem)tabMain.SelectedItem; 117 | ti.Header = "..."; 118 | ti.ToolTip = ""; 119 | ControlLog cl = (ControlLog)ti.Content; 120 | cl.Dispose(); 121 | } 122 | 123 | /// 124 | /// 125 | /// 126 | /// 127 | /// 128 | private void MnuToolsConfiguration_Click(object sender, RoutedEventArgs e) 129 | { 130 | WindowConfiguration wc = new WindowConfiguration(this.config); 131 | // Hack to set owner from a user control?! 132 | HwndSource source = HwndSource.FromVisual(this) as HwndSource; 133 | if (source != null) 134 | { 135 | WindowInteropHelper helper = new WindowInteropHelper(wc); 136 | helper.Owner = source.Handle; 137 | } 138 | if (wc.ShowDialog() == false) 139 | { 140 | return; 141 | } 142 | } 143 | 144 | /// 145 | /// 146 | /// 147 | /// 148 | /// 149 | private void MnuToolsMultiStringSearch_Click(object sender, RoutedEventArgs e) 150 | { 151 | ControlLog cl = (ControlLog)((TabItem)tabMain.SelectedItem).Content; 152 | cl.ShowMultiSearch(); 153 | 154 | //LogFile lf = logs[tabControl.SelectedTab.Tag.ToString()]; 155 | 156 | //using (FormSearch f = new FormSearch(lf.Searches)) 157 | //{ 158 | // DialogResult dr = f.ShowDialog(this); 159 | // if (dr == DialogResult.Cancel) 160 | // { 161 | // return; 162 | // } 163 | 164 | // // Clear any existing filter ID's as we will only show the multi-string search 165 | // lf.FilterIds.Clear(); 166 | // lf.Searches.Reset(); 167 | // foreach (SearchCriteria sc in f.NewSearches) 168 | // { 169 | // // Add the ID so that any matches show up straight away 170 | // lf.FilterIds.Add(sc.Id); 171 | // lf.Searches.Add(sc); 172 | // } 173 | 174 | // this.processing = true; 175 | // this.hourGlass = new HourGlass(this); 176 | // SetProcessingState(false); 177 | // statusProgress.Visible = true; 178 | // this.cancellationTokenSource = new CancellationTokenSource(); 179 | // lf.SearchMulti(f.NewSearches, cancellationTokenSource.Token, config.NumContextLines); 180 | //} 181 | } 182 | 183 | /// 184 | /// 185 | /// 186 | /// 187 | /// 188 | private void MnuFileExit_Click(object sender, RoutedEventArgs e) 189 | { 190 | Application.Current.Shutdown(); 191 | } 192 | #endregion 193 | 194 | #region Core Methods 195 | /// 196 | /// 197 | /// 198 | /// 199 | /// 200 | private void LoadFile(string filePath, bool newTab) 201 | { 202 | SetProcessingState(false); 203 | this.cancellationTokenSource = new CancellationTokenSource(); 204 | 205 | if (newTab == true) 206 | { 207 | ControlLog cl = new ControlLog(this.config) 208 | { 209 | ViewMode = Global.ViewMode.Standard, 210 | }; 211 | 212 | cl.ProgressUpdate += LogFile_LoadProgress; 213 | cl.LoadComplete += LogFile_LoadComplete; 214 | cl.SearchComplete += LogFile_SearchComplete; 215 | cl.ExportInitiated += LogFile_ExportInitiated; 216 | cl.ExportComplete += LogFile_ExportComplete; ; 217 | cl.LoadError += LogFile_LoadError; 218 | cl.MultiSearchInitiated += LogFile_MultiSearchInitiated; 219 | cl.DragEnter += LogFile_DragEnter; 220 | cl.Drop += LogFile_Drop; 221 | 222 | TabItem ti = new TabItem 223 | { 224 | Content = cl, 225 | Header = Path.GetFileName(filePath), 226 | Tag = cl.Guid, 227 | ToolTip = filePath 228 | }; 229 | 230 | tabMain.Items.Add(ti); 231 | tabMain.SelectedIndex = tabMain.Items.Count - 1; 232 | 233 | cl.Load(filePath, cancellationTokenSource.Token); 234 | } 235 | else 236 | { 237 | if (tabMain.SelectedItem == null) 238 | { 239 | MessageBox.Show("Cannot identify current tab", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 240 | return; 241 | } 242 | 243 | ControlLog cl = (ControlLog)((TabItem)tabMain.SelectedItem).Content; 244 | ((TabItem)tabMain.SelectedItem).ToolTip = filePath; 245 | cl.Dispose(); 246 | cl.Load(filePath, cancellationTokenSource.Token); 247 | } 248 | } 249 | 250 | /// 251 | /// 252 | /// 253 | /// 254 | private void SearchFile() 255 | { 256 | ControlLog cl = (ControlLog)((TabItem)tabMain.SelectedItem).Content; 257 | 258 | SearchCriteria sc = new SearchCriteria(); 259 | sc.Type = (Global.SearchType)comboType.SelectedIndex; 260 | sc.Pattern = txtSearch.Text; 261 | sc.Id = cl.Searches.Add(sc, (bool)toolBtnCumulative.IsChecked); 262 | 263 | if (sc.Id == 0) 264 | { 265 | MessageBox.Show("Search pattern already exists", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 266 | return; 267 | } 268 | 269 | // Add the ID so that any matches show up straight away 270 | cl.FilterIds.Add(sc.Id); 271 | 272 | SetProcessingState(false); 273 | this.cancellationTokenSource = new CancellationTokenSource(); 274 | cl.Search(sc, (bool)toolBtnCumulative.IsChecked, cancellationTokenSource.Token, config.NumContextLines); 275 | } 276 | #endregion 277 | 278 | #region Log File Event Handlers 279 | /// 280 | /// 281 | /// 282 | /// 283 | /// 284 | private void LogFile_ExportInitiated(ControlLog cl, bool all) 285 | { 286 | SaveFileDialog sfd = new SaveFileDialog(); 287 | sfd.Filter = "All Files|*.*"; 288 | sfd.FileName = "*.*"; 289 | sfd.Title = "Select export file"; 290 | 291 | if (sfd.ShowDialog() == false) 292 | { 293 | return; 294 | } 295 | 296 | this.cancellationTokenSource = new CancellationTokenSource(); 297 | SetProcessingState(false); 298 | 299 | if (all == true) 300 | { 301 | cl.Export(sfd.FileName, cancellationTokenSource.Token); 302 | } 303 | else 304 | { 305 | cl.ExportSelected(sfd.FileName, cancellationTokenSource.Token); 306 | } 307 | } 308 | 309 | /// 310 | /// 311 | /// 312 | /// 313 | private void LogFile_MultiSearchInitiated(ControlLog cl, List searches) 314 | { 315 | this.cancellationTokenSource = new CancellationTokenSource(); 316 | SetProcessingState(false); 317 | 318 | cl.SearchMulti(searches, cancellationTokenSource.Token, config.NumContextLines); 319 | } 320 | 321 | /// 322 | /// 323 | /// 324 | /// 325 | /// 326 | /// 327 | /// 328 | private void LogFile_ExportComplete(ControlLog cl, string fileName, TimeSpan duration, bool cancelled) 329 | { 330 | Dispatcher.Invoke(() => 331 | { 332 | this.cancellationTokenSource.Dispose(); 333 | SetProcessingState(true); 334 | UpdateStatusLabel("Export complete # Duration: " + duration + " (" + fileName + ")"); 335 | }); 336 | } 337 | 338 | /// 339 | /// 340 | /// 341 | /// 342 | /// 343 | /// 344 | /// 345 | private void LogFile_LoadComplete(ControlLog cl, string fileName, TimeSpan duration, bool cancelled) 346 | { 347 | Dispatcher.Invoke(() => 348 | { 349 | // Update the UI 350 | cl.ItemsSource = cl.Lines; 351 | cl.UpdateColumnWidths(); 352 | tabMain.UpdateLayout(); 353 | 354 | // Enable the menu items that only apply if a file is open 355 | mnuFileClose.IsEnabled = true; 356 | mnuFileOpen.IsEnabled = true; 357 | 358 | UpdateStatusLabel(cl.Lines.Count + " Lines # Duration: " + duration + " (" + fileName + ")"); 359 | this.cancellationTokenSource.Dispose(); 360 | SetProcessingState(true); 361 | 362 | }); 363 | } 364 | 365 | /// 366 | /// 367 | /// 368 | /// 369 | private void LogFile_LoadProgress(int percent) 370 | { 371 | Dispatcher.Invoke(() => 372 | { 373 | statusProgressBar.Value = (int)percent; 374 | }); 375 | } 376 | 377 | /// 378 | /// 379 | /// 380 | /// 381 | private void LogFile_LoadError(string fileName, string message) 382 | { 383 | MessageBox.Show(message + " (" + fileName + ")", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Error); 384 | 385 | Dispatcher.Invoke(() => 386 | { 387 | this.cancellationTokenSource.Dispose(); 388 | SetProcessingState(true); 389 | 390 | // Lets clear the LogFile state and set the UI correctly 391 | MnuFileClose_Click(this, null); 392 | }); 393 | } 394 | 395 | /// 396 | /// 397 | /// 398 | private void LogFile_SearchComplete(ControlLog cl, string fileName, TimeSpan duration, long matches, int numTerms, bool cancelled) 399 | { 400 | Dispatcher.Invoke(() => 401 | { 402 | // Update the list 403 | cl.ItemsSource = null; 404 | cl.ItemsSource = cl.Lines; 405 | 406 | UpdateStatusLabel("Matched " + matches + " lines (Search Terms: " + numTerms + ") # Duration: " + duration + " (" + fileName + ")"); 407 | this.cancellationTokenSource.Dispose(); 408 | SetProcessingState(true); 409 | }); 410 | } 411 | 412 | /// 413 | /// 414 | /// 415 | /// 416 | /// 417 | private void LogFile_Drop(object sender, DragEventArgs e) 418 | { 419 | if (processing == true) 420 | { 421 | return; 422 | } 423 | 424 | string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); 425 | if (files.Length == 0) 426 | { 427 | return; 428 | } 429 | 430 | if (files.Length > 1) 431 | { 432 | MessageBox.Show("Only one file can be processed at one time", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 433 | return; 434 | } 435 | 436 | LoadFile(files[0], false); 437 | } 438 | 439 | /// 440 | /// 441 | /// 442 | /// 443 | /// 444 | private void LogFile_DragEnter(object sender, DragEventArgs e) 445 | { 446 | if (processing == true) 447 | { 448 | return; 449 | } 450 | 451 | if (e.Data.GetDataPresent(DataFormats.FileDrop)) 452 | { 453 | e.Effects = DragDropEffects.Move; 454 | } 455 | else 456 | { 457 | e.Effects = DragDropEffects.None; 458 | } 459 | 460 | e.Handled = true; 461 | } 462 | #endregion 463 | 464 | #region UI Methods 465 | /// 466 | /// 467 | /// 468 | /// 469 | private void SetProcessingState(bool enabled) 470 | { 471 | Dispatcher.Invoke(() => 472 | { 473 | mnuFileOpenNew.IsEnabled = enabled; 474 | mnuFileOpen.IsEnabled = enabled; 475 | mnuFileClose.IsEnabled = enabled; 476 | mnuFileExit.IsEnabled = enabled; 477 | //menuToolsMultiStringSearch.IsEnabled = enabled; 478 | toolBtnCumulative.IsEnabled = enabled; 479 | toolBtnSearch.IsEnabled = enabled; 480 | 481 | if (enabled == true) 482 | { 483 | this.waitCursor.Dispose(); 484 | this.processing = false; 485 | statusProgressBar.Visibility = Visibility.Hidden; 486 | } 487 | else 488 | { 489 | this.waitCursor = new WaitCursor(); 490 | this.processing = true; 491 | statusProgressBar.Visibility = Visibility.Visible; 492 | } 493 | 494 | }); 495 | } 496 | 497 | /// 498 | /// 499 | /// 500 | /// 501 | private void UpdateStatusLabel(string text) 502 | { 503 | Dispatcher.Invoke(() => 504 | { 505 | statusLabelMain.Content = text; 506 | }); 507 | } 508 | #endregion 509 | 510 | #region Toolbar Event Handlers 511 | /// 512 | /// 513 | /// 514 | /// 515 | /// 516 | private void toolBtnSearch_Click(object sender, RoutedEventArgs e) 517 | { 518 | if (comboType.SelectedIndex == -1) 519 | { 520 | MessageBox.Show("Type is not selected", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 521 | comboType.Focus(); 522 | return; 523 | } 524 | 525 | SearchFile(); 526 | } 527 | #endregion 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /Source/WindowMultiSearch.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Source/WindowMultiSearch.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Windows; 5 | 6 | namespace LogViewer2 7 | { 8 | /// 9 | /// Interaction logic for WindowMultiSearch.xaml 10 | /// 11 | public partial class WindowMultiSearch : Window 12 | { 13 | #region Member Variables 14 | public List Patterns { get; private set; } = new List(); 15 | private Searches existingSearches; 16 | public List NewSearches { get; private set; } = new List(); 17 | #endregion 18 | 19 | #region Constructor 20 | /// 21 | /// 22 | /// 23 | public WindowMultiSearch(Searches searches) 24 | { 25 | InitializeComponent(); 26 | 27 | cmbSearchType.SelectedIndex = 0; 28 | this.existingSearches = searches; 29 | } 30 | #endregion 31 | 32 | #region Button Event Handlers 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | private void BtnOk_Click(object sender, RoutedEventArgs e) 39 | { 40 | if (cmbSearchType.SelectedIndex == -1) 41 | { 42 | MessageBox.Show("Type is not selected", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 43 | cmbSearchType.Focus(); 44 | return; 45 | } 46 | 47 | if (this.Patterns.Count == 0) 48 | { 49 | MessageBox.Show("No search patterns loaded", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 50 | listSearchTerms.Focus(); 51 | return; 52 | } 53 | 54 | // Disable all existing SearchCriteria 55 | foreach (SearchCriteria sc in this.existingSearches.Items) 56 | { 57 | sc.Enabled = false; 58 | } 59 | 60 | bool exists = false; 61 | foreach (string p in this.Patterns) 62 | { 63 | SearchCriteria sc = new SearchCriteria(); 64 | sc.Type = (Global.SearchType)cmbSearchType.SelectedIndex; 65 | sc.Pattern = p; 66 | sc.Enabled = true; 67 | sc.Id = this.existingSearches.Add(sc); 68 | //if (sc.Id == 0) 69 | //{ 70 | // sc.Enabled = true; 71 | // exists = true; 72 | // continue; 73 | //} 74 | 75 | NewSearches.Add(sc); 76 | } 77 | 78 | if (exists == true) 79 | { 80 | MessageBox.Show("At least one pattern already exists and has not been added", Application.ResourceAssembly.GetName().Name, MessageBoxButton.OK, MessageBoxImage.Exclamation); 81 | } 82 | 83 | DialogResult = true; 84 | Close(); 85 | } 86 | 87 | /// 88 | /// 89 | /// 90 | /// 91 | /// 92 | private void BtnImport_Click(object sender, RoutedEventArgs e) 93 | { 94 | OpenFileDialog openFileDialog = new OpenFileDialog(); 95 | openFileDialog.Filter = "All Files|*.*"; 96 | openFileDialog.FileName = "*.*"; 97 | openFileDialog.Title = "Select file"; 98 | 99 | if (openFileDialog.ShowDialog(this) == false) 100 | { 101 | return; 102 | } 103 | 104 | string line = string.Empty; 105 | using (StreamReader sr = new StreamReader(openFileDialog.FileName)) 106 | { 107 | while ((line = sr.ReadLine()) != null) 108 | { 109 | this.Patterns.Add(line); 110 | } 111 | } 112 | 113 | listSearchTerms.ItemsSource = null; 114 | listSearchTerms.ItemsSource = this.Patterns; 115 | } 116 | #endregion 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Source/WindowSearchTerms.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Source/WindowSearchTerms.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace LogViewer2 4 | { 5 | /// 6 | /// Interaction logic for WindowSearchTerms.xaml 7 | /// 8 | public partial class WindowSearchTerms : Window 9 | { 10 | #region Member Variables/Properties 11 | public Searches Searches { get; private set; } 12 | #endregion 13 | 14 | #region Constructor 15 | /// 16 | /// 17 | /// 18 | public WindowSearchTerms(Searches searches) 19 | { 20 | InitializeComponent(); 21 | 22 | listSearches.ItemsSource = searches.Items; 23 | this.Searches = searches; 24 | } 25 | #endregion 26 | 27 | #region Button Event Handlers 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | private void BtnOk_Click(object sender, RoutedEventArgs e) 34 | { 35 | DialogResult = true; 36 | Close(); 37 | } 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /help.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | The following provides a brief help guide for the core operations within LogViewer. 4 | 5 | ## Opening a file 6 | 7 | Either use the File->Open menu item or drag and drop a file onto the list 8 | 9 | ## Search 10 | 11 | - Enter the search term/pattern in the toolbar text box 12 | - Select the Search Type from the drop down 13 | - If you want the search to be performed on the lines currently displayed then ensure the **Cumulative** button is checked. If the button is not checked then the entire file will be searched, any existing search terms will also be removed 14 | - Next click the **Search** button (magnify glass icon) 15 | 16 | ### Search Types 17 | 18 | There are four different search types available: 19 | 20 | - Sub String Case Insensitive (Default) 21 | - Sub String Case Sensitive 22 | - Regex Case Insensitive 23 | - Regex Case Sensitive 24 | 25 | ### Search Terms 26 | 27 | By using the **Cumulative** search facility, there can be multiple search terms. Each of the terms can be enabled/disabled individually. The search terms can be accessed via the list context menu. When the **Search Terms** window is displayed, check or uncheck the search terms required, upon exiting the display will be refreshed. 28 | 29 | ### Multi-String Search 30 | 31 | To search for multiple strings in one search, use the Tools->Multi-String Search menu. Select the search type, then use the import button to import the search patterns. Any previous search results are cleared when using the multi-string search. 32 | 33 | ### Highlight Line Colour 34 | 35 | The search match highlight colour can be modified via the list context menu (Search->Colour->Match). The colour is persisted to the configuration file. 36 | 37 | ### Context Line Colour 38 | 39 | If the **Show context lines** option is enabled, then the context line colour can be modified via the list context menu (Search->Colour->Context). The colour is persisted to the configuration file. 40 | 41 | ## Filtering 42 | 43 | There are two modes for filtering; hide matched and show matched. Filtering and filter clearing is accessed via the list context menu. 44 | 45 | - **Show matched**: Hides all lines where there is not a search match; therefore only show the matched lines 46 | - **Hide matched**: Hides the lines that matched the search; therefore only show the lines that don't match 47 | 48 | ## Export 49 | 50 | The export functionality exports all lines within the current view or the selected lines. There is a maximum limit of 10000 lines. This functionality is accessed via the list content menu. 51 | 52 | ## Copy Line 53 | 54 | The selected line's contents can be copied to the clipboard via the list context menu. There is a maximum limit of 10000 lines 55 | 56 | ## Context Lines 57 | 58 | To show context around a match, the searches can be configured (Tools->Configuration) to show a user configurable number of lines before and after a search match. 59 | 60 | ## Go To Line 61 | 62 | The various line functions can be accessed via the context menu. For example to go to a specific line, right click and select the **Lines->Go To Line** context menu item. A window will be displayed, enter the line number and click OK. 63 | 64 | 65 | -------------------------------------------------------------------------------- /help.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/help.pdf -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # LogViewer2 2 | 3 | LogViewer2 is a WPF rewrite of the LogViewer WinForms application. It is 2019 and I thought it was about time I embraced the **new** technology known as WPF, released in 2006 ;-) 4 | 5 | LogViewer2 is designed to work with any large text files, so that even very large files can be opened, viewed and searched. 6 | 7 | It's original use case is for DFIR cases that involve log analysis. Whilst I use grep (well actually I use [sift](https://github.com/svent/sift) to extract data from logs, it is handy to be able to view log files, search for terms, hide lines whilst you get an idea what the log file contains, what actions are being performed. 8 | 9 | I normally use a combination of various text editors, glogg, and the Mandiant Highlighter tool. The Mandiant Highlighter tool is great but hasn't been updated since 2011. It has lots of functionality, most of which I don't use. I wanted to implement my own log viewer and looked at the source code for Highlighter and realised it uses a custom owner drawn textbox, which is how it can work on large files. 10 | 11 | So that is how LogViewer was born, by design it is simpler, it doesn't have field operations, it doesn't have the line view etc. 12 | 13 | The use of the custom control would make debugging any future issues a lot harder, so after a bit of thought I used the [ObjectListView](http://objectlistview.sourceforge.net/cs/index.html) library. The ObjectListView library is a custom list view control for use with .Net projects, I have used it extensively as it is easy to use and works with large data sets. 14 | 15 | The core operation of LogViewer works in the same way as Highlighter e.g. parse the file, find the line offsets and line lengths, then when a line is needed for display, an existing file stream is used to seek to the offset, and then read X bytes. 16 | 17 | I tested the v0.0.1 release of LogViewer against v1.1.3 of Mandiant Highlighter. My test log file was 1.2 GB and had 4.4 million rows. The following shows the operation and duration of the operation to compare: 18 | 19 | - Load (LogViewer): 15s 20 | - Load (Highlighter): 42s 21 | - Search (LogViewer): 1m 5s 22 | - Search (Highlighter): 2m 15s 23 | - Show Only Highlighted (LogViewer): 2s (+ the search operation above 1m 5s) Total: 1m 7s 24 | - Show Only Highlighted (Highlighter): Killed after 35m 25 | 26 | The main reasons for this being faster is that it has removed some functionality and I have optimised the file load code so that there is less memory allocation and unnecessary checks/logic, plus Highlighter does some Md5 calcs etc. 27 | 28 | ## Features 29 | 30 | - Very fast 31 | - Supports huge files 32 | - Cumulative search 33 | - Can disable/enable search terms that are cumulative and the results are displayed instantly 34 | - Export current view 35 | - Show/Hide matched lines 36 | - Four search modes (Sub String Case Insensitive, Sub String Case Sensitive, Regex Case Insensitive, Regex Case Sensitive) 37 | 38 | ## General 39 | 40 | - To stop an action such as load, search, export, you double click on the progress bar, located in the status bar 41 | - The context menu holds the majority of actions 42 | - Lots of stuff to be fixed/added! :-) 43 | 44 | ## Screenshot 45 | 46 | ![](Screenshot.png) 47 | 48 | ## Third Party 49 | 50 | [ObjectListView](http://objectlistview.sourceforge.net/cs/index.html): Used for displaying the file lines 51 | 52 | [Nett](https://github.com/paiden/Nett): Used for the TOML configuration file reading/writing 53 | 54 | [icons8](https://icons8.com): Icons used within the application -------------------------------------------------------------------------------- /readme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woanware/LogViewer2/3c2260ec2373d5c9439f35e0302c7eef165b6bcc/readme.pdf --------------------------------------------------------------------------------