Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 6df88e655f315a947c80da7e040ce4a7384059af @SHSE committed Mar 22, 2013
Showing with 6,216 additions and 0 deletions.
  1. +6 −0 .nuget/NuGet.Config
  2. +151 −0 .nuget/NuGet.targets
  3. BIN .nuget/nuget.exe
  4. +106 −0 LogWatch.Tests/Formats/CsvFormatTests.cs
  5. +70 −0 LogWatch.Tests/Formats/Log4JXmlFormatTests.cs
  6. +117 −0 LogWatch.Tests/LogWatch.Tests.csproj
  7. +36 −0 LogWatch.Tests/Properties/AssemblyInfo.cs
  8. +91 −0 LogWatch.Tests/RecordCollectionTests.cs
  9. +71 −0 LogWatch.Tests/TestMessenger.cs
  10. +13 −0 LogWatch.Tests/TestSynchronizationContext.cs
  11. +36 −0 LogWatch.Tests/ViewModels/RecordDetailsViewModelTests.cs
  12. +83 −0 LogWatch.Tests/ViewModels/RecordsViewModelTests.cs
  13. +82 −0 LogWatch.Tests/ViewModels/StatsViewModelTests.cs
  14. +14 −0 LogWatch.Tests/packages.config
  15. +33 −0 LogWatch.sln
  16. +98 −0 LogWatch.sln.DotSettings
  17. +18 −0 LogWatch/App.config
  18. BIN LogWatch/App.ico
  19. +23 −0 LogWatch/App.xaml
  20. +135 −0 LogWatch/App.xaml.cs
  21. +117 −0 LogWatch/Features/RecordDetails/ExceptionHighlighter.cs
  22. +84 −0 LogWatch/Features/RecordDetails/RecordDetailsView.xaml
  23. +7 −0 LogWatch/Features/RecordDetails/RecordDetailsView.xaml.cs
  24. +95 −0 LogWatch/Features/RecordDetails/RecordDetailsViewModel.cs
  25. +19 −0 LogWatch/Features/RecordDetails/WindowsExplorerLinkNavigator.cs
  26. +103 −0 LogWatch/Features/Records/AutoScrollToEndBehaviour.cs
  27. +28 −0 LogWatch/Features/Records/AutoSizeColumnBehaviour.cs
  28. +70 −0 LogWatch/Features/Records/FilteredRecordCollection.cs
  29. +11 −0 LogWatch/Features/Records/GoToIndexEventArgs.cs
  30. +21 −0 LogWatch/Features/Records/IgnoreLinebreaksConverter.cs
  31. +21 −0 LogWatch/Features/Records/LoggerToShortStringConverter.cs
  32. +400 −0 LogWatch/Features/Records/RecordCollection.cs
  33. +152 −0 LogWatch/Features/Records/RecordsView.xaml
  34. +31 −0 LogWatch/Features/Records/RecordsView.xaml.cs
  35. +80 −0 LogWatch/Features/Records/RecordsViewModel.cs
  36. +15 −0 LogWatch/Features/Records/SelectItemByIndexAction.cs
  37. +11 −0 LogWatch/Features/Records/VisibleItemsInfo.cs
  38. +77 −0 LogWatch/Features/Records/VisibleItemsInfoBehaviour.cs
  39. +33 −0 LogWatch/Features/Search/SearchView.xaml
  40. +7 −0 LogWatch/Features/Search/SearchView.xaml.cs
  41. +37 −0 LogWatch/Features/SelectSource/SelectFormatView.xaml
  42. +11 −0 LogWatch/Features/SelectSource/SelectFormatView.xaml.cs
  43. +46 −0 LogWatch/Features/SelectSource/SelectFormatViewModel.cs
  44. +45 −0 LogWatch/Features/SelectSource/SelectSourceView.xaml
  45. +11 −0 LogWatch/Features/SelectSource/SelectSourceView.xaml.cs
  46. +89 −0 LogWatch/Features/SelectSource/SelectSourceViewModel.cs
  47. +129 −0 LogWatch/Features/Stats/StatsView.xaml
  48. +7 −0 LogWatch/Features/Stats/StatsView.xaml.cs
  49. +206 −0 LogWatch/Features/Stats/StatsViewModel.cs
  50. +20 −0 LogWatch/Formats/AutoLogFormatSelector.cs
  51. +293 −0 LogWatch/Formats/CsvLogFormat.cs
  52. +17 −0 LogWatch/Formats/ILogFormat.cs
  53. +5 −0 LogWatch/Formats/ILogFormatMetadata.cs
  54. +116 −0 LogWatch/Formats/Log4JXmlLogFormat.cs
  55. +13 −0 LogWatch/Formats/LogFormatAttribute.cs
  56. +50 −0 LogWatch/Formats/PlainTextLogFormat.cs
  57. +10 −0 LogWatch/LogLevel.cs
  58. +249 −0 LogWatch/LogWatch.csproj
  59. +9 −0 LogWatch/Messages/NavigatedToRecordMessage.cs
  60. +11 −0 LogWatch/Messages/RecordContextChangedMessage.cs
  61. +9 −0 LogWatch/Messages/RecordSelectedMessage.cs
  62. +665 −0 LogWatch/Properties/Annotations.cs
  63. +55 −0 LogWatch/Properties/AssemblyInfo.cs
  64. +62 −0 LogWatch/Properties/Resources.Designer.cs
  65. +117 −0 LogWatch/Properties/Resources.resx
  66. +26 −0 LogWatch/Properties/Settings.Designer.cs
  67. +7 −0 LogWatch/Properties/Settings.settings
  68. +57 −0 LogWatch/Record.cs
  69. +15 −0 LogWatch/RecordSegment.cs
  70. +103 −0 LogWatch/ShellView.xaml
  71. +7 −0 LogWatch/ShellView.xaml.cs
  72. +6 −0 LogWatch/ShellViewModel.cs
  73. +190 −0 LogWatch/Sources/FileLogSource.cs
  74. +12 −0 LogWatch/Sources/ILogSource.cs
  75. +15 −0 LogWatch/Sources/LogSourceInfo.cs
  76. +13 −0 LogWatch/Sources/LogSourceStatus.cs
  77. +145 −0 LogWatch/Sources/UdpLogSource.cs
  78. +122 −0 LogWatch/Styles/CompactMenu.xaml
  79. +284 −0 LogWatch/Styles/CompactWindow.xaml
  80. +114 −0 LogWatch/Util/AutoResetEventAsync.cs
  81. +50 −0 LogWatch/Util/DialogResultBehaviour.cs
  82. +135 −0 LogWatch/Util/KmpUtil.cs
  83. +47 −0 LogWatch/Util/TextGeometry.cs
  84. +16 −0 LogWatch/packages.config
  85. +5 −0 packages/repositories.config
