Compare commits

...

15 commits

Author SHA1 Message Date
181fec9e37 Fixed a memory leak in Scintilla injection 2020-06-21 14:20:59 +10:00
Yaakov
6b2d78a3c8
Update README.md 2020-05-24 22:34:49 +10:00
Yaakov
74b9d0bd1c
Merge pull request #6 from 4lexKislitsyn/dev/notepad2
Added support of notepad2.
2020-05-24 15:17:47 +10:00
Alexander Kislitsyn
690e6bd4c3 Added support of notepad2. 2020-05-23 18:31:44 +03:00
1116c4b54c Introduce code analysis, clean up code to comply 2020-05-23 23:55:51 +10:00
Yaakov
895ef8c01e
Merge pull request #5 from 4lexKislitsyn/master
Added options to provider
2020-05-23 23:45:56 +10:00
Alexander Kislitsyn
f1eafc15db Moved window name to IWindowFinder method parameter from property. 2020-05-23 14:54:13 +03:00
Alexander Kislitsyn
bf96d0622a Removed private keyword. 2020-05-23 13:07:49 +03:00
Alexander Kislitsyn
d2e66538e0 Added support of window name from options in WindowEnumerationState class. 2020-05-23 13:00:55 +03:00
Alexander Kislitsyn
6926e2a858 Merge non static changes. 2020-05-23 12:25:55 +03:00
c658b437b5 Remove statics and threadstatic so that we can pass options around 2020-05-23 19:05:09 +10:00
Alexander Kislitsyn
6a85b0bb0b Merge from source repository. 2020-05-22 08:40:59 +03:00
Alexander Kislitsyn
7b4969ce0d Removed string builder from logger. Added support of IOptions. 2020-05-22 08:19:42 +03:00
Alexander Kislitsyn
ed95e98a27 Removed default value and added exception. 2020-05-21 18:52:06 +03:00
Alexander Kislitsyn
a2a2451a6b Added options to configure window name to log into. Change window search mechanism. 2020-05-21 18:42:55 +03:00
17 changed files with 463 additions and 115 deletions

139
.editorconfig Normal file
View file

@ -0,0 +1,139 @@
[*.cs]
# SA1200: Using directives should be placed correctly
dotnet_diagnostic.SA1200.severity = none
# SA1600: Elements should be documented
dotnet_diagnostic.SA1600.severity = none
# SA1400: Access modifier should be declared
dotnet_diagnostic.SA1400.severity = none
# SA1633: File should have header
dotnet_diagnostic.SA1633.severity = none
# SA1101: Prefix local calls with this
dotnet_diagnostic.SA1101.severity = none
# SA1201: Elements should appear in the correct order
dotnet_diagnostic.SA1201.severity = none
# SA1204: Static elements should appear before instance elements
dotnet_diagnostic.SA1204.severity = none
# SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = none
# CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = none
# SA0001: XML comment analysis is disabled due to project configuration
dotnet_diagnostic.SA0001.severity = none
# WTG1001: Do not use the 'private' keyword.
dotnet_diagnostic.WTG1001.severity = warning
# WTG1002: Use the 'var' keyword instead of an explicit type where possible.
dotnet_diagnostic.WTG1002.severity = warning
# WTG1003: Do not leave whitespace on the end of the line.
dotnet_diagnostic.WTG1003.severity = warning
# WTG1004: Indent with tabs rather than spaces.
dotnet_diagnostic.WTG1004.severity = none
# WTG1005: Use consistent line endings.
dotnet_diagnostic.WTG1005.severity = warning
# WTG1006: Do not use the 'internal' keyword for non-nested type definitions.
dotnet_diagnostic.WTG1006.severity = warning
# WTG1007: Do not compare bool to a constant value.
dotnet_diagnostic.WTG1007.severity = warning
# WTG1008: Do not compare bool to a constant value in an expression.
dotnet_diagnostic.WTG1008.severity = warning
# WTG1009: Using directives must be ordered by kind.
dotnet_diagnostic.WTG1009.severity = warning
# WTG1010: Use the 'var' keyword instead of an explicit type where possible.
dotnet_diagnostic.WTG1010.severity = warning
# WTG1011: Use the 'var' keyword once when deconstructing an object.
dotnet_diagnostic.WTG1011.severity = warning
# WTG1012: Use the 'var' keyword instead of an explicit type where possible.
dotnet_diagnostic.WTG1012.severity = warning
# WTG1013: Don't use tuple types in public interfaces.
dotnet_diagnostic.WTG1013.severity = warning
# WTG1014: Don't nest conditional operators.
dotnet_diagnostic.WTG1014.severity = warning
# WTG1015: Conditional operators should not have multiline values.
dotnet_diagnostic.WTG1015.severity = warning
# WTG1016: Avoid the discard-coalesce-throw pattern.
dotnet_diagnostic.WTG1016.severity = warning
# WTG1017: Don't define variables that could be confused with discards.
dotnet_diagnostic.WTG1017.severity = warning
# WTG2001: Do not use ConfigureAwait from an async void method.
dotnet_diagnostic.WTG2001.severity = warning
# WTG2002: Avoid conditional compilation based on DEBUG.
dotnet_diagnostic.WTG2002.severity = warning
# WTG2003: Flags enums should specify explicit values.
dotnet_diagnostic.WTG2003.severity = warning
# WTG2004: This project does not use Code Contracts.
dotnet_diagnostic.WTG2004.severity = warning
# WTG2005: Use Correct Emit Overload
dotnet_diagnostic.WTG2005.severity = warning
# WTG2006: Do not pass the Compiled option into static methods on Regex.
dotnet_diagnostic.WTG2006.severity = warning
# WTG3001: Remove orphaned suppressions.
dotnet_diagnostic.WTG3001.severity = warning
# WTG3002: Prefer direct member access over linq.
dotnet_diagnostic.WTG3002.severity = warning
# WTG3003: Prefer direct member access over linq in an expression.
dotnet_diagnostic.WTG3003.severity = warning
# WTG3004: Prefer Array.Empty<T>() over creating a new empty array.
dotnet_diagnostic.WTG3004.severity = warning
# WTG3005: Don't call ToString() on a string.
dotnet_diagnostic.WTG3005.severity = warning
# WTG3006: Prefer nameof over calling ToString on an enum literal.
dotnet_diagnostic.WTG3006.severity = warning
# WTG3007: Overrides should not simply call base.
dotnet_diagnostic.WTG3007.severity = warning
# WTG3008: Do not compare value types with null.
dotnet_diagnostic.WTG3008.severity = warning
# WTG3009: Prefer Task.CompletedTask when applicable.
dotnet_diagnostic.WTG3009.severity = warning
# WTG3010: Don't await a trivially completed task.
dotnet_diagnostic.WTG3010.severity = warning
# WTG3101: Do not nest regions.
dotnet_diagnostic.WTG3101.severity = warning
# WTG3102: Regions should not split structures.
dotnet_diagnostic.WTG3102.severity = warning
# WTG3103: Conditional compilation directives should not split structures.
dotnet_diagnostic.WTG3103.severity = warning

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
@ -6,8 +6,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.4" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="WTG.Analyzers" Version="2.10.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -6,13 +6,13 @@ namespace Notepad.Extensions.Logging.FunctionalTest
{ {
class Program class Program
{ {
static void Main(string[] args) static void Main()
{ {
var sc = new ServiceCollection(); var sc = new ServiceCollection();
sc.AddLogging(lb => sc.AddLogging(lb =>
{ {
lb.AddConsole(); lb.AddConsole();
lb.AddNotepad(); lb.AddNotepad(o => o.WindowName = "mylog - Notepad++");
}); });
var sp = sc.BuildServiceProvider(); var sp = sc.BuildServiceProvider();
@ -29,7 +29,7 @@ namespace Notepad.Extensions.Logging.FunctionalTest
{ {
throw new InvalidOperationException("Wheeeeeeee"); throw new InvalidOperationException("Wheeeeeeee");
} }
catch (Exception ex) catch (InvalidOperationException ex)
{ {
return ex; return ex;
} }

View file

@ -3,9 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105 VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notepad.Extensions.Logging", "Notepad.Extensions.Logging\Notepad.Extensions.Logging.csproj", "{859E3EAB-04EB-42EE-86B2-A6FE90D71731}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Notepad.Extensions.Logging", "Notepad.Extensions.Logging\Notepad.Extensions.Logging.csproj", "{859E3EAB-04EB-42EE-86B2-A6FE90D71731}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notepad.Extensions.Logging.FunctionalTest", "Notepad.Extensions.Logging.FunctionalTest\Notepad.Extensions.Logging.FunctionalTest.csproj", "{82E1E9EF-9284-4E7B-B350-0318F9B7B411}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Notepad.Extensions.Logging.FunctionalTest", "Notepad.Extensions.Logging.FunctionalTest\Notepad.Extensions.Logging.FunctionalTest.csproj", "{82E1E9EF-9284-4E7B-B350-0318F9B7B411}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1B0EB23C-7148-453B-BC03-AD132DF7E13B}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -0,0 +1,7 @@
namespace Notepad.Extensions.Logging
{
interface IWindowFinder
{
WindowInfo FindNotepadWindow(string windowName);
}
}

View file

@ -1,4 +1,8 @@
using Notepad.Extensions.Logging; using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Configuration;
using Notepad.Extensions.Logging;
namespace Microsoft.Extensions.Logging namespace Microsoft.Extensions.Logging
{ {
@ -6,7 +10,32 @@ namespace Microsoft.Extensions.Logging
{ {
public static ILoggingBuilder AddNotepad(this ILoggingBuilder builder) public static ILoggingBuilder AddNotepad(this ILoggingBuilder builder)
{ {
builder.AddProvider(NotepadLoggerProvider.Instance); if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddConfiguration();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, NotepadLoggerProvider>());
LoggerProviderOptions.RegisterProviderOptions<NotepadLoggerOptions, NotepadLoggerProvider>(builder.Services);
return builder;
}
public static ILoggingBuilder AddNotepad(this ILoggingBuilder builder, Action<NotepadLoggerOptions> configure)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure is null)
{
throw new ArgumentNullException(nameof(configure));
}
AddNotepad(builder);
builder.Services.Configure(configure);
return builder; return builder;
} }
} }

View file

@ -22,12 +22,12 @@ namespace Notepad.Extensions.Logging
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
public delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam); public delegate bool EnumWindowsDelegate(IntPtr hWnd, object lParam);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsDelegate lpEnumFunc, IntPtr lParam); public static extern bool EnumWindows(EnumWindowsDelegate lpEnumFunc, object lParam);
[DllImport("User32.dll")] [DllImport("User32.dll", CharSet = CharSet.Unicode)]
public static extern int GetWindowText(IntPtr hWndParent, StringBuilder sb, int maxCount); public static extern int GetWindowText(IntPtr hWndParent, StringBuilder sb, int maxCount);
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]

View file

@ -11,12 +11,29 @@
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.1</Version> <Version>1.0.3</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.4" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="3.1.4" /> <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="3.1.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="WTG.Analyzers" Version="2.10.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -12,14 +11,18 @@ namespace Notepad.Extensions.Logging
{ {
class NotepadLogger : ILogger class NotepadLogger : ILogger
{ {
public NotepadLogger(ObjectPool<StringBuilder> stringBuilderPool, string categoryName) public NotepadLogger(ObjectPool<StringBuilder> stringBuilderPool, IWindowFinder windowFinder, string categoryName, string windowName)
{ {
this.stringBuilderPool = stringBuilderPool; this.stringBuilderPool = stringBuilderPool ?? throw new ArgumentNullException(nameof(stringBuilderPool));
this.windowFinder = windowFinder ?? throw new ArgumentNullException(nameof(windowFinder));
this.categoryName = categoryName; this.categoryName = categoryName;
this.windowName = windowName;
} }
readonly ObjectPool<StringBuilder> stringBuilderPool; readonly ObjectPool<StringBuilder> stringBuilderPool;
readonly IWindowFinder windowFinder;
readonly string categoryName; readonly string categoryName;
readonly string windowName;
public IDisposable BeginScope<TState>(TState state) => NullDisposable.Instance; public IDisposable BeginScope<TState>(TState state) => NullDisposable.Instance;
@ -74,46 +77,42 @@ namespace Notepad.Extensions.Logging
{ {
stringBuilderPool.Return(builder); stringBuilderPool.Return(builder);
} }
static string GetLogLevelString(LogLevel logLevel) => logLevel switch
{
LogLevel.Trace => "trce",
LogLevel.Debug => "dbug",
LogLevel.Information => "info",
LogLevel.Warning => "warn",
LogLevel.Error => "fail",
LogLevel.Critical => "crit",
_ => throw new ArgumentOutOfRangeException(nameof(logLevel)),
};
} }
static string GetLogLevelString(LogLevel logLevel) => logLevel switch void WriteToNotepad(string message)
{ {
LogLevel.Trace => "trce", var info = windowFinder.FindNotepadWindow(windowName);
LogLevel.Debug => "dbug", switch (info.Kind)
LogLevel.Information => "info",
LogLevel.Warning => "warn",
LogLevel.Error => "fail",
LogLevel.Critical => "crit",
_ => throw new ArgumentOutOfRangeException(nameof(logLevel)),
};
static void WriteToNotepad(string message)
{
var (kind, hwnd) = WindowFinder.FindNotepadWindow();
switch (kind)
{ {
case WindowKind.Notepad: case WindowKind.Notepad:
SendMessage(hwnd, EM_REPLACESEL, (IntPtr)1, message); SendMessage(info.Handle, EM_REPLACESEL, (IntPtr)1, message);
break; break;
case WindowKind.Notepad2:
case WindowKind.NotepadPlusPlus: case WindowKind.NotepadPlusPlus:
{ WriteToNotepadPlusPlus(info.Handle, message);
WriteToNotepadPlusPlus(hwnd, message);
break; break;
}
} }
} }
unsafe static void WriteToNotepadPlusPlus(IntPtr hwnd, string message) static void WriteToNotepadPlusPlus(IntPtr hwnd, string message)
{ {
var dataLength = Encoding.UTF8.GetByteCount(message); var dataLength = Encoding.UTF8.GetByteCount(message);
//
// HERE BE DRAGONS // HERE BE DRAGONS
// We need to copy the message into Notepad++'s memory so that it can read it. // We need to copy the message into Notepad++'s memory so that it can read it.
// Look away now, before its too late. // Look away now, before its too late.
//
/* unused thread ID */ _ = GetWindowThreadProcessId(hwnd, out var remoteProcessId); /* unused thread ID */ _ = GetWindowThreadProcessId(hwnd, out var remoteProcessId);
using var remoteProcess = Process.GetProcessById(remoteProcessId); using var remoteProcess = Process.GetProcessById(remoteProcessId);
var mem = VirtualAllocEx(remoteProcess.Handle, IntPtr.Zero, (IntPtr)dataLength, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); var mem = VirtualAllocEx(remoteProcess.Handle, IntPtr.Zero, (IntPtr)dataLength, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
@ -129,7 +128,7 @@ namespace Notepad.Extensions.Logging
{ {
var idx = Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0); var idx = Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0);
WriteProcessMemory(remoteProcess.Handle, mem, data, (IntPtr)dataLength, out var bytesWritten); _ = WriteProcessMemory(remoteProcess.Handle, mem, data, (IntPtr)dataLength, out var bytesWritten);
SendMessage(hwnd, SCI_ADDTEXT, (IntPtr)dataLength, mem); SendMessage(hwnd, SCI_ADDTEXT, (IntPtr)dataLength, mem);
} }
finally finally
@ -139,7 +138,7 @@ namespace Notepad.Extensions.Logging
} }
finally finally
{ {
VirtualFreeEx(remoteProcess.Handle, IntPtr.Zero, IntPtr.Zero, MEM_RELEASE); VirtualFreeEx(remoteProcess.Handle, mem, IntPtr.Zero, MEM_RELEASE);
} }
} }
} }

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Extensions.Logging
{
public class NotepadLoggerOptions
{
/// <summary>
/// Name of window to search.
/// </summary>
public string WindowName { get; set; }
}
}

View file

@ -1,25 +1,52 @@
using System.Text; using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Notepad.Extensions.Logging namespace Notepad.Extensions.Logging
{ {
[ProviderAlias("Notepad")]
[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated via DI container")]
class NotepadLoggerProvider : ILoggerProvider class NotepadLoggerProvider : ILoggerProvider
{ {
public NotepadLoggerProvider(IOptionsMonitor<NotepadLoggerOptions> options)
: this()
{
// Filter would be applied on LoggerFactory level
optionsReloadToken = options.OnChange(ReloadLoggerOptions);
ReloadLoggerOptions(options.CurrentValue);
}
public NotepadLoggerProvider(NotepadLoggerOptions options)
: this()
{
this.options = options;
}
NotepadLoggerProvider() NotepadLoggerProvider()
{ {
var poolProvider = new DefaultObjectPoolProvider(); var poolProvider = new DefaultObjectPoolProvider();
stringBuilderPool = poolProvider.CreateStringBuilderPool(); stringBuilderPool = poolProvider.CreateStringBuilderPool();
windowFinder = new WindowFinder();
} }
public static ILoggerProvider Instance { get; } = new NotepadLoggerProvider();
readonly ObjectPool<StringBuilder> stringBuilderPool; readonly ObjectPool<StringBuilder> stringBuilderPool;
readonly IWindowFinder windowFinder;
readonly IDisposable optionsReloadToken;
NotepadLoggerOptions options;
public ILogger CreateLogger(string categoryName) => new NotepadLogger(stringBuilderPool, categoryName); public ILogger CreateLogger(string categoryName) => new NotepadLogger(stringBuilderPool, windowFinder, categoryName, options.WindowName);
public void Dispose() public void Dispose()
{ {
optionsReloadToken?.Dispose();
}
void ReloadLoggerOptions(NotepadLoggerOptions options)
{
this.options = options;
} }
} }
} }

View file

@ -0,0 +1,115 @@
using System;
using System.ComponentModel;
using System.Text;
using System.Text.RegularExpressions;
namespace Notepad.Extensions.Logging
{
class WindowEnumerationState
{
public WindowEnumerationState()
{
sb = new StringBuilder(capacity: 4096);
}
readonly StringBuilder sb;
public IntPtr Handle { get; private set; }
public WindowKind WindowKind { get; private set; }
public string WindowName { get; set; }
public void Reset()
{
Handle = default;
WindowKind = default;
WindowName = default;
sb.Clear();
}
public bool ExamineWindow(IntPtr hWnd)
{
var result = NativeMethods.GetWindowText(hWnd, sb, sb.Capacity);
if (result < 0)
{
throw new Win32Exception(result);
}
Handle = hWnd;
if (sb.Length > 0 && sb[0] == '*')
{
// Notepad and Notepad++ both mark dirty documents by adding a leading asterisk to the window name.
sb.Remove(0, 1);
}
if (sb.Length > 0 && sb[0] == ' ')
{
// Notepad2 added space after asterisk
sb.Remove(0, 1);
}
if (IsKnownNotepadWindow(sb.ToString()))
{
return false;
}
return true;
}
static Regex notepadPlusPlusRegex = new Regex(@"^new \d+ - Notepad\+\+$", RegexOptions.Compiled);
bool IsKnownNotepadWindow(string titleText)
{
if (!string.IsNullOrWhiteSpace(WindowName))
{
if (WindowName.Equals(titleText, StringComparison.Ordinal))
{
if (titleText.EndsWith(" - Notepad++", StringComparison.Ordinal))
{
WindowKind = WindowKind.NotepadPlusPlus;
}
else if (titleText.EndsWith(" - Notepad2", StringComparison.Ordinal))
{
WindowKind = WindowKind.Notepad2;
}
else
{
WindowKind = WindowKind.Notepad;
}
}
}
else if (titleText.Equals("Untitled - Notepad", StringComparison.Ordinal))
{
WindowKind = WindowKind.Notepad;
}
else if (titleText.Equals("Untitled - Notepad2", StringComparison.Ordinal))
{
WindowKind = WindowKind.Notepad2;
}
else if (notepadPlusPlusRegex.IsMatch(titleText))
{
WindowKind = WindowKind.NotepadPlusPlus;
}
Handle = FindInnerWindow(WindowKind);
return WindowKind != default;
}
IntPtr FindInnerWindow(WindowKind windowKind)
{
switch (windowKind)
{
case WindowKind.Notepad:
return NativeMethods.FindWindowEx(Handle, IntPtr.Zero, "EDIT", null);
case WindowKind.Notepad2:
case WindowKind.NotepadPlusPlus:
return NativeMethods.FindWindowEx(Handle, IntPtr.Zero, "Scintilla", null);
default:
return Handle;
}
}
}
}

View file

@ -0,0 +1,17 @@
using Microsoft.Extensions.ObjectPool;
namespace Notepad.Extensions.Logging
{
class WindowEnumerationStatePoolingPolicy : IPooledObjectPolicy<WindowEnumerationState>
{
public static IPooledObjectPolicy<WindowEnumerationState> Instance { get; } = new WindowEnumerationStatePoolingPolicy();
public WindowEnumerationState Create() => new WindowEnumerationState();
public bool Return(WindowEnumerationState obj)
{
obj.Reset();
return true;
}
}
}

View file

@ -1,89 +1,38 @@
using System; using System;
using System.ComponentModel; using Microsoft.Extensions.ObjectPool;
using System.Text;
using System.Text.RegularExpressions;
namespace Notepad.Extensions.Logging namespace Notepad.Extensions.Logging
{ {
static class WindowFinder class WindowFinder : IWindowFinder
{ {
public static (WindowKind kind, IntPtr hwnd) FindNotepadWindow() public WindowFinder()
{ {
sb ??= new StringBuilder(4096); statePool = new DefaultObjectPool<WindowEnumerationState>(WindowEnumerationStatePoolingPolicy.Instance);
}
readonly ObjectPool<WindowEnumerationState> statePool;
public WindowInfo FindNotepadWindow(string windowName)
{
var stateObject = statePool.Get();
try try
{ {
FindMainWindow(); stateObject.WindowName = windowName;
return (windowKind, handle); NativeMethods.EnumWindows(enumWindowsDelegate, stateObject);
return new WindowInfo(stateObject.WindowKind, stateObject.Handle);
} }
finally finally
{ {
handle = IntPtr.Zero; statePool.Return(stateObject);
sb.Clear();
windowKind = WindowKind.Invalid;
} }
} }
static IntPtr FindMainWindow()
{
NativeMethods.EnumWindows(enumWindowsDelegate, IntPtr.Zero);
return handle;
}
static NativeMethods.EnumWindowsDelegate enumWindowsDelegate = new NativeMethods.EnumWindowsDelegate(EnumWindowsCallback); static NativeMethods.EnumWindowsDelegate enumWindowsDelegate = new NativeMethods.EnumWindowsDelegate(EnumWindowsCallback);
static bool EnumWindowsCallback(IntPtr hWnd, IntPtr lParam) static bool EnumWindowsCallback(IntPtr hWnd, object lParam)
{ {
var result = NativeMethods.GetWindowText(hWnd, sb, sb.Capacity); var state = (WindowEnumerationState)lParam;
if (result < 0) return state.ExamineWindow(hWnd);
{
throw new Win32Exception(result);
}
WindowFinder.handle = hWnd;
if (sb.Length > 0 && sb[0] == '*')
{
// Notepad and Notepad++ both mark dirty documents by adding a leading asterisk to the window name.
sb.Remove(0, 1);
}
if (IsKnownNotepadWindow(sb.ToString()))
{
return false;
}
return true;
}
[ThreadStatic]
static IntPtr handle;
[ThreadStatic]
static WindowKind windowKind;
[ThreadStatic]
static StringBuilder sb;
static Regex notepadPlusPlusRegex = new Regex(@"^new \d+ - Notepad\+\+$", RegexOptions.Compiled);
static bool IsKnownNotepadWindow(string titleText)
{
switch (titleText)
{
case "Untitled - Notepad":
windowKind = WindowKind.Notepad;
handle = NativeMethods.FindWindowEx(handle, IntPtr.Zero, "EDIT", null);
return true;
}
if (notepadPlusPlusRegex.IsMatch(titleText))
{
windowKind = WindowKind.NotepadPlusPlus;
handle = NativeMethods.FindWindowEx(handle, IntPtr.Zero, "Scintilla", null);
return true;
}
return false;
} }
} }
} }

View file

@ -0,0 +1,17 @@
using System;
namespace Notepad.Extensions.Logging
{
struct WindowInfo
{
public WindowInfo(WindowKind kind, IntPtr handle)
{
Kind = kind;
Handle = handle;
}
public WindowKind Kind { get; }
public IntPtr Handle { get; }
}
}

View file

@ -1,9 +1,10 @@
namespace Notepad.Extensions.Logging namespace Notepad.Extensions.Logging
{ {
public enum WindowKind enum WindowKind
{ {
Invalid, Invalid,
Notepad, Notepad,
NotepadPlusPlus NotepadPlusPlus,
Notepad2,
} }
} }

View file

@ -23,7 +23,7 @@ public void ConfigureServices(IServiceCollection services)
} }
``` ```
3. Open a new Notepad window. 3. Open a new Notepad, Notepad++, or Notepad2 window.
4. Run your application. 4. Run your application.
## Source Material ## Source Material