diff --git a/Notepad.Extensions.Logging/NativeMethods.cs b/Notepad.Extensions.Logging/NativeMethods.cs new file mode 100644 index 0000000..873b5de --- /dev/null +++ b/Notepad.Extensions.Logging/NativeMethods.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Notepad.Extensions.Logging +{ + static class NativeMethods + { + [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string lpszWindow); + + public const int EM_REPLACESEL = 0x00C2; + + [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + public const int SCI_ADDTEXT = 2001; + + [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); + + public delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern bool EnumWindows(EnumWindowsDelegate lpEnumFunc, IntPtr lParam); + + [DllImport("User32.dll")] + public static extern int GetWindowText(IntPtr hWndParent, StringBuilder sb, int maxCount); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, int flAllocationType, int flProtect); + + public const int MEM_COMMIT = 0x00001000; + public const int MEM_RESERVE = 0x00002000; + public const int MEM_RELEASE = 0x8000; + public const int PAGE_READWRITE = 0x04; + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, int dwFreeType); + + [DllImport("User32.dll")] + public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern int WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesWritten); + } +} diff --git a/Notepad.Extensions.Logging/Notepad.Extensions.Logging.csproj b/Notepad.Extensions.Logging/Notepad.Extensions.Logging.csproj index b7eefed..427e3ed 100644 --- a/Notepad.Extensions.Logging/Notepad.Extensions.Logging.csproj +++ b/Notepad.Extensions.Logging/Notepad.Extensions.Logging.csproj @@ -10,6 +10,8 @@ git Apache-2.0 true + true + 1.0.1 diff --git a/Notepad.Extensions.Logging/NotepadLogger.cs b/Notepad.Extensions.Logging/NotepadLogger.cs index 228046b..bf17382 100644 --- a/Notepad.Extensions.Logging/NotepadLogger.cs +++ b/Notepad.Extensions.Logging/NotepadLogger.cs @@ -1,9 +1,14 @@ using System; +using System.Buffers; +using System.ComponentModel; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; +using static Notepad.Extensions.Logging.NativeMethods; -namespace Microsoft.Extensions.Logging +namespace Notepad.Extensions.Logging { class NotepadLogger : ILogger { @@ -88,41 +93,58 @@ namespace Microsoft.Extensions.Logging void WriteToNotepad(string message) { - IntPtr hwnd = FindNotepadWindow(); + var (kind, hwnd) = WindowFinder.FindNotepadWindow(); + switch (kind) + { + case WindowKind.Notepad: + SendMessage(hwnd, EM_REPLACESEL, (IntPtr)1, message); + break; - if (hwnd.Equals(IntPtr.Zero)) + case WindowKind.NotepadPlusPlus: + { + WriteToNotepadPlusPlus(hwnd, message); + break; + } + } + } + + unsafe static void WriteToNotepadPlusPlus(IntPtr hwnd, string message) + { + var dataLength = Encoding.UTF8.GetByteCount(message); + + // + // HERE BE DRAGONS + // We need to copy the message into Notepad++'s memory so that it can read it. + // Look away now, before its too late. + // + + /* unused thread ID */ _ = GetWindowThreadProcessId(hwnd, out var remoteProcessId); + using var remoteProcess = Process.GetProcessById(remoteProcessId); + var mem = VirtualAllocEx(remoteProcess.Handle, IntPtr.Zero, (IntPtr)dataLength, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (mem == IntPtr.Zero) { return; } - IntPtr edit = NativeMethods.FindWindowEx(hwnd, IntPtr.Zero, "EDIT", null); - NativeMethods.SendMessage(edit, NativeMethods.EM_REPLACESEL, (IntPtr)1, message); - } - - IntPtr FindNotepadWindow() - { - IntPtr hwnd; - - hwnd = NativeMethods.FindWindow(null, windowName); - if (hwnd.Equals(IntPtr.Zero)) + try { - hwnd = NativeMethods.FindWindow(null, changedWindowName); + var data = ArrayPool.Shared.Rent(dataLength); + try + { + var idx = Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0); + + WriteProcessMemory(remoteProcess.Handle, mem, data, (IntPtr)dataLength, out var bytesWritten); + SendMessage(hwnd, SCI_ADDTEXT, (IntPtr)dataLength, mem); + } + finally + { + ArrayPool.Shared.Return(data); + } + } + finally + { + VirtualFreeEx(remoteProcess.Handle, IntPtr.Zero, IntPtr.Zero, MEM_RELEASE); } - return hwnd; } } - - static class NativeMethods - { - [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); - - [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string lpszWindow); - - public const int EM_REPLACESEL = 0x00C2; - - [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); - } } diff --git a/Notepad.Extensions.Logging/NotepadLoggerProvider.cs b/Notepad.Extensions.Logging/NotepadLoggerProvider.cs index f291622..4c390df 100644 --- a/Notepad.Extensions.Logging/NotepadLoggerProvider.cs +++ b/Notepad.Extensions.Logging/NotepadLoggerProvider.cs @@ -1,9 +1,10 @@ using System; using System.Text; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.Logging +namespace Notepad.Extensions.Logging { [ProviderAlias("Notepad")] class NotepadLoggerProvider : ILoggerProvider diff --git a/Notepad.Extensions.Logging/NullDisposable.cs b/Notepad.Extensions.Logging/NullDisposable.cs index 4e3e782..a8b7db3 100644 --- a/Notepad.Extensions.Logging/NullDisposable.cs +++ b/Notepad.Extensions.Logging/NullDisposable.cs @@ -1,6 +1,6 @@ using System; -namespace Microsoft.Extensions.Logging +namespace Notepad.Extensions.Logging { class NullDisposable : IDisposable { diff --git a/Notepad.Extensions.Logging/WindowFinder.cs b/Notepad.Extensions.Logging/WindowFinder.cs new file mode 100644 index 0000000..b716ec9 --- /dev/null +++ b/Notepad.Extensions.Logging/WindowFinder.cs @@ -0,0 +1,89 @@ +using System; +using System.ComponentModel; +using System.Text; +using System.Text.RegularExpressions; + +namespace Notepad.Extensions.Logging +{ + static class WindowFinder + { + public static (WindowKind kind, IntPtr hwnd) FindNotepadWindow() + { + sb ??= new StringBuilder(4096); + + try + { + FindMainWindow(); + return (windowKind, handle); + } + finally + { + handle = IntPtr.Zero; + sb.Clear(); + windowKind = WindowKind.Invalid; + } + } + + static IntPtr FindMainWindow() + { + NativeMethods.EnumWindows(enumWindowsDelegate, IntPtr.Zero); + return handle; + } + + static NativeMethods.EnumWindowsDelegate enumWindowsDelegate = new NativeMethods.EnumWindowsDelegate(EnumWindowsCallback); + + static bool EnumWindowsCallback(IntPtr hWnd, IntPtr lParam) + { + var result = NativeMethods.GetWindowText(hWnd, sb, sb.Capacity); + if (result < 0) + { + 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; + } + } +} diff --git a/Notepad.Extensions.Logging/WindowKind.cs b/Notepad.Extensions.Logging/WindowKind.cs new file mode 100644 index 0000000..d79e383 --- /dev/null +++ b/Notepad.Extensions.Logging/WindowKind.cs @@ -0,0 +1,9 @@ +namespace Notepad.Extensions.Logging +{ + public enum WindowKind + { + Invalid, + Notepad, + NotepadPlusPlus + } +}