6 .nuget/NuGet.Config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <solution>
+ <add key="disableSourceControlIntegration" value="true" />
+ </solution>
+</configuration>
151 .nuget/NuGet.targets
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
+
+ <!-- Enable the restore command to run before builds -->
+ <RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>
+
+ <!-- Property that enables building a package from a project -->
+ <BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>
+
+ <!-- Determines if package restore consent is required to restore packages -->
+ <RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>
+
+ <!-- Download NuGet.exe if it does not already exist -->
+ <DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
+ </PropertyGroup>
+
+ <ItemGroup Condition=" '$(PackageSources)' == '' ">
+ <!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
+ <!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
+ <!--
+ <PackageSource Include="https://nuget.org/api/v2/" />
+ <PackageSource Include="https://my-nuget-source/nuget/" />
+ -->
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
+ <!-- Windows specific commands -->
+ <NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
+ <PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
+ <!-- We need to launch nuget.exe with the mono command if we're not on windows -->
+ <NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
+ <PackagesConfig>packages.config</PackagesConfig>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <!-- NuGet command -->
+ <NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\nuget.exe</NuGetExePath>
+ <PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>
+
+ <NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
+ <NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>
+
+ <PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
+
+ <RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
+ <!-- Commands -->
+ <RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -solutionDir "$(SolutionDir) "</RestoreCommand>
+ <BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols</BuildCommand>
+
+ <!-- We need to ensure packages are restored prior to assembly resolve -->
+ <BuildDependsOn Condition="$(RestorePackages) == 'true'">
+ RestorePackages;
+ $(BuildDependsOn);
+ </BuildDependsOn>
+
+ <!-- Make the build depend on restore packages -->
+ <BuildDependsOn Condition="$(BuildPackage) == 'true'">
+ $(BuildDependsOn);
+ BuildPackage;
+ </BuildDependsOn>
+ </PropertyGroup>
+
+ <Target Name="CheckPrerequisites">
+ <!-- Raise an error if we're unable to locate nuget.exe -->
+ <Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
+ <SetEnvironmentVariable EnvKey="VisualStudioVersion" EnvValue="$(VisualStudioVersion)" Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' " />
+ <!--
+ Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
+ This effectively acts as a lock that makes sure that the download operation will only happen once and all
+ parallel builds will have to wait for it to complete.
+ -->
+ <MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT" />
+ </Target>
+
+ <Target Name="_DownloadNuGet">
+ <DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
+ </Target>
+
+ <Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
+ <Exec Command="$(RestoreCommand)"
+ Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />
+
+ <Exec Command="$(RestoreCommand)"
+ LogStandardErrorAsError="true"
+ Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
+ </Target>
+
+ <Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
+ <Exec Command="$(BuildCommand)"
+ Condition=" '$(OS)' != 'Windows_NT' " />
+
+ <Exec Command="$(BuildCommand)"
+ LogStandardErrorAsError="true"
+ Condition=" '$(OS)' == 'Windows_NT' " />
+ </Target>
+
+ <UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
+ <ParameterGroup>
+ <OutputFilename ParameterType="System.String" Required="true" />
+ </ParameterGroup>
+ <Task>
+ <Reference Include="System.Core" />
+ <Using Namespace="System" />
+ <Using Namespace="System.IO" />
+ <Using Namespace="System.Net" />
+ <Using Namespace="Microsoft.Build.Framework" />
+ <Using Namespace="Microsoft.Build.Utilities" />
+ <Code Type="Fragment" Language="cs">
+ <![CDATA[
+ try {
+ OutputFilename = Path.GetFullPath(OutputFilename);
+
+ Log.LogMessage("Downloading latest version of NuGet.exe...");
+ WebClient webClient = new WebClient();
+ webClient.DownloadFile("https://nuget.org/nuget.exe", OutputFilename);
+
+ return true;
+ }
+ catch (Exception ex) {
+ Log.LogErrorFromException(ex);
+ return false;
+ }
+ ]]>
+ </Code>
+ </Task>
+ </UsingTask>
+
+ <UsingTask TaskName="SetEnvironmentVariable" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
+ <ParameterGroup>
+ <EnvKey ParameterType="System.String" Required="true" />
+ <EnvValue ParameterType="System.String" Required="true" />
+ </ParameterGroup>
+ <Task>
+ <Using Namespace="System" />
+ <Code Type="Fragment" Language="cs">
+ <![CDATA[
+ try {
+ Environment.SetEnvironmentVariable(EnvKey, EnvValue, System.EnvironmentVariableTarget.Process);
+ }
+ catch {
+ }
+ ]]>
+ </Code>
+ </Task>
+ </UsingTask>
+</Project>
BIN .nuget/nuget.exe
Binary file not shown.
106 LogWatch.Tests/Formats/CsvFormatTests.cs
@@ -0,0 +1,106 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Text;
+using System.Threading;
+using LogWatch.Formats;
+using Xunit;
+
+namespace LogWatch.Tests.Formats {
+ public class CsvFormatTests {
+ [Fact]
+ public void ReadsSegment() {
+ var stream = CreateStream("2012-03-05 13:56:12;warn;Program;\"Test message\";TextException");
+
+ var format = new CsvLogFormat {
+ ReadHeader = false,
+ Delimeter = ';',
+ FieldCount = 5
+ };
+
+ var subject = new ReplaySubject<RecordSegment>();
+
+ format.ReadSegments(subject, stream, CancellationToken.None).Wait();
+
+ subject.OnCompleted();
+
+ var segment = subject.ToEnumerable().FirstOrDefault();
+
+ Assert.Equal(0, segment.Offset);
+ Assert.Equal(stream.Length, segment.Length);
+ }
+
+ [Fact]
+ public void GuessesOptionsFromHeader() {
+ var stream1 = CreateStream("time;level;message;logger;exception");
+ var stream2 = CreateStream("time,level,message,logger,exception");
+
+ var format = new CsvLogFormat {ReadHeader = true};
+
+ format.ReadSegments(new Subject<RecordSegment>(), stream1, CancellationToken.None).Wait();
+
+ Assert.Equal(';', format.Delimeter);
+ Assert.Equal(5, format.FieldCount);
+ Assert.Equal(0, format.TimestampFieldIndex);
+ Assert.Equal(1, format.LevelFieldIndex);
+ Assert.Equal(2, format.MessageFieldIndex);
+ Assert.Equal(3, format.LoggerFieldIndex);
+ Assert.Equal(4, format.ExceptionFieldIndex);
+
+ format = new CsvLogFormat {ReadHeader = true};
+ format.ReadSegments(new Subject<RecordSegment>(), stream2, CancellationToken.None).Wait();
+
+ Assert.Equal(',', format.Delimeter);
+ }
+
+ [Fact]
+ public void ReadsSegmentWithQuotedField() {
+ var stream = CreateStream(
+ "time;level;message;logger;exception\r\n" +
+ "2012-01-01 00:00:00;Info;\"Quoted \r\n field \r \n\";Program;Exception");
+
+ var format = new CsvLogFormat();
+ var subject = new ReplaySubject<RecordSegment>();
+
+ format.ReadSegments(subject, stream, CancellationToken.None).Wait();
+
+ subject.OnCompleted();
+
+ var segment = subject.ToEnumerable().FirstOrDefault();
+
+ Assert.Equal(37, segment.Offset);
+ Assert.Equal(66, segment.Length);
+ }
+
+ [Fact]
+ public void DeserializesRecord() {
+ var bytes = Encoding.UTF8.GetBytes("2012-01-01 01:39:40;Info;\"Quoted \r\n field \r \n\";Program;Exception");
+
+ var format = new CsvLogFormat {
+ Delimeter = ';',
+ FieldCount = 5,
+ TimestampFieldIndex = 0,
+ LevelFieldIndex = 1,
+ MessageFieldIndex = 2,
+ LoggerFieldIndex = 3,
+ ExceptionFieldIndex = 4
+ };
+
+ var record = format.DeserializeRecord(new ArraySegment<byte>(bytes));
+
+ Assert.Equal(new DateTime(2012, 1, 1, 1, 39, 40), record.Timestamp);
+ Assert.Equal(LogLevel.Info, record.Level);
+ Assert.Equal("Quoted \r\n field \r \n", record.Message);
+ Assert.Equal("Program", record.Logger);
+ Assert.Equal("Exception", record.Exception);
+ }
+
+ private static MemoryStream CreateStream(string content) {
+ var bytes = Encoding.UTF8.GetBytes(content);
+ var stream = new MemoryStream(bytes);
+ return stream;
+ }
+ }
+}
70 LogWatch.Tests/Formats/Log4JXmlFormatTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using LogWatch.Formats;
+using Microsoft.Reactive.Testing;
+using Xunit;
+
+namespace LogWatch.Tests.Formats {
+ public class Log4JXmlFormatTests {
+ [Fact]
+ public void ReadsSegments() {
+ var bytes = Encoding.UTF8.GetBytes(
+ "<log4j:event logger=\"ConsoleApplication1.Program\" level=\"INFO\" timestamp=\"1361281966733\" thread=\"1\">" +
+ " <log4j:message>Istcua orojurf bysgurnl t.</log4j:message>" +
+ " <log4j:properties>" +
+ " <log4j:data name=\"log4japp\" value=\"ConsoleApplication1.exe(6512)\" />" +
+ " <log4j:data name=\"log4jmachinename\" value=\"user1\" />" +
+ " </log4j:properties>" +
+ "</log4j:event>" +
+ "<log4j:event logger=\"ConsoleApplication1.Program\" level=\"WARN\" timestamp=\"1361281966808\" thread=\"1\">" +
+ " <log4j:message>Ebo ohow aco inldrfb pameenegy.</log4j:message>" +
+ " <log4j:properties>" +
+ " <log4j:data name=\"log4japp\" value=\"ConsoleApplication1.exe(6512)\" />" +
+ " <log4j:data name=\"log4jmachinename\" value=\"user2\" />" +
+ " </log4j:properties>" +
+ "</log4j:event>");
+
+ var stream = new MemoryStream(bytes);
+ var format = new Log4JXmlLogFormat();
+ var testScheduler = new TestScheduler();
+ var observer = testScheduler.CreateObserver<RecordSegment>();
+
+ var offset = format.ReadSegments(observer, stream, CancellationToken.None).Result;
+
+ Assert.Equal(2, observer.Messages.Count);
+
+ var segments = observer.Messages.Select(x => x.Value.Value).ToArray();
+
+ Assert.True(segments.Any(x => x.Offset == 0 && x.Length == 342));
+ Assert.True(segments.Any(x => x.Offset == 342 && x.Length == 347));
+ Assert.Equal(stream.Length, offset);
+ }
+
+ [Fact]
+ public void DeserializesRecord() {
+ var bytes = Encoding.UTF8.GetBytes(
+ "<log4j:event logger=\"ConsoleApplication1.Program\" level=\"INFO\" timestamp=\"1361281966733\" thread=\"1\">" +
+ " <log4j:message>Istcua orojurf bysgurnl t.</log4j:message>" +
+ " <log4j:properties>" +
+ " <log4j:data name=\"log4japp\" value=\"ConsoleApplication1.exe(6512)\" />" +
+ " <log4j:data name=\"log4jmachinename\" value=\"user1\" />" +
+ " <log4j:data name=\"exception\" value=\"TestException\" />" +
+ " </log4j:properties>" +
+ "</log4j:event>");
+
+ var format = new Log4JXmlLogFormat();
+
+ var record = format.DeserializeRecord(new ArraySegment<byte>(bytes));
+
+ Assert.Equal(LogLevel.Info, record.Level);
+ Assert.Equal("Istcua orojurf bysgurnl t.", record.Message);
+ Assert.Equal("ConsoleApplication1.Program", record.Logger);
+ Assert.Equal("1", record.Thread);
+ Assert.Equal("TestException", record.Exception);
+ Assert.Equal(new DateTime(2013, 02, 19, 13, 52, 47), record.Timestamp);
+ }
+ }
+}
117 LogWatch.Tests/LogWatch.Tests.csproj
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{02D87BF3-0627-4D3F-90A5-85564F0A9E2D}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>LogWatch.Tests</RootNamespace>
+ <AssemblyName>LogWatch.Tests</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
+ <RestorePackages>true</RestorePackages>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <PlatformTarget>x86</PlatformTarget>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <PlatformTarget>x86</PlatformTarget>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="GalaSoft.MvvmLight.Extras.WPF45">
+ <HintPath>..\packages\MvvmLightLibs.4.1.27.0\lib\net45\GalaSoft.MvvmLight.Extras.WPF45.dll</HintPath>
+ </Reference>
+ <Reference Include="GalaSoft.MvvmLight.WPF45, Version=4.1.27.22784, Culture=neutral, PublicKeyToken=eabbf6a5f5af5004, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\MvvmLightLibs.4.1.27.0\lib\net45\GalaSoft.MvvmLight.WPF45.dll</HintPath>
+ </Reference>
+ <Reference Include="Microsoft.Practices.ServiceLocation">
+ <HintPath>..\packages\CommonServiceLocator.1.0\lib\NET35\Microsoft.Practices.ServiceLocation.dll</HintPath>
+ </Reference>
+ <Reference Include="Microsoft.Reactive.Testing">
+ <HintPath>..\packages\Rx-Testing.2.1.30214.0\lib\Net45-Full\Microsoft.Reactive.Testing.dll</HintPath>
+ </Reference>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+ <Reference Include="Moq">
+ <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="PresentationFramework" />
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Reactive.Core">
+ <HintPath>..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Interfaces">
+ <HintPath>..\packages\Rx-Interfaces.2.1.30214.0\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Linq">
+ <HintPath>..\packages\Rx-Linq.2.1.30214.0\lib\Net45\System.Reactive.Linq.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.PlatformServices">
+ <HintPath>..\packages\Rx-PlatformServices.2.1.30214.0\lib\Net45\System.Reactive.PlatformServices.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Windows.Threading, Version=2.1.30214.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Xaml.2.1.30214.0\lib\Net45\System.Reactive.Windows.Threading.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\MvvmLightLibs.4.1.27.0\lib\net45\System.Windows.Interactivity.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Xaml" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ <Reference Include="WindowsBase" />
+ <Reference Include="xunit">
+ <HintPath>..\packages\xunit.1.9.1\lib\net20\xunit.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Formats\CsvFormatTests.cs" />
+ <Compile Include="Formats\Log4JXmlFormatTests.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="RecordCollectionTests.cs" />
+ <Compile Include="ViewModels\RecordDetailsViewModelTests.cs" />
+ <Compile Include="ViewModels\RecordsViewModelTests.cs" />
+ <Compile Include="ViewModels\StatsViewModelTests.cs" />
+ <Compile Include="TestMessenger.cs" />
+ <Compile Include="TestSynchronizationContext.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\LogWatch\LogWatch.csproj">
+ <Project>{bd78d588-dd85-4006-9952-3ff51b9390c7}</Project>
+ <Name>LogWatch</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
36 LogWatch.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("LogWatch.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("LogWatch.Tests")]
+[assembly: AssemblyCopyright("Copyright © 2013")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("a31add10-a060-477e-a912-8bf0d614b1ce")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
91 LogWatch.Tests/RecordCollectionTests.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reactive.Threading.Tasks;
+using System.Threading;
+using System.Threading.Tasks;
+using LogWatch.Features.Records;
+using LogWatch.Sources;
+using Microsoft.Reactive.Testing;
+using Moq;
+using Xunit;
+
+namespace LogWatch.Tests {
+ public class RecordCollectionTests {
+ private readonly RecordCollection collection;
+ private readonly Mock<ILogSource> logSource;
+ private readonly Subject<LogSourceStatus> status = new Subject<LogSourceStatus>();
+ private readonly TestScheduler testScheduler;
+
+ public RecordCollectionTests() {
+ SynchronizationContext.SetSynchronizationContext(new TestSynchronizationContext());
+
+ this.testScheduler = new TestScheduler();
+ this.logSource = new Mock<ILogSource>(MockBehavior.Strict);
+
+ this.logSource.SetupGet(x => x.Status).Returns(this.status.ObserveOn(this.testScheduler));
+
+ this.collection = new RecordCollection(this.logSource.Object) {Scheduler = this.testScheduler};
+ this.collection.Initialize();
+ }
+
+ [Fact]
+ public void LoadsRecord() {
+ this.logSource
+ .Setup(x => x.ReadRecordAsync(1, CancellationToken.None))
+ .Returns(Task.FromResult(new Record {Index = 1}));
+
+ var record = this.collection.GetRecordAsync(1, CancellationToken.None).Result;
+
+ Assert.Equal(1, record.Index);
+ }
+
+ [Fact(Timeout = 30000)]
+ public void LoadsRequestedRecords() {
+ this.logSource
+ .Setup(x => x.ReadRecordAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
+ .Returns(
+ (int index, CancellationToken cancellationToken) => Task.FromResult(new Record {Index = index}));
+
+ var record1 = this.collection[0];
+ var record2 = this.collection[1];
+
+ var loaded = this.collection.LoadingRecordCount.Where(x => x == 0).FirstAsync().ToTask();
+
+ this.testScheduler.AdvanceBy(TimeSpan.FromMinutes(5).Ticks);
+
+ loaded.Wait();
+
+ Assert.True(record1.IsLoaded);
+ Assert.True(record2.IsLoaded);
+ }
+
+ [Fact]
+ public void UpdatesLoadingRecordCount() {
+ var completionSource = new TaskCompletionSource<Record>();
+
+ this.logSource
+ .Setup(x => x.ReadRecordAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
+ .Returns(completionSource.Task);
+
+ var record = this.collection[1];
+
+ Assert.NotNull(record);
+
+ var count = this.collection.LoadingRecordCount.FirstAsync().ToTask().Result;
+
+ Assert.Equal(1, count);
+ }
+
+ [Fact]
+ public void UpdatesStatus() {
+ this.status.OnNext(new LogSourceStatus(7, true, 20));
+
+ this.testScheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks);
+
+ Assert.Equal(7, this.collection.Count);
+ Assert.True(this.collection.IsProcessingSavedData);
+ Assert.Equal(20, this.collection.Progress);
+ }
+ }
+}
71 LogWatch.Tests/TestMessenger.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using GalaSoft.MvvmLight.Messaging;
+
+namespace LogWatch.Tests {
+ public class TestMessenger : IMessenger {
+ private readonly Dictionary<Type, Delegate> subscribers = new Dictionary<Type, Delegate>();
+
+ public TestMessenger() {
+ this.SentMessages = new List<object>();
+ }
+
+ public List<object> SentMessages { get; set; }
+
+ public void Register<TMessage>(object recipient, Action<TMessage> action) {
+ this.subscribers[typeof (TMessage)] = action;
+ }
+
+ public void Register<TMessage>(object recipient, object token, Action<TMessage> action) {
+ this.subscribers[typeof (TMessage)] = action;
+ }
+
+ public void Register<TMessage>(
+ object recipient,
+ object token,
+ bool receiveDerivedMessagesToo,
+ Action<TMessage> action) {
+ this.subscribers[typeof (TMessage)] = action;
+ }
+
+ public void Register<TMessage>(object recipient, bool receiveDerivedMessagesToo, Action<TMessage> action) {
+ this.subscribers[typeof (TMessage)] = action;
+ }
+
+ public void Send<TMessage>(TMessage message) {
+ this.SentMessages.Add(message);
+ this.InvokeSubscriber(message);
+ }
+
+ public void Send<TMessage, TTarget>(TMessage message) {
+ this.SentMessages.Add(message);
+ this.InvokeSubscriber(message);
+ }
+
+ public void Send<TMessage>(TMessage message, object token) {
+ this.SentMessages.Add(message);
+ this.InvokeSubscriber(message);
+ }
+
+ public void Unregister(object recipient) {
+ }
+
+ public void Unregister<TMessage>(object recipient) {
+ }
+
+ public void Unregister<TMessage>(object recipient, object token) {
+ }
+
+ public void Unregister<TMessage>(object recipient, Action<TMessage> action) {
+ }
+
+ public void Unregister<TMessage>(object recipient, object token, Action<TMessage> action) {
+ }
+
+ private void InvokeSubscriber<TMessage>(TMessage message) {
+ foreach (var subscriber in this.subscribers)
+ if (subscriber.Key == message.GetType())
+ subscriber.Value.DynamicInvoke(message);
+ }
+ }
+}
13 LogWatch.Tests/TestSynchronizationContext.cs
@@ -0,0 +1,13 @@
+using System.Threading;
+
+namespace LogWatch.Tests {
+ public class TestSynchronizationContext : SynchronizationContext {
+ public override void Post(SendOrPostCallback d, object state) {
+ d(state);
+ }
+
+ public override void Send(SendOrPostCallback d, object state) {
+ d(state);
+ }
+ }
+}
36 LogWatch.Tests/ViewModels/RecordDetailsViewModelTests.cs
@@ -0,0 +1,36 @@
+using GalaSoft.MvvmLight.Messaging;
+using LogWatch.Features.RecordDetails;
+using LogWatch.Features.SelectSource;
+using LogWatch.Messages;
+using Xunit;
+
+namespace LogWatch.Tests.ViewModels {
+ public class RecordDetailsViewModelTests {
+ private readonly TestMessenger messenger;
+ private readonly RecordDetailsViewModel viewModel;
+
+ public RecordDetailsViewModelTests() {
+ this.messenger = new TestMessenger();
+
+ Messenger.OverrideDefault(this.messenger);
+
+ this.viewModel = new RecordDetailsViewModel();
+ }
+
+ [Fact]
+ public void ShowsSelectedRecordDetails() {
+ this.messenger.Send(new RecordSelectedMessage(new Record {Index = 7}));
+
+ Assert.NotNull(this.viewModel.Record);
+ Assert.Equal(7, this.viewModel.Record.Index);
+ }
+ }
+
+ public class SelectSourceViewModelTests {
+ private SelectSourceViewModel viewModel;
+
+ public SelectSourceViewModelTests() {
+ this.viewModel = new SelectSourceViewModel();
+ }
+ }
+}
83 LogWatch.Tests/ViewModels/RecordsViewModelTests.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Threading;
+using GalaSoft.MvvmLight.Messaging;
+using LogWatch.Features.Records;
+using LogWatch.Messages;
+using LogWatch.Sources;
+using Microsoft.Reactive.Testing;
+using Moq;
+using Xunit;
+
+namespace LogWatch.Tests.ViewModels {
+ public class RecordsViewModelTests {
+ private readonly Mock<ILogSource> logSource;
+ private readonly Subject<LogSourceStatus> status = new Subject<LogSourceStatus>();
+ private readonly TestMessenger testMessenger;
+ private readonly TestScheduler testScheduler;
+ private readonly RecordsViewModel viewModel;
+
+ public RecordsViewModelTests() {
+ SynchronizationContext.SetSynchronizationContext(new TestSynchronizationContext());
+
+ this.testScheduler = new TestScheduler();
+ this.testMessenger = new TestMessenger();
+
+ Messenger.OverrideDefault(this.testMessenger);
+
+ this.logSource = new Mock<ILogSource>(MockBehavior.Strict);
+
+ this.logSource
+ .SetupGet(x => x.Status)
+ .Returns(this.status.ObserveOn(this.testScheduler));
+
+ this.viewModel = new RecordsViewModel {
+ LogSourceInfo = new LogSourceInfo(this.logSource.Object, null, false, false),
+ Scheduler = this.testScheduler
+ };
+
+ this.viewModel.Initialize();
+ }
+
+ [Fact]
+ public void SelectsRecord() {
+ this.viewModel.SelectRecordCommand.Execute(this.viewModel.Records[1]);
+
+ var message = this.testMessenger.SentMessages.OfType<RecordSelectedMessage>().Single();
+
+ Assert.Equal(1, message.Record.Index);
+ }
+
+ [Fact]
+ public void NavigatesToRecord() {
+ var recordIndex = 0;
+
+ this.viewModel.Navigated += (sender, args) => recordIndex = ((GoToIndexEventArgs) args).Index;
+
+ this.testMessenger.Send(new NavigatedToRecordMessage(7));
+
+ Assert.Equal(7, recordIndex);
+ }
+
+ [Fact]
+ public void ReportsCurrentRecordContext() {
+ RecordContextChangedMessage message = null;
+
+ this.testMessenger.Register<RecordContextChangedMessage>(this, x => message = x);
+
+ var context = new Subject<VisibleItemsInfo>();
+
+ this.viewModel.RecordContext = context.ObserveOn(this.testScheduler);
+
+ context.OnNext(new VisibleItemsInfo(new Record {Index = 7}, new Record {Index = 23}));
+
+ this.testScheduler.AdvanceBy(TimeSpan.FromMinutes(5).Ticks);
+
+ Assert.NotNull(message);
+ Assert.Equal(7, message.FromRecord.Index);
+ Assert.Equal(23, message.ToRecord.Index);
+ }
+ }
+}
82 LogWatch.Tests/ViewModels/StatsViewModelTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Threading;
+using GalaSoft.MvvmLight.Messaging;
+using LogWatch.Features.Stats;
+using LogWatch.Messages;
+using LogWatch.Sources;
+using Microsoft.Reactive.Testing;
+using Moq;
+using Xunit;
+
+namespace LogWatch.Tests.ViewModels {
+ public class StatsViewModelTests {
+ private readonly Mock<ILogSource> logSource;
+ private readonly TestMessenger messenger;
+ private readonly Subject<Record> records = new Subject<Record>();
+ private readonly TestScheduler testScheduler;
+ private readonly StatsViewModel viewModel;
+
+ public StatsViewModelTests() {
+ SynchronizationContext.SetSynchronizationContext(new TestSynchronizationContext());
+
+ this.testScheduler = new TestScheduler();
+ this.messenger = new TestMessenger();
+ this.logSource = new Mock<ILogSource>();
+ this.logSource.SetupGet(x => x.Records).Returns(this.records.ObserveOn(this.testScheduler));
+
+ Messenger.OverrideDefault(this.messenger);
+
+ this.viewModel = new StatsViewModel {
+ Scheduler = this.testScheduler,
+ LogSourceInfo = new LogSourceInfo(this.logSource.Object, null, false, false)
+ };
+ }
+
+ [Fact]
+ public void NavigatesToRecord() {
+ this.viewModel.CollectCommand.Execute(null);
+
+ this.records.OnNext(new Record {Index = 1, Level = LogLevel.Trace});
+ this.records.OnNext(new Record {Index = 2, Level = LogLevel.Debug});
+ this.records.OnNext(new Record {Index = 3, Level = LogLevel.Info});
+ this.records.OnNext(new Record {Index = 4, Level = LogLevel.Warn});
+ this.records.OnNext(new Record {Index = 5, Level = LogLevel.Error});
+ this.records.OnNext(new Record {Index = 6, Level = LogLevel.Fatal});
+
+ this.testScheduler.AdvanceBy(TimeSpan.FromMinutes(5).Ticks);
+
+ this.messenger.Send(
+ new RecordContextChangedMessage(
+ new Record {Index = 1},
+ new Record {Index = 2}));
+
+ this.viewModel.GoToNextCommand.Execute(LogLevel.Error);
+
+ Assert.NotEmpty(this.messenger.SentMessages.OfType<NavigatedToRecordMessage>().Where(x => x.Index == 5));
+ }
+
+ [Fact]
+ public void CollectsStats() {
+ this.viewModel.CollectCommand.Execute(null);
+
+ this.records.OnNext(new Record {Index = 1, Level = LogLevel.Trace});
+ this.records.OnNext(new Record {Index = 2, Level = LogLevel.Debug});
+ this.records.OnNext(new Record {Index = 3, Level = LogLevel.Info});
+ this.records.OnNext(new Record {Index = 4, Level = LogLevel.Warn});
+ this.records.OnNext(new Record {Index = 5, Level = LogLevel.Error});
+ this.records.OnNext(new Record {Index = 6, Level = LogLevel.Fatal});
+
+ this.testScheduler.AdvanceBy(TimeSpan.FromMinutes(5).Ticks);
+
+ Assert.Equal(1, this.viewModel.TraceCount);
+ Assert.Equal(1, this.viewModel.DebugCount);
+ Assert.Equal(1, this.viewModel.InfoCount);
+ Assert.Equal(1, this.viewModel.WarnCount);
+ Assert.Equal(1, this.viewModel.ErrorCount);
+ Assert.Equal(1, this.viewModel.FatalCount);
+ }
+ }
+}
14 LogWatch.Tests/packages.config
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="CommonServiceLocator" version="1.0" targetFramework="net45" />
+ <package id="Moq" version="4.0.10827" targetFramework="net45" />
+ <package id="MvvmLightLibs" version="4.1.27.0" targetFramework="net45" />
+ <package id="Rx-Core" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-Interfaces" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-Linq" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-Main" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-PlatformServices" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-Testing" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-WPF" version="2.1.30214.0" targetFramework="net45" />
+ <package id="Rx-Xaml" version="2.1.30214.0" targetFramework="net45" />
+</packages>
33 LogWatch.sln
@@ -0,0 +1,33 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogWatch", "LogWatch\LogWatch.csproj", "{BD78D588-DD85-4006-9952-3FF51B9390C7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogWatch.Tests", "LogWatch.Tests\LogWatch.Tests.csproj", "{02D87BF3-0627-4D3F-90A5-85564F0A9E2D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{D0AA1C1B-3669-4BDA-A818-647FE722EED6}"
+ ProjectSection(SolutionItems) = preProject
+ .nuget\NuGet.Config = .nuget\NuGet.Config
+ .nuget\NuGet.exe = .nuget\NuGet.exe
+ .nuget\NuGet.targets = .nuget\NuGet.targets
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BD78D588-DD85-4006-9952-3FF51B9390C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD78D588-DD85-4006-9952-3FF51B9390C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD78D588-DD85-4006-9952-3FF51B9390C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD78D588-DD85-4006-9952-3FF51B9390C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {02D87BF3-0627-4D3F-90A5-85564F0A9E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {02D87BF3-0627-4D3F-90A5-85564F0A9E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {02D87BF3-0627-4D3F-90A5-85564F0A9E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {02D87BF3-0627-4D3F-90A5-85564F0A9E2D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
98 LogWatch.sln.DotSettings
@@ -0,0 +1,98 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Default/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Default"&gt;&lt;CSArrangeThisQualifier&gt;True&lt;/CSArrangeThisQualifier&gt;&lt;CSRemoveCodeRedundancies&gt;True&lt;/CSRemoveCodeRedundancies&gt;&lt;CSUseAutoProperty&gt;True&lt;/CSUseAutoProperty&gt;&lt;CSMakeFieldReadonly&gt;True&lt;/CSMakeFieldReadonly&gt;&lt;CSUseVar&gt;&lt;BehavourStyle&gt;CAN_CHANGE_TO_IMPLICIT&lt;/BehavourStyle&gt;&lt;LocalVariableStyle&gt;ALWAYS_IMPLICIT&lt;/LocalVariableStyle&gt;&lt;ForeachVariableStyle&gt;ALWAYS_IMPLICIT&lt;/ForeachVariableStyle&gt;&lt;/CSUseVar&gt;&lt;CSShortenReferences&gt;True&lt;/CSShortenReferences&gt;&lt;CSReformatCode&gt;True&lt;/CSReformatCode&gt;&lt;CSharpFormatDocComments&gt;True&lt;/CSharpFormatDocComments&gt;&lt;CSReorderTypeMembers&gt;True&lt;/CSReorderTypeMembers&gt;&lt;XMLReformatCode&gt;True&lt;/XMLReformatCode&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;/Profile&gt;</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeCleanup/RecentlyUsedProfile/@EntryValue">Default</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">Default</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_ARGUMENT/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_EXTENDS_LIST/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_PARAMETER/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_LIST/@EntryValue">False</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/CASE_BLOCK_BRACES/@EntryValue">END_OF_LINE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FIXED_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOR_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOREACH_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_IFELSE_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_USING_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_WHILE_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INITIALIZER_BRACES/@EntryValue">END_OF_LINE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INVOCABLE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+ <s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_DECLARATIONS/@EntryValue">1</s:Int64>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue">END_OF_LINE</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_CATCH_ON_NEW_LINE/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ELSE_ON_NEW_LINE/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FINALLY_ON_NEW_LINE/@EntryValue">False</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/REDUNDANT_THIS_QUALIFIER_STYLE/@EntryValue">ALWAYS_USE</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">LINE_BREAK</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/STICK_COMMENT/@EntryValue">False</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/TYPE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_FIRST_TYPE_PARAMETER_CONSTRAINT/@EntryValue">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_OBJECT_AND_COLLECTION_INITIALIZER_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_TERNARY_EXPR_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/JavaScriptFormatOther/ALIGN_MULTIPLE_DECLARATION/@EntryValue">True</s:Boolean>
+ <s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/XmlFormatter/MaxSingleLineTagLength/@EntryValue">20</s:Int64>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/XmlFormatter/TagAttributeIndenting/@EntryValue">OneStep</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/XmlFormatter/WrapInsideText/@EntryValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Constructor/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Constructor/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Delegating/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Delegating/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Equality/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Equality/Options/=EqualityOperators/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Equality/Options/=ImplementIEquatable/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Equality/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=EqualityMembers/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=EqualityMembers/Options/=EqualityOperators/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=EqualityMembers/Options/=ImplementIEquatable/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Global/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Global/Options/=DebuggerStepsThrough/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Global/Options/=PropertyBody/@EntryIndexedValue">Automatic property</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Global/Options/=WrapInRegion/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Implementations/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Implementations/Options/=WrapInRegion/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Implementations/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Overrides/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=WrapInRegion/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=Properties/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Properties/Options/=DebuggerStepsThrough/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=Properties/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/Generate/=ReadProperties/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Generate/=ReadProperties/Options/=DebuggerStepsThrough/@EntryIndexedValue">False</s:String>
+ <s:String x:Key="/Default/CodeStyle/Generate/=ReadProperties/Options/=XmlDocumentation/@EntryIndexedValue">False</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/IntroduceVariableUseVar/UseVarForIntroduceVariableRefactoring/@EntryValue">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AD/@EntryIndexedValue">AD</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CRC/@EntryIndexedValue">CRC</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DN/@EntryIndexedValue">DN</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FTP/@EntryIndexedValue">FTP</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GB/@EntryIndexedValue">GB</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSV/@EntryIndexedValue">HSV</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTML/@EntryIndexedValue">HTML</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IO/@EntryIndexedValue">IO</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MB/@EntryIndexedValue">MB</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue">MD</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NFS/@EntryIndexedValue">NFS</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OK/@EntryIndexedValue">OK</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PDF/@EntryIndexedValue">PDF</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RIR/@EntryIndexedValue">RIR</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RTF/@EntryIndexedValue">RTF</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SHA/@EntryIndexedValue">SHA</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SMB/@EntryIndexedValue">SMB</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SMTP/@EntryIndexedValue">SMTP</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=WKID/@EntryIndexedValue">WKID</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=WKT/@EntryIndexedValue">WKT</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=XML/@EntryIndexedValue">XML</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/EventHandlerPatternLong/@EntryValue">$object$_On$event$</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/VBNaming/EventHandlerPatternLong/@EntryValue">$object$_On$event$</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
+ <s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=EDEE567DCD9AEC4A97E53709F3B893B0/@KeyIndexDefined">True</s:Boolean>
+ <s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=EDEE567DCD9AEC4A97E53709F3B893B0/AbsolutePath/@EntryValue">C:\Users\shse\Documents\GitHub\LogWatch\LogWatch.sln.DotSettings</s:String>
+ <s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEDEE567DCD9AEC4A97E53709F3B893B0/@KeyIndexDefined">True</s:Boolean>
+ <s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEDEE567DCD9AEC4A97E53709F3B893B0/RelativePriority/@EntryValue">1</s:Double></wpf:ResourceDictionary>
18 LogWatch/App.config
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
+ </startup>
+ <runtime>
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+ <dependentAssembly>
+ <assemblyIdentity name="System.Windows.Interactivity" publicKeyToken="31bf3856ad364e35" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
+ </dependentAssembly>
+ <dependentAssembly>
+ <assemblyIdentity name="Microsoft.Windows.Design.Extensibility" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
+ </dependentAssembly>
+ </assemblyBinding>
+ </runtime>
+</configuration>
BIN LogWatch/App.ico
Binary file not shown.
23 LogWatch/App.xaml
@@ -0,0 +1,23 @@
+<Application
+ x:Class="LogWatch.App"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ DispatcherUnhandledException="OnDispatcherUnhandledException">
+ <Application.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/FirstFloor.ModernUI;component/Assets/ModernUI.xaml" />
+ <ResourceDictionary Source="/FirstFloor.ModernUI;component/Assets/ModernUI.Dark.xaml" />
+ <ResourceDictionary Source="/LogWatch;component/Styles/CompactMenu.xaml" />
+ <ResourceDictionary Source="/LogWatch;component/Styles/CompactWindow.xaml" />
+ </ResourceDictionary.MergedDictionaries>
+
+ <SolidColorBrush x:Key="LogLevelTrace" Color="Gray" />
+ <SolidColorBrush x:Key="LogLevelDebug" Color="White" />
+ <SolidColorBrush x:Key="LogLevelInfo" Color="Turquoise" />
+ <SolidColorBrush x:Key="LogLevelWarn" Color="Yellow" />
+ <SolidColorBrush x:Key="LogLevelError" Color="Red" />
+ <SolidColorBrush x:Key="LogLevelFatal" Color="#CDFF0000" />
+ </ResourceDictionary>
+ </Application.Resources>
+</Application>
135 LogWatch/App.xaml.cs
@@ -0,0 +1,135 @@
+using System;
+using System.ComponentModel.Composition;
+using System.ComponentModel.Composition.Hosting;
+using System.IO;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Threading;
+using FirstFloor.ModernUI.Windows.Controls;
+using LogWatch.Features.SelectSource;
+using LogWatch.Formats;
+using LogWatch.Sources;
+using Microsoft.Win32;
+
+namespace LogWatch {
+ public sealed partial class App {
+ public static readonly Action<Exception> HandleException =
+ exception => {
+ if (Current.Dispatcher.CheckAccess())
+ if (exception is AggregateException)
+ ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
+ else
+ ExceptionDispatchInfo.Capture(exception).Throw();
+ else
+ Current.Dispatcher.Invoke(() => HandleException(exception));
+ };
+
+ public static readonly Func<string> OpenFileDialog =
+ () => {
+ var dialog = new OpenFileDialog {
+ CheckFileExists = true,
+ CheckPathExists = true
+ };
+
+ if (dialog.ShowDialog() == true)
+ return dialog.FileName;
+
+ return null;
+ };
+
+ public static readonly Action<string> ErrorDialog =
+ message => ModernDialog.ShowMessage(message, "Error", MessageBoxButton.OK);
+
+ public static readonly Action<string> InfoDialog =
+ message => ModernDialog.ShowMessage(message, "Log Watch", MessageBoxButton.OK);
+
+ private static readonly Func<string, bool> CollectStatsOnDemand =
+ filePath => new FileInfo(filePath).Length >= 10*1024*1024;
+
+ private static readonly CompositionContainer Container = new CompositionContainer(
+ new AssemblyCatalog(typeof (App).Assembly));
+
+ public static readonly Func<Stream, ILogFormat> SelectFormat = stream => {
+ var formatSelector = new AutoLogFormatSelector();
+
+ Container.SatisfyImportsOnce(formatSelector);
+
+ var logFormats = formatSelector.SelectFormat(stream).ToArray();
+
+ if (logFormats.Length == 0)
+ return new PlainTextLogFormat();
+
+ if (logFormats.Length == 1)
+ return logFormats[0].Value;
+
+ var view = new SelectFormatView();
+ var viewModel = view.ViewModel;
+
+ foreach (var format in logFormats)
+ viewModel.Formats.Add(format);
+
+ viewModel.Formats.Add(
+ new Lazy<ILogFormat, ILogFormatMetadata>(
+ () => new PlainTextLogFormat(),
+ new LogFormatAttribute("Plain text")));
+
+ return view.ShowDialog() != true ? null : viewModel.Format;
+ };
+
+ public static readonly Func<string, LogSourceInfo> CreateFileLogSource = filePath => {
+ ILogFormat logFormat;
+
+ using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ logFormat = SelectFormat(stream);
+
+ if (logFormat == null)
+ return null;
+
+ return new LogSourceInfo(
+ new FileLogSource(filePath, logFormat),
+ filePath,
+ false,
+ CollectStatsOnDemand(filePath));
+ };
+
+ public static LogSourceInfo SourceInfo { get; set; }
+
+ protected override void OnStartup(StartupEventArgs e) {
+ base.OnStartup(e);
+
+ TaskScheduler.UnobservedTaskException += (sender, args) => HandleException(args.Exception);
+
+ var filePath = e.Args.FirstOrDefault();
+
+ this.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ if (string.IsNullOrEmpty(filePath)) {
+ var view = new SelectSourceView();
+
+ if (view.ShowDialog() == true)
+ SourceInfo = view.ViewModel.Source;
+ } else
+ SourceInfo = CreateFileLogSource(filePath);
+
+ if (SourceInfo == null) {
+ this.Shutdown();
+ return;
+ }
+
+ this.ShutdownMode = ShutdownMode.OnLastWindowClose;
+
+ this.MainWindow = new ShellView();
+ this.MainWindow.Closed += (sender, args) => SourceInfo.Source.Dispose();
+ this.MainWindow.Show();
+ }
+
+ private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) {
+ ErrorDialog(e.Exception.ToString());
+
+ if (e.Exception is ApplicationException)
+ e.Handled = true;
+ }
+ }
+}
117 LogWatch/Features/RecordDetails/ExceptionHighlighter.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace LogWatch.Features.RecordDetails {
+ public class ExceptionHighlighter : Freezable, IValueConverter {
+ private static readonly Regex Header = new Regex(@"^(?<NS>(?:\w+[.])+)(?<Class>\w+): (?<Message>.*)");
+
+ private static readonly Regex StackTraceItem = new Regex(
+ @"^\s+\w+\s+(?<NS>(?:\w+[.])+)(?<Class>[\w\[\],<>]+)[.](?<Method><.ctor>\w+|[\w\[\],<>]+)[(](?<Params>[^)]+)?[)](?:\s+\w+\s+(?<Url>.+):\w+\s+(?<Line>\d+))?");
+
+ public static readonly DependencyProperty NavigateToUrlCommandProperty =
+ DependencyProperty.Register("NavigateToUrlCommand", typeof (ICommand), typeof (ExceptionHighlighter));
+
+ public Brush Namespace { get; set; }
+ public Brush Method { get; set; }
+ public Brush Message { get; set; }
+ public Brush MethodParameterType { get; set; }
+ public Brush Class { get; set; }
+ public Brush String { get; set; }
+ public Brush Line { get; set; }
+
+ public ICommand NavigateToUrlCommand {
+ get { return (ICommand) this.GetValue(NavigateToUrlCommandProperty); }
+ set { this.SetValue(NavigateToUrlCommandProperty, value); }
+ }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
+ return this.CreateTextBlock((string) value);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
+ throw new NotSupportedException();
+ }
+
+ private TextBlock CreateTextBlock(string exception) {
+ if (string.IsNullOrEmpty(exception))
+ return new TextBlock {Text = exception};
+
+ var result = new TextBlock();
+
+ var lines = exception.Split(new[] {Environment.NewLine}, StringSplitOptions.None);
+
+ foreach (var line in lines) {
+ var span = new Span();
+
+ if (!this.TryHeader(line, span) && !this.TryStackTraceItem(line, span))
+ span.Inlines.Add(line);
+
+ span.Inlines.Add(new LineBreak());
+ result.Inlines.Add(span);
+ }
+
+ return result;
+ }
+
+ private bool TryStackTraceItem(string line, Span span) {
+ var match = StackTraceItem.Match(line);
+ if (match.Success) {
+ span.Inlines.Add(" at ");
+ span.Inlines.Add(new Run(match.Groups["NS"].Value) {Foreground = this.Namespace});
+ span.Inlines.Add(new Run(match.Groups["Class"].Value) {Foreground = this.Class});
+ span.Inlines.Add(".");
+ span.Inlines.Add(new Run(match.Groups["Method"].Value) {Foreground = this.Method});
+ span.Inlines.Add("(");
+ span.Inlines.Add(new Run(match.Groups["Params"].Value));
+ span.Inlines.Add(")");
+
+ var url = match.Groups["Url"].Value;
+
+ if (!string.IsNullOrEmpty(url)) {
+ var hyperlink = new Hyperlink(new Run(Path.GetFileName(url))) {ToolTip = url};
+
+ BindingOperations.SetBinding(hyperlink, Hyperlink.CommandProperty,
+ new Binding("NavigateToUrlCommand") {
+ Source = this,
+ Mode = BindingMode.OneWay
+ });
+
+ hyperlink.CommandParameter = url;
+
+ span.Inlines.Add(" in ");
+ span.Inlines.Add(hyperlink);
+ span.Inlines.Add(":line ");
+ span.Inlines.Add(new Run(match.Groups["Line"].Value) {Foreground = this.Line});
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private bool TryHeader(string line, Span span) {
+ var match = Header.Match(line);
+
+ if (match.Success) {
+ span.Inlines.Add(new Run(match.Groups["NS"].Value) {Foreground = this.Namespace});
+ span.Inlines.Add(new Run(match.Groups["Class"].Value) {Foreground = this.Class});
+ span.Inlines.Add(": ");
+ span.Inlines.Add(new Run(match.Groups["Message"].Value) {Foreground = this.Message});
+ return true;
+ }
+
+ return false;
+ }
+
+ protected override Freezable CreateInstanceCore() {
+ return this;
+ }
+ }
+}
84 LogWatch/Features/RecordDetails/RecordDetailsView.xaml
@@ -0,0 +1,84 @@
+<UserControl
+ x:Class="LogWatch.Features.RecordDetails.RecordDetailsView"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:feature="clr-namespace:LogWatch.Features.RecordDetails"
+ xmlns:local="clr-namespace:LogWatch"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ Background="#1A1A1A"
+ d:DesignHeight="300"
+ d:DesignWidth="300"
+ mc:Ignorable="d">
+
+ <UserControl.Resources>
+ <feature:RecordDetailsViewModel x:Key="ViewModel" ErrorDialog="{x:Static local:App.ErrorDialog}" />
+
+ <feature:ExceptionHighlighter
+ x:Key="Highlighter"
+ Class="#E8BC64"
+ Line="#6897BB"
+ Message="White"
+ Method="White"
+ MethodParameterType="#E8BC64"
+ Namespace="LightGray"
+ NavigateToUrlCommand="{Binding OpenFileCommand}"
+ String="#A5C25C" />
+ </UserControl.Resources>
+
+ <UserControl.DataContext>
+ <StaticResource ResourceKey="ViewModel" />
+ </UserControl.DataContext>
+
+ <ScrollViewer HorizontalScrollBarVisibility="Disabled">
+ <StackPanel>
+ <TextBlock
+ Margin="12,0,12,0"
+ FontFamily="Consolas"
+ FontSize="11pt"
+ Text="{Binding Record.Message}"
+ TextWrapping="Wrap">
+ <TextBlock.Style>
+ <Style TargetType="TextBlock">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Record.Level}" Value="{x:Static local:LogLevel.Trace}">
+ <Setter Property="Foreground" Value="{StaticResource LogLevelTrace}" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Record.Level}" Value="{x:Static local:LogLevel.Debug}">
+ <Setter Property="Foreground" Value="{StaticResource LogLevelDebug}" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Record.Level}" Value="{x:Static local:LogLevel.Info}">
+ <Setter Property="Foreground" Value="{StaticResource LogLevelInfo}" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Record.Level}" Value="{x:Static local:LogLevel.Warn}">
+ <Setter Property="Foreground" Value="{StaticResource LogLevelWarn}" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Record.Level}" Value="{x:Static local:LogLevel.Error}">
+ <Setter Property="Foreground" Value="{StaticResource LogLevelError}" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Record.Level}" Value="{x:Static local:LogLevel.Fatal}">
+ <Setter Property="Foreground" Value="Black" />
+ <Setter Property="Background" Value="{StaticResource LogLevelFatal}" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </TextBlock.Style>
+ </TextBlock>
+
+ <ContentControl
+ Margin="12,5,12,0"
+ Content="{Binding Record.Exception, Converter={StaticResource Highlighter}}"
+ FontFamily="Consolas"
+ FontSize="11pt">
+ <ContentControl.Resources>
+ <!-- ReSharper disable Xaml.RedundantResource -->
+ <Style TargetType="TextBlock">
+ <Setter Property="TextWrapping" Value="Wrap" />
+ </Style>
+ <!-- ReSharper restore Xaml.RedundantResource -->
+ </ContentControl.Resources>
+ </ContentControl>
+
+ </StackPanel>
+ </ScrollViewer>
+</UserControl>
7 LogWatch/Features/RecordDetails/RecordDetailsView.xaml.cs
@@ -0,0 +1,7 @@
+namespace LogWatch.Features.RecordDetails {
+ public partial class RecordDetailsView {
+ public RecordDetailsView() {
+ this.InitializeComponent();
+ }
+ }
+}
95 LogWatch/Features/RecordDetails/RecordDetailsViewModel.cs
@@ -0,0 +1,95 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using GalaSoft.MvvmLight;
+using GalaSoft.MvvmLight.Command;
+using LogWatch.Messages;
+
+namespace LogWatch.Features.RecordDetails {
+ public class RecordDetailsViewModel : ViewModelBase {
+ private Record record;
+
+ public RecordDetailsViewModel() {
+ if (this.IsInDesignMode)
+ return;
+
+ this.CopyAllCommand = new RelayCommand(this.CopyAll);
+ this.OpenFileCommand = new RelayCommand<string>(url => {
+ try {
+ Process.Start(url);
+ } catch (FileNotFoundException) {
+ ErrorDialog("File not found: " + url);
+ } catch (Win32Exception exception) {
+ ErrorDialog(exception.Message);
+ }
+ });
+
+ this.MessengerInstance.Register<RecordSelectedMessage>(this, message => { this.Record = message.Record; });
+ }
+
+ public Action<string> ErrorDialog { get; set; }
+
+ public string Test {
+ get {
+ return
+ @"System.ArgumentException: Destination array is not long enough to copy all the items in the collection. Check array index and length." +
+ Environment.NewLine +
+ @" at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)" + Environment.NewLine +
+ @" at System.BitConverter.ToUInt16(Byte[] value, Int32 startIndex)" + Environment.NewLine +
+ @" at SharpCompress.Common.Zip.Headers.ZipFileEntry.LoadExtra(Byte[] extra)" + Environment.NewLine +
+ @" at SharpCompress.Common.Zip.Headers.LocalEntryHeader.Read(BinaryReader reader)" +
+ Environment.NewLine +
+ @" at SharpCompress.Common.Zip.ZipHeaderFactory.ReadHeader(UInt32 headerBytes, BinaryReader reader)" +
+ Environment.NewLine +
+ @" at SharpCompress.Common.Zip.SeekableZipHeaderFactory.GetLocalHeader(Stream stream, DirectoryEntryHeader directoryEntryHeader)" +
+ Environment.NewLine +
+ @" at SharpCompress.Common.Zip.SeekableZipFilePart.LoadLocalHeader()" + Environment.NewLine +
+ @" at SharpCompress.Common.Zip.SeekableZipFilePart.GetStream()" + Environment.NewLine +
+ @" at SharpCompress.Archive.Zip.ZipArchiveEntry.OpenEntryStream()" + Environment.NewLine +
+ @" at SharpCompress.Utility.Extract[TEntry,TVolume](TEntry entry, AbstractArchive`2 archive, Stream streamToWriteTo)" +
+ Environment.NewLine +
+ @" at SharpCompress.Archive.Zip.ZipArchiveEntry.WriteTo(Stream streamToWriteTo)" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Indexers.ArchiveIndexer.ArchiveEntryInfo.get_FileStream() in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Indexers\ArchiveIndexer.cs:line 131" +
+ Environment.NewLine +
+ @" at Forensics.Worker.FileIndexContext.get_FileStream() in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\FileIndexContext.cs:line 93" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Impl.WorkerTaskExecutorDomainProxy.<.ctor>b__4(FileIndexContext context) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Impl\WorkerTaskExecutorDomainProxy.cs:line 69" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Impl.WorkerTaskExecutor.OnInitializingContext(FileIndexContext context) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Impl\WorkerTaskExecutor.cs:line 161" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Impl.WorkerTaskExecutor.IndexFile(FileIndexContext context, IDictionary`2 footprints) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Impl\WorkerTaskExecutor.cs:line 176" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Impl.WorkerTaskExecutor.<>c__DisplayClass13.<ExecuteTask>b__d(FileIndexContext subcontext) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Impl\WorkerTaskExecutor.cs:line 295" +
+ Environment.NewLine +
+ @" at Forensics.Worker.FileIndexContext.IndexSubfile(IFileInfo subfileInfo, IFileContext subfileContext, String relativeParentId) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\FileIndexContext.cs:line 147" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Indexers.ArchiveIndexer.IndexArchive(IFileIndexContext context, IArchive archive) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Indexers\ArchiveIndexer.cs:line 98" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Indexers.ArchiveIndexer.Index(IFileIndexContext context) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Indexers\ArchiveIndexer.cs:line 67" +
+ Environment.NewLine +
+ @" at Forensics.Worker.Impl.WorkerTaskExecutor.IndexFile(FileIndexContext context, IDictionary`2 footprints) in x:\TeamCity\buildAgent\work\8712fdd031dc966\Forensics.Worker\Impl\WorkerTaskExecutor.cs:line 198" +
+ Environment.NewLine;
+ }
+ }
+
+ public Record Record {
+ get { return this.record; }
+ set { this.Set(ref this.record, value); }
+ }
+
+ public RelayCommand CopyAllCommand { get; set; }
+ public RelayCommand<string> OpenFileCommand { get; set; }
+
+ private void Set<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null) {
+ this.Set(propertyName, ref field, newValue, false);
+ }
+
+ private void CopyAll() {
+ Clipboard.SetText(string.Concat(this.Record.Message, Environment.NewLine, this.Record.Exception));
+ }
+ }
+}
19 LogWatch/Features/RecordDetails/WindowsExplorerLinkNavigator.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Diagnostics;
+using System.Windows;
+using FirstFloor.ModernUI.Windows;
+using FirstFloor.ModernUI.Windows.Navigation;
+
+namespace LogWatch.Features.RecordDetails {
+ public class WindowsExplorerLinkNavigator : ILinkNavigator {
+ public WindowsExplorerLinkNavigator() {
+ this.Commands = new CommandDictionary();
+ }
+
+ public void Navigate(Uri uri, FrameworkElement source, string parameter = null) {
+ Process.Start(uri.LocalPath, parameter);
+ }
+
+ public CommandDictionary Commands { get; set; }
+ }
+}
103 LogWatch/Features/Records/AutoScrollToEndBehaviour.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Interactivity;
+
+namespace LogWatch.Features.Records {
+ public class AutoScrollToEndBehaviour : Behavior<ListView> {
+ public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register(
+ "IsEnabled", typeof (bool), typeof (AutoScrollToEndBehaviour), new PropertyMetadata(true));
+
+ private bool isScrolling;
+
+ private RecordCollection recordCollection;
+ private IDisposable subscription;
+
+ public bool IsEnabled {
+ get { return (bool) this.GetValue(IsEnabledProperty); }
+ set { this.SetValue(IsEnabledProperty, value); }
+ }
+
+ protected override void OnAttached() {
+ base.OnAttached();
+
+ this.recordCollection = this.AssociatedObject.ItemsSource as RecordCollection;
+
+ var descriptor = DependencyPropertyDescriptor.FromProperty(
+ ItemsControl.ItemsSourceProperty,
+ typeof (Control));
+
+ descriptor.AddValueChanged(this.AssociatedObject, this.OnItemsSourceChanged);
+
+ if (this.recordCollection != null)
+ this.Subscribe();
+ }
+
+ private void OnItemsSourceChanged(object sender, EventArgs args) {
+ if (this.recordCollection != null)
+ this.Unsubscribe();
+
+ this.recordCollection = this.AssociatedObject.ItemsSource as RecordCollection;
+
+ if (this.recordCollection != null)
+ this.Subscribe();
+ }
+
+ private void Subscribe() {
+ this.AssociatedObject.AddHandler(ScrollViewer.ScrollChangedEvent, (RoutedEventHandler) this.OnScrollChanged);
+
+ this.subscription =
+ Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(this.recordCollection, "CollectionChanged")
+ .Throttle(TimeSpan.FromMilliseconds(700))
+ .ObserveOnDispatcher()
+ .Where(_ => this.IsEnabled && !this.isScrolling)
+ .Subscribe(_ => this.OnObservableCollectionChanged());
+ }
+
+ private void OnScrollChanged(object sender, RoutedEventArgs routedEventArgs) {
+ var args = (ScrollChangedEventArgs) routedEventArgs;
+
+ if (args.VerticalChange < 0)
+ this.IsEnabled = false;
+ }
+
+ protected override void OnDetaching() {
+ base.OnDetaching();
+
+ if (this.recordCollection != null)
+ this.Unsubscribe();
+ }
+
+ private void Unsubscribe() {
+ this.AssociatedObject.RemoveHandler(
+ ScrollViewer.ScrollChangedEvent,
+ (RoutedEventHandler) this.OnScrollChanged);
+
+ this.subscription.Dispose();
+ }
+
+ private async void OnObservableCollectionChanged() {
+ if (this.recordCollection.Count == 0)
+ return;
+
+ this.isScrolling = true;
+
+ await this.recordCollection.LoadingRecordCount.Where(x => x == 0).FirstAsync();
+
+ var lastRecord =
+ await this.recordCollection.GetRecordAsync(this.recordCollection.Count - 1, CancellationToken.None);
+
+ if (Equals(this.AssociatedObject.Items.CurrentItem, lastRecord))
+ return;
+
+ if (this.AssociatedObject.Items.MoveCurrentTo(lastRecord))
+ this.AssociatedObject.ScrollIntoView(lastRecord);
+
+ this.isScrolling = false;
+ }
+ }
+}
28 LogWatch/Features/Records/AutoSizeColumnBehaviour.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Interactivity;
+
+namespace LogWatch.Features.Records {
+ public class AutoSizeColumnBehaviour : Behavior<ListView> {
+ protected override void OnAttached() {
+ base.OnAttached();
+
+
+ this.AssociatedObject.AddHandler(ScrollViewer.ScrollChangedEvent, (RoutedEventHandler) OnScrollChanged);
+ }
+
+ public int ColumnIndex { get; set; }
+
+ void OnScrollChanged(object sender, RoutedEventArgs args) {
+ var gridView = this.AssociatedObject.View as GridView;
+
+ if(gridView == null)
+ return;
+
+ var column = gridView.Columns[ColumnIndex];
+
+
+ }
+ }
+}
70 LogWatch/Features/Records/FilteredRecordCollection.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using LogWatch.Sources;
+
+namespace LogWatch.Features.Records {
+ public class FilteredRecordCollection : RecordCollection {
+ private readonly Predicate<Record> filter;
+ private readonly ConcurrentDictionary<int, int> recordIndexByLocalIndex = new ConcurrentDictionary<int, int>();
+ private readonly ConcurrentDictionary<int, int> localIndexByRecordIndex = new ConcurrentDictionary<int, int>();
+
+ private int count = -1;
+
+ public FilteredRecordCollection(ILogSource logSource, Predicate<Record> filter) : base(logSource) {
+ this.filter = filter;
+ }
+
+ public int GetLocalIndex(int recordIndex) {
+ int localIndex;
+ if (this.localIndexByRecordIndex.TryGetValue(recordIndex, out localIndex))
+ return localIndex;
+ return -1;
+ }
+
+ protected override IDisposable UpdateState() {
+ return this.LogSource.Records
+ .Buffer(TimeSpan.FromMilliseconds(2000), 2*1024, Scheduler)
+ .Where(batch => batch.Count > 0)
+ .Select(batch => new {
+ Filtered = batch.AsParallel().Where(record => this.filter(record)).ToArray(),
+ Progress = batch.Max(record => record.SourceStatus.Progress)
+ })
+ .ObserveOnDispatcher()
+ .Subscribe(x => {
+ this.Progress = x.Progress;
+
+ var batch = x.Filtered;
+
+ if (batch.Length > 0) {
+ foreach (var record in batch) {
+ var filteredIndex = Interlocked.Increment(ref this.count);
+ this.recordIndexByLocalIndex.TryAdd(filteredIndex, record.Index);
+ this.localIndexByRecordIndex.TryAdd(record.Index, filteredIndex);
+ }
+
+ this.Count += batch.Length;
+ this.OnCollectionReset();
+ }
+ });
+ }
+
+ protected override async Task<Record> LoadRecordAsync(int index) {
+ int actualIndex;
+
+ Record record;
+
+ if (this.recordIndexByLocalIndex.TryGetValue(index, out actualIndex))
+ record = await this.LogSource.ReadRecordAsync(actualIndex, this.CancellationToken) ?? new Record();
+ else
+ record = new Record();
+
+ record.Index = index;
+
+ return record;
+ }
+ }
+}
11 LogWatch/Features/Records/GoToIndexEventArgs.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace LogWatch.Features.Records {
+ public sealed class GoToIndexEventArgs : EventArgs {
+ public GoToIndexEventArgs(int index) {
+ this.Index = index;
+ }
+
+ public int Index { get; private set; }
+ }
+}
21 LogWatch/Features/Records/IgnoreLinebreaksConverter.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+using System.Windows.Data;
+
+namespace LogWatch.Features.Records {
+ public class IgnoreLinebreaksConverter : IValueConverter {
+ private const string Sub = " \u200B";
+ private const string Lb = "\r\n";
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
+ var s = (string) value;
+ return string.IsNullOrEmpty(s) ? s : Regex.Replace(s, Lb, Sub);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
+ var s = (string) value;
+ return string.IsNullOrEmpty(s) ? s : Regex.Replace(s, Sub, Lb);
+ }
+ }
+}
21 LogWatch/Features/Records/LoggerToShortStringConverter.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using System.Linq;
+
+namespace LogWatch.Features.Records {
+ public class LoggerToShortStringConverter : IValueConverter{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
+ var logger = value as string;
+
+ if (string.IsNullOrEmpty(logger))
+ return null;
+
+ return logger.Split('.').LastOrDefault();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
+ throw new NotSupportedException();
+ }
+ }
+}
400 LogWatch/Features/Records/RecordCollection.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Linq;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using GalaSoft.MvvmLight;
+using LogWatch.Sources;
+
+namespace LogWatch.Features.Records {
+ public class RecordCollection :
+ ObservableObject,
+ IList<Record>,
+ IList,
+ IReadOnlyList<Record>,
+ INotifyCollectionChanged,
+ IDisposable {
+ private readonly ConcurrentDictionary<int, Record> cache;
+ private readonly ConcurrentQueue<int> cacheSlots;
+ private readonly NotifyCollectionChangedEventArgs collectionResetArgs;
+ private readonly ReplaySubject<int> loadingRecordCountSubject = new ReplaySubject<int>(1);
+ private readonly ILogSource logSource;
+
+ private readonly Subject<int> requestedResocords = new Subject<int>();
+ private readonly CancellationTokenSource tokenSource;
+ private readonly TaskScheduler uiScheduler;
+ private int count;
+
+ private bool isDisposed;
+ private bool isInitialized;
+ private bool isProcessingSavedData;
+ private IDisposable loadRecord;
+ private int loadingRecordCount;
+ private int progress;
+ private IDisposable updateState;
+
+ public RecordCollection(ILogSource logSource) {
+ this.CacheSize = 512;
+ this.logSource = logSource;
+ this.tokenSource = new CancellationTokenSource();
+ this.collectionResetArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ this.cache = new ConcurrentDictionary<int, Record>();
+ this.cacheSlots = new ConcurrentQueue<int>();
+ this.uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
+ this.Scheduler = System.Reactive.Concurrency.Scheduler.Default;
+ }
+
+ protected ILogSource LogSource {
+ get { return this.logSource; }
+ }
+
+ protected CancellationToken CancellationToken {
+ get { return this.tokenSource.Token; }
+ }
+
+ public int CacheSize { get; set; }
+
+ public int Progress {
+ get { return this.progress; }
+ set { this.Set(ref this.progress, value); }
+ }
+
+ public bool IsProcessingSavedData {
+ get { return this.isProcessingSavedData; }
+ set { this.Set(ref this.isProcessingSavedData, value); }
+ }
+
+ public IObservable<int> LoadingRecordCount {
+ get { return this.loadingRecordCountSubject; }
+ }
+
+ public IScheduler Scheduler { get; set; }
+
+ public void Dispose() {
+ if (this.isDisposed)
+ return;
+
+ this.isDisposed = true;
+ this.tokenSource.Cancel();
+ this.updateState.Dispose();
+ this.loadRecord.Dispose();
+
+ GC.SuppressFinalize(this);
+ }
+
+ public int Add(object value) {
+ throw new NotSupportedException();
+ }
+
+ public bool Contains(object value) {
+ return true;
+ }
+
+ void IList.Clear() {
+ throw new NotSupportedException();
+ }
+
+ public int IndexOf(object value) {
+ return this.IndexOf((Record) value);
+ }
+
+ public void Insert(int index, object value) {
+ throw new NotSupportedException();
+ }
+
+ public void Remove(object value) {
+ throw new NotSupportedException();
+ }
+
+ void IList.RemoveAt(int index) {
+ throw new NotSupportedException();
+ }
+
+ object IList.this[int index] {
+ get { return this[index]; }
+ set { throw new NotSupportedException(); }
+ }
+
+ bool IList.IsReadOnly {
+ get { return true; }
+ }
+
+ public bool IsFixedSize {
+ get { return false; }
+ }
+
+ public void CopyTo(Array array, int index) {
+ throw new NotSupportedException();
+ }
+
+ public int Count {
+ get { return this.count; }
+ set { this.Set(ref this.count, value); }
+ }
+
+ public object SyncRoot {
+ get { return this; }
+ }
+
+ public bool IsSynchronized {
+ get { return false; }
+ }
+
+ public IEnumerator<Record> GetEnumerator() {
+ return Enumerable.Empty<Record>().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() {
+ return this.GetEnumerator();
+ }
+
+ public void Add(Record item) {
+ throw new NotSupportedException();
+ }
+
+ void ICollection<Record>.Clear() {
+ throw new NotSupportedException();
+ }
+
+ public bool Contains(Record item) {
+ if (item == null)
+ return false;
+ return item.Index < this.Count;
+ }
+
+ public void CopyTo(Record[] array, int arrayIndex) {
+ throw new NotSupportedException();
+ }
+
+ public bool Remove(Record item) {
+ throw new NotSupportedException();
+ }
+
+ int ICollection<Record>.Count {
+ get { return this.Count; }
+ }
+
+ bool ICollection<Record>.IsReadOnly {
+ get { return true; }
+ }
+
+ public int IndexOf(Record item) {
+ if (item == null)
+ return -1;
+ return item.Index;
+ }
+
+ public void Insert(int index, Record item) {
+ throw new NotSupportedException();
+ }
+
+ void IList<Record>.RemoveAt(int index) {
+ throw new NotSupportedException();
+ }
+
+ public Record this[int index] {
+ get {
+ var isNew = false;
+
+ var record = this.cache.GetOrAdd(index, key => {
+ isNew = true;
+ return new Record {Index = key};
+ });
+
+ if (isNew) {
+ lock (this.cacheSlots)
+ this.cacheSlots.Enqueue(index);
+ this.RequestRecord(index);
+ }
+
+ return record;
+ }
+ set { throw new NotSupportedException(); }
+ }
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ public void Initialize() {
+ if (this.isInitialized)
+ throw new InvalidOperationException("Already initialized");
+
+ this.updateState = this.UpdateState();
+
+ this.loadRecord =
+ this.requestedResocords
+ .Buffer(TimeSpan.FromMilliseconds(300), this.Scheduler)
+ .Where(batch => batch.Count > 0)
+ .Subscribe(this.LoadRecordsAsync);
+