From 1728be2d63b4cff8becdfa155c269f751737278c Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Thu, 11 May 2023 16:14:48 +0200 Subject: [PATCH 01/15] Implementing an alternate way of finding editor handle for new Notepad * Adding needed interop definitions to enumerate child windows * If first attempt using FindWindowEx with notepad process MainWindowHandle does not find editor handle, the search is done by enumerating child window handles for the notepad process MainWindowHandle. --- .../Notepad/Interop/NotepadTextWriter.cs | 46 ++++++++++++++++++- .../Sinks/Notepad/Interop/User32.cs | 11 +++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 651d260..5618712 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -14,12 +14,14 @@ // #endregion +using Serilog.Debugging; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; -using Serilog.Debugging; namespace Serilog.Sinks.Notepad.Interop { @@ -120,6 +122,13 @@ private static IntPtr FindNotepadEditorHandle(IntPtr notepadWindowHandle) return richEditHandle; } + // Issue #59 - Alternate way of finding the RichEditD2DPT class: + if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var childRichEditHandle + && childRichEditHandle != IntPtr.Zero) + { + return childRichEditHandle; + } + return User32.FindWindowEx(notepadWindowHandle, IntPtr.Zero, "Edit", null); } @@ -130,5 +139,40 @@ private void EnsureNotDisposed() throw new ObjectDisposedException(GetType().Name); } } + + private static bool EnumWindow(IntPtr handle, IntPtr pointer) + { + GCHandle gch = GCHandle.FromIntPtr(pointer); + List list = gch.Target as List; + if (list == null) + { + throw new InvalidCastException("GCHandle Target could not be cast as List"); + } + + // We only want windows of class RichEditD2DPT. + if (User32.FindWindowEx(handle, IntPtr.Zero, "RichEditD2DPT", null) != IntPtr.Zero) + { + list.Add(handle); + } + + return true; + } + + private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHandle) + { + List result = new List(); + GCHandle listHandle = GCHandle.Alloc(result); + try + { + User32.Win32Callback childProc = new User32.Win32Callback(EnumWindow); + User32.EnumChildWindows(notepadWindowHandle, childProc, GCHandle.ToIntPtr(listHandle)); + } + finally + { + if (listHandle.IsAllocated) + listHandle.Free(); + } + return result.FirstOrDefault(); + } } } diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs index f417510..5ab2be0 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs @@ -35,5 +35,16 @@ internal class User32 [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + // Needed for EnumChildWindows for registering a call back function. + public delegate bool Win32Callback(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32.Dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumChildWindows(IntPtr parentHandle, Win32Callback callback, IntPtr lParam); } } From 65f53998dbfdea2c8f345a28f5ce618739532119 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Fri, 12 May 2023 14:17:20 +0200 Subject: [PATCH 02/15] Changes to handle changes in Notepad that allows multiple documents. --- .../Notepad/Interop/NotepadTextWriter.cs | 21 +++++++++++++------ .../Sinks/Notepad/Interop/User32.cs | 9 ++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 5618712..41cb29c 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -123,10 +123,10 @@ private static IntPtr FindNotepadEditorHandle(IntPtr notepadWindowHandle) } // Issue #59 - Alternate way of finding the RichEditD2DPT class: - if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var childRichEditHandle - && childRichEditHandle != IntPtr.Zero) + if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var richEditHandleFromChildren + && richEditHandleFromChildren != IntPtr.Zero) { - return childRichEditHandle; + return richEditHandleFromChildren; } return User32.FindWindowEx(notepadWindowHandle, IntPtr.Zero, "Edit", null); @@ -140,6 +140,13 @@ private void EnsureNotDisposed() } } + private static string GetClassNameFromWindow(IntPtr handle) + { + StringBuilder sb = new StringBuilder(256); + var ret = User32.GetClassName(handle, sb, sb.Capacity); + return ret != 0 ? sb.ToString() : string.Empty; + } + private static bool EnumWindow(IntPtr handle, IntPtr pointer) { GCHandle gch = GCHandle.FromIntPtr(pointer); @@ -149,10 +156,12 @@ private static bool EnumWindow(IntPtr handle, IntPtr pointer) throw new InvalidCastException("GCHandle Target could not be cast as List"); } - // We only want windows of class RichEditD2DPT. - if (User32.FindWindowEx(handle, IntPtr.Zero, "RichEditD2DPT", null) != IntPtr.Zero) + if (string.Equals(GetClassNameFromWindow(handle), "RichEditD2DPT", StringComparison.OrdinalIgnoreCase)) { list.Add(handle); + + // Stop enumerating - we found the one. + return false; } return true; @@ -160,7 +169,7 @@ private static bool EnumWindow(IntPtr handle, IntPtr pointer) private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHandle) { - List result = new List(); + List result = new List(1); GCHandle listHandle = GCHandle.Alloc(result); try { diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs index 5ab2be0..ad84f1a 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs @@ -37,14 +37,15 @@ internal class User32 public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - // Needed for EnumChildWindows for registering a call back function. public delegate bool Win32Callback(IntPtr hwnd, IntPtr lParam); - [DllImport("user32.Dll")] + [DllImport("user32.Dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumChildWindows(IntPtr parentHandle, Win32Callback callback, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount); + } } From 0084257f6ae46a5d43b44340c3de936df43b96d8 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Fri, 12 May 2023 15:33:51 +0200 Subject: [PATCH 03/15] In some situations, a notepad window opened after logging start would not receive log messages. --- sample/ConsoleDemo/Program.cs | 18 +++++++++++++----- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/sample/ConsoleDemo/Program.cs b/sample/ConsoleDemo/Program.cs index 42102d7..3c0b2a9 100644 --- a/sample/ConsoleDemo/Program.cs +++ b/sample/ConsoleDemo/Program.cs @@ -14,9 +14,9 @@ // #endregion +using Serilog; using System; using System.Threading; -using Serilog; namespace ConsoleDemo { @@ -33,15 +33,23 @@ private static void Main(string[] args) try { - Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); - Console.ReadLine(); + //Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); + //Console.ReadLine(); Console.WriteLine("Writing messages to the most recent Notepad you opened..."); Log.Debug("Getting started"); - Log.Information("Hello {Name} from thread {ThreadId}", Environment.GetEnvironmentVariable("USERNAME"), - Thread.CurrentThread.ManagedThreadId); + var startTime = DateTime.Now; + + while (DateTime.Now - startTime < TimeSpan.FromMinutes(1)) + { + + Log.Information("Hello {Name} from thread {ThreadId}", Environment.GetEnvironmentVariable("USERNAME"), + Thread.CurrentThread.ManagedThreadId); + + Thread.Sleep(1000); + } Log.Warning("No coins remain at position {@Position}", new { Lat = 25, Long = 134 }); diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 41cb29c..3e812c2 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -57,7 +57,10 @@ public override void Flush() // No instances of Notepad found... Nothing to do return; } + } + if (_currentNotepadEditorHandle == IntPtr.Zero) + { var notepadWindowHandle = currentNotepadProcess.MainWindowHandle; var notepadEditorHandle = FindNotepadEditorHandle(notepadWindowHandle); From ab9ac54dac0b74069dc1374051f39d49a8ab6d76 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Mon, 15 May 2023 13:50:45 +0100 Subject: [PATCH 04/15] Sometimes, the editor handle seems not correct. So if the text length does not change by sending text to it, it is cleared in order to reget it. --- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 3e812c2..54af0d0 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -85,7 +85,16 @@ public override void Flush() // Write the log message to Notepad User32.SendMessage(_currentNotepadEditorHandle, User32.EM_REPLACESEL, (IntPtr)1, message); - buffer.Clear(); + // Get how many characters are in the Notepad editor after putting in new text + var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + + // If no change in textLength, reset editor handle to try to find it again. + if (textLengthAfter == textLength) + _currentNotepadEditorHandle = IntPtr.Zero; + + // Otherwise, we clear the buffer + else + buffer.Clear(); } protected override void Dispose(bool disposing) From 1b5bec493a0a0e75d1d568689cb618921ae48221 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Thu, 11 May 2023 16:14:48 +0200 Subject: [PATCH 05/15] Implementing an alternate way of finding editor handle for new Notepad * Adding needed interop definitions to enumerate child windows * If first attempt using FindWindowEx with notepad process MainWindowHandle does not find editor handle, the search is done by enumerating child window handles for the notepad process MainWindowHandle. --- .../Notepad/Interop/NotepadTextWriter.cs | 46 ++++++++++++++++++- .../Sinks/Notepad/Interop/User32.cs | 11 +++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 651d260..5618712 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -14,12 +14,14 @@ // #endregion +using Serilog.Debugging; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; -using Serilog.Debugging; namespace Serilog.Sinks.Notepad.Interop { @@ -120,6 +122,13 @@ private static IntPtr FindNotepadEditorHandle(IntPtr notepadWindowHandle) return richEditHandle; } + // Issue #59 - Alternate way of finding the RichEditD2DPT class: + if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var childRichEditHandle + && childRichEditHandle != IntPtr.Zero) + { + return childRichEditHandle; + } + return User32.FindWindowEx(notepadWindowHandle, IntPtr.Zero, "Edit", null); } @@ -130,5 +139,40 @@ private void EnsureNotDisposed() throw new ObjectDisposedException(GetType().Name); } } + + private static bool EnumWindow(IntPtr handle, IntPtr pointer) + { + GCHandle gch = GCHandle.FromIntPtr(pointer); + List list = gch.Target as List; + if (list == null) + { + throw new InvalidCastException("GCHandle Target could not be cast as List"); + } + + // We only want windows of class RichEditD2DPT. + if (User32.FindWindowEx(handle, IntPtr.Zero, "RichEditD2DPT", null) != IntPtr.Zero) + { + list.Add(handle); + } + + return true; + } + + private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHandle) + { + List result = new List(); + GCHandle listHandle = GCHandle.Alloc(result); + try + { + User32.Win32Callback childProc = new User32.Win32Callback(EnumWindow); + User32.EnumChildWindows(notepadWindowHandle, childProc, GCHandle.ToIntPtr(listHandle)); + } + finally + { + if (listHandle.IsAllocated) + listHandle.Free(); + } + return result.FirstOrDefault(); + } } } diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs index f417510..5ab2be0 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs @@ -35,5 +35,16 @@ internal class User32 [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + // Needed for EnumChildWindows for registering a call back function. + public delegate bool Win32Callback(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32.Dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumChildWindows(IntPtr parentHandle, Win32Callback callback, IntPtr lParam); } } From 7647b4bb37e7462e919b32c8dd69cfd40eec495a Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Fri, 12 May 2023 14:17:20 +0200 Subject: [PATCH 06/15] Changes to handle changes in Notepad that allows multiple documents. --- .../Notepad/Interop/NotepadTextWriter.cs | 21 +++++++++++++------ .../Sinks/Notepad/Interop/User32.cs | 9 ++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 5618712..41cb29c 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -123,10 +123,10 @@ private static IntPtr FindNotepadEditorHandle(IntPtr notepadWindowHandle) } // Issue #59 - Alternate way of finding the RichEditD2DPT class: - if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var childRichEditHandle - && childRichEditHandle != IntPtr.Zero) + if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var richEditHandleFromChildren + && richEditHandleFromChildren != IntPtr.Zero) { - return childRichEditHandle; + return richEditHandleFromChildren; } return User32.FindWindowEx(notepadWindowHandle, IntPtr.Zero, "Edit", null); @@ -140,6 +140,13 @@ private void EnsureNotDisposed() } } + private static string GetClassNameFromWindow(IntPtr handle) + { + StringBuilder sb = new StringBuilder(256); + var ret = User32.GetClassName(handle, sb, sb.Capacity); + return ret != 0 ? sb.ToString() : string.Empty; + } + private static bool EnumWindow(IntPtr handle, IntPtr pointer) { GCHandle gch = GCHandle.FromIntPtr(pointer); @@ -149,10 +156,12 @@ private static bool EnumWindow(IntPtr handle, IntPtr pointer) throw new InvalidCastException("GCHandle Target could not be cast as List"); } - // We only want windows of class RichEditD2DPT. - if (User32.FindWindowEx(handle, IntPtr.Zero, "RichEditD2DPT", null) != IntPtr.Zero) + if (string.Equals(GetClassNameFromWindow(handle), "RichEditD2DPT", StringComparison.OrdinalIgnoreCase)) { list.Add(handle); + + // Stop enumerating - we found the one. + return false; } return true; @@ -160,7 +169,7 @@ private static bool EnumWindow(IntPtr handle, IntPtr pointer) private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHandle) { - List result = new List(); + List result = new List(1); GCHandle listHandle = GCHandle.Alloc(result); try { diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs index 5ab2be0..ad84f1a 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs @@ -37,14 +37,15 @@ internal class User32 public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - // Needed for EnumChildWindows for registering a call back function. public delegate bool Win32Callback(IntPtr hwnd, IntPtr lParam); - [DllImport("user32.Dll")] + [DllImport("user32.Dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumChildWindows(IntPtr parentHandle, Win32Callback callback, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount); + } } From 756b26952b4fc9795876e9c5e51a54536d4f6936 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Fri, 12 May 2023 15:33:51 +0200 Subject: [PATCH 07/15] In some situations, a notepad window opened after logging start would not receive log messages. --- sample/ConsoleDemo/Program.cs | 18 +++++++++++++----- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/sample/ConsoleDemo/Program.cs b/sample/ConsoleDemo/Program.cs index 42102d7..3c0b2a9 100644 --- a/sample/ConsoleDemo/Program.cs +++ b/sample/ConsoleDemo/Program.cs @@ -14,9 +14,9 @@ // #endregion +using Serilog; using System; using System.Threading; -using Serilog; namespace ConsoleDemo { @@ -33,15 +33,23 @@ private static void Main(string[] args) try { - Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); - Console.ReadLine(); + //Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); + //Console.ReadLine(); Console.WriteLine("Writing messages to the most recent Notepad you opened..."); Log.Debug("Getting started"); - Log.Information("Hello {Name} from thread {ThreadId}", Environment.GetEnvironmentVariable("USERNAME"), - Thread.CurrentThread.ManagedThreadId); + var startTime = DateTime.Now; + + while (DateTime.Now - startTime < TimeSpan.FromMinutes(1)) + { + + Log.Information("Hello {Name} from thread {ThreadId}", Environment.GetEnvironmentVariable("USERNAME"), + Thread.CurrentThread.ManagedThreadId); + + Thread.Sleep(1000); + } Log.Warning("No coins remain at position {@Position}", new { Lat = 25, Long = 134 }); diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 41cb29c..3e812c2 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -57,7 +57,10 @@ public override void Flush() // No instances of Notepad found... Nothing to do return; } + } + if (_currentNotepadEditorHandle == IntPtr.Zero) + { var notepadWindowHandle = currentNotepadProcess.MainWindowHandle; var notepadEditorHandle = FindNotepadEditorHandle(notepadWindowHandle); From 7802dece19af5cbfc316a6eec733aa66fca94ae6 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Mon, 15 May 2023 13:50:45 +0100 Subject: [PATCH 08/15] Sometimes, the editor handle seems not correct. So if the text length does not change by sending text to it, it is cleared in order to reget it. --- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 3e812c2..54af0d0 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -85,7 +85,16 @@ public override void Flush() // Write the log message to Notepad User32.SendMessage(_currentNotepadEditorHandle, User32.EM_REPLACESEL, (IntPtr)1, message); - buffer.Clear(); + // Get how many characters are in the Notepad editor after putting in new text + var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + + // If no change in textLength, reset editor handle to try to find it again. + if (textLengthAfter == textLength) + _currentNotepadEditorHandle = IntPtr.Zero; + + // Otherwise, we clear the buffer + else + buffer.Clear(); } protected override void Dispose(bool disposing) From 6db95e1789c01c5fc57f4ac6ce4145c12d0f39f3 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Thu, 11 May 2023 16:14:48 +0200 Subject: [PATCH 09/15] Implementing an alternate way of finding editor handle for new Notepad * Adding needed interop definitions to enumerate child windows * If first attempt using FindWindowEx with notepad process MainWindowHandle does not find editor handle, the search is done by enumerating child window handles for the notepad process MainWindowHandle. --- sample/ConsoleDemo/Program.cs | 18 +++-- .../Notepad/Interop/NotepadTextWriter.cs | 69 ++++++++++++++++++- .../Sinks/Notepad/Interop/User32.cs | 12 ++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/sample/ConsoleDemo/Program.cs b/sample/ConsoleDemo/Program.cs index 42102d7..3c0b2a9 100644 --- a/sample/ConsoleDemo/Program.cs +++ b/sample/ConsoleDemo/Program.cs @@ -14,9 +14,9 @@ // #endregion +using Serilog; using System; using System.Threading; -using Serilog; namespace ConsoleDemo { @@ -33,15 +33,23 @@ private static void Main(string[] args) try { - Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); - Console.ReadLine(); + //Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); + //Console.ReadLine(); Console.WriteLine("Writing messages to the most recent Notepad you opened..."); Log.Debug("Getting started"); - Log.Information("Hello {Name} from thread {ThreadId}", Environment.GetEnvironmentVariable("USERNAME"), - Thread.CurrentThread.ManagedThreadId); + var startTime = DateTime.Now; + + while (DateTime.Now - startTime < TimeSpan.FromMinutes(1)) + { + + Log.Information("Hello {Name} from thread {ThreadId}", Environment.GetEnvironmentVariable("USERNAME"), + Thread.CurrentThread.ManagedThreadId); + + Thread.Sleep(1000); + } Log.Warning("No coins remain at position {@Position}", new { Lat = 25, Long = 134 }); diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 651d260..54af0d0 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -14,12 +14,14 @@ // #endregion +using Serilog.Debugging; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; -using Serilog.Debugging; namespace Serilog.Sinks.Notepad.Interop { @@ -55,7 +57,10 @@ public override void Flush() // No instances of Notepad found... Nothing to do return; } + } + if (_currentNotepadEditorHandle == IntPtr.Zero) + { var notepadWindowHandle = currentNotepadProcess.MainWindowHandle; var notepadEditorHandle = FindNotepadEditorHandle(notepadWindowHandle); @@ -80,7 +85,16 @@ public override void Flush() // Write the log message to Notepad User32.SendMessage(_currentNotepadEditorHandle, User32.EM_REPLACESEL, (IntPtr)1, message); - buffer.Clear(); + // Get how many characters are in the Notepad editor after putting in new text + var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + + // If no change in textLength, reset editor handle to try to find it again. + if (textLengthAfter == textLength) + _currentNotepadEditorHandle = IntPtr.Zero; + + // Otherwise, we clear the buffer + else + buffer.Clear(); } protected override void Dispose(bool disposing) @@ -120,6 +134,13 @@ private static IntPtr FindNotepadEditorHandle(IntPtr notepadWindowHandle) return richEditHandle; } + // Issue #59 - Alternate way of finding the RichEditD2DPT class: + if (FindEditorHandleThroughChildWindows(notepadWindowHandle) is var richEditHandleFromChildren + && richEditHandleFromChildren != IntPtr.Zero) + { + return richEditHandleFromChildren; + } + return User32.FindWindowEx(notepadWindowHandle, IntPtr.Zero, "Edit", null); } @@ -130,5 +151,49 @@ private void EnsureNotDisposed() throw new ObjectDisposedException(GetType().Name); } } + + private static string GetClassNameFromWindow(IntPtr handle) + { + StringBuilder sb = new StringBuilder(256); + var ret = User32.GetClassName(handle, sb, sb.Capacity); + return ret != 0 ? sb.ToString() : string.Empty; + } + + private static bool EnumWindow(IntPtr handle, IntPtr pointer) + { + GCHandle gch = GCHandle.FromIntPtr(pointer); + List list = gch.Target as List; + if (list == null) + { + throw new InvalidCastException("GCHandle Target could not be cast as List"); + } + + if (string.Equals(GetClassNameFromWindow(handle), "RichEditD2DPT", StringComparison.OrdinalIgnoreCase)) + { + list.Add(handle); + + // Stop enumerating - we found the one. + return false; + } + + return true; + } + + private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHandle) + { + List result = new List(1); + GCHandle listHandle = GCHandle.Alloc(result); + try + { + User32.Win32Callback childProc = new User32.Win32Callback(EnumWindow); + User32.EnumChildWindows(notepadWindowHandle, childProc, GCHandle.ToIntPtr(listHandle)); + } + finally + { + if (listHandle.IsAllocated) + listHandle.Free(); + } + return result.FirstOrDefault(); + } } } diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs index f417510..ad84f1a 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/User32.cs @@ -35,5 +35,17 @@ internal class User32 [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + + // Needed for EnumChildWindows for registering a call back function. + public delegate bool Win32Callback(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32.Dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumChildWindows(IntPtr parentHandle, Win32Callback callback, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount); + } } From 7ab2698963496dda24a0e89208e3652f203d875c Mon Sep 17 00:00:00 2001 From: "C. Augusto Proiete" Date: Sun, 4 Jun 2023 03:13:00 -0300 Subject: [PATCH 10/15] PR rebase & minor tweaks --- sample/ConsoleDemo/Program.cs | 4 +-- .../Notepad/Interop/NotepadTextWriter.cs | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/sample/ConsoleDemo/Program.cs b/sample/ConsoleDemo/Program.cs index 3c0b2a9..70379a0 100644 --- a/sample/ConsoleDemo/Program.cs +++ b/sample/ConsoleDemo/Program.cs @@ -33,8 +33,8 @@ private static void Main(string[] args) try { - //Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); - //Console.ReadLine(); + Console.WriteLine("Open a `notepad.exe` instance and press to continue..."); + Console.ReadLine(); Console.WriteLine("Writing messages to the most recent Notepad you opened..."); diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 54af0d0..99280ab 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -74,10 +74,10 @@ public override void Flush() } // Get how many characters are in the Notepad editor already - var textLength = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + var textLengthBefore = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); // Set the caret position to the end of the text - User32.SendMessage(_currentNotepadEditorHandle, User32.EM_SETSEL, (IntPtr)textLength, (IntPtr)textLength); + User32.SendMessage(_currentNotepadEditorHandle, User32.EM_SETSEL, (IntPtr)textLengthBefore, (IntPtr)textLengthBefore); var buffer = base.GetStringBuilder(); var message = buffer.ToString(); @@ -88,13 +88,17 @@ public override void Flush() // Get how many characters are in the Notepad editor after putting in new text var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); - // If no change in textLength, reset editor handle to try to find it again. - if (textLengthAfter == textLength) + // If no change in the text length, reset editor handle to try to find it again. + if (textLengthAfter == textLengthBefore) + { _currentNotepadEditorHandle = IntPtr.Zero; - - // Otherwise, we clear the buffer + } else + { + // Otherwise, we clear the buffer + buffer.Clear(); + } } protected override void Dispose(bool disposing) @@ -161,9 +165,10 @@ private static string GetClassNameFromWindow(IntPtr handle) private static bool EnumWindow(IntPtr handle, IntPtr pointer) { - GCHandle gch = GCHandle.FromIntPtr(pointer); - List list = gch.Target as List; - if (list == null) + var gch = GCHandle.FromIntPtr(pointer); + var list = gch.Target as List; + + if (list is null) { throw new InvalidCastException("GCHandle Target could not be cast as List"); } @@ -181,8 +186,9 @@ private static bool EnumWindow(IntPtr handle, IntPtr pointer) private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHandle) { - List result = new List(1); - GCHandle listHandle = GCHandle.Alloc(result); + var result = new List(1); + var listHandle = GCHandle.Alloc(result); + try { User32.Win32Callback childProc = new User32.Win32Callback(EnumWindow); @@ -191,8 +197,11 @@ private static IntPtr FindEditorHandleThroughChildWindows(IntPtr notepadWindowHa finally { if (listHandle.IsAllocated) + { listHandle.Free(); + } } + return result.FirstOrDefault(); } } From c28d1f3f88e2bb95d3792c479ec4c298aa7ba438 Mon Sep 17 00:00:00 2001 From: "C. Augusto Proiete" Date: Sun, 4 Jun 2023 03:48:13 -0300 Subject: [PATCH 11/15] Add retry policy (3 attempts before giving up) --- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 99280ab..e9456e0 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -44,6 +44,9 @@ public override void Flush() base.Flush(); + var attempts = 0; + + TryWriteToNotepad: var currentNotepadProcess = _currentNotepadProcess; var targetNotepadProcess = _notepadProcessFinderFunc(); @@ -92,13 +95,16 @@ public override void Flush() if (textLengthAfter == textLengthBefore) { _currentNotepadEditorHandle = IntPtr.Zero; - } - else - { - // Otherwise, we clear the buffer + attempts++; - buffer.Clear(); + // We try to write to Notepad 3 times before we give up and discard the buffer + if (attempts < 3) + { + goto TryWriteToNotepad; + } } + + buffer.Clear(); } protected override void Dispose(bool disposing) From e86d7d303d07934d42ee971571347341974d5e34 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Tue, 6 Jun 2023 09:14:41 +0200 Subject: [PATCH 12/15] Rewriting logic to avoid Goto statement. The buffer is retrieved using base.GetStringBuilder - which just returns the StringBuilder _sb from the base TextWriter class. And this one may or may not have created the StringBuilder instance - depending on which constructor has been called. But it does not destroy it. --- .../Notepad/Interop/NotepadTextWriter.cs | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index c257eb6..35d329b 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -45,66 +45,66 @@ public override void Flush() base.Flush(); var attempts = 0; + var succeeded = false; + StringBuilder buffer = null; - TryWriteToNotepad: - var currentNotepadProcess = _currentNotepadProcess; - var targetNotepadProcess = _notepadProcessFinderFunc(); - - if (currentNotepadProcess is null || targetNotepadProcess is null || currentNotepadProcess.Id != targetNotepadProcess.Id) + do { - _currentNotepadProcess = currentNotepadProcess = targetNotepadProcess; - _currentNotepadEditorHandle = IntPtr.Zero; + var currentNotepadProcess = _currentNotepadProcess; + var targetNotepadProcess = _notepadProcessFinderFunc(); - if (currentNotepadProcess is null || currentNotepadProcess.HasExited) + if (currentNotepadProcess is null || targetNotepadProcess is null || currentNotepadProcess.Id != targetNotepadProcess.Id) { - // No instances of Notepad found... Nothing to do - return; + _currentNotepadProcess = currentNotepadProcess = targetNotepadProcess; + _currentNotepadEditorHandle = IntPtr.Zero; + + if (currentNotepadProcess is null || currentNotepadProcess.HasExited) + { + // No instances of Notepad found... Nothing to do + return; + } } - } - - if (_currentNotepadEditorHandle == IntPtr.Zero) - { - var notepadWindowHandle = currentNotepadProcess.MainWindowHandle; - var notepadEditorHandle = FindNotepadEditorHandle(notepadWindowHandle); - if (notepadEditorHandle == IntPtr.Zero) + if (_currentNotepadEditorHandle == IntPtr.Zero) { - SelfLog.WriteLine($"Unable to access a Notepad Editor on process {currentNotepadProcess.ProcessName} ({currentNotepadProcess.Id})"); - return; - } + var notepadWindowHandle = currentNotepadProcess.MainWindowHandle; - _currentNotepadEditorHandle = notepadEditorHandle; - } + var notepadEditorHandle = FindNotepadEditorHandle(notepadWindowHandle); + if (notepadEditorHandle == IntPtr.Zero) + { + SelfLog.WriteLine($"Unable to access a Notepad Editor on process {currentNotepadProcess.ProcessName} ({currentNotepadProcess.Id})"); + return; + } - // Get how many characters are in the Notepad editor already - var textLength = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + _currentNotepadEditorHandle = notepadEditorHandle; + } - // Set the caret position to the end of the text - User32.SendMessage(_currentNotepadEditorHandle, User32.EM_SETSEL, (IntPtr)textLength, (IntPtr)textLength); + // Get how many characters are in the Notepad editor already + var textLength = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); - var buffer = base.GetStringBuilder(); - var message = buffer.ToString(); + // Set the caret position to the end of the text + User32.SendMessage(_currentNotepadEditorHandle, User32.EM_SETSEL, (IntPtr)textLength, (IntPtr)textLength); - // Write the log message to Notepad - User32.SendMessage(_currentNotepadEditorHandle, User32.EM_REPLACESEL, (IntPtr)1, message); + buffer = base.GetStringBuilder(); + var message = buffer.ToString(); - // Get how many characters are in the Notepad editor after putting in new text - var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + // Write the log message to Notepad + User32.SendMessage(_currentNotepadEditorHandle, User32.EM_REPLACESEL, (IntPtr)1, message); - // If no change in the text length, reset editor handle to try to find it again. - if (textLengthAfter == textLengthBefore) - { - _currentNotepadEditorHandle = IntPtr.Zero; - attempts++; + // Get how many characters are in the Notepad editor after putting in new text + var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); - // We try to write to Notepad 3 times before we give up and discard the buffer - if (attempts < 3) + // If no change in the text length, reset editor handle to try to find it again. + if (textLengthAfter == textLength) { - goto TryWriteToNotepad; + _currentNotepadEditorHandle = IntPtr.Zero; + attempts++; } } + while (!succeeded && attempts < 3); - buffer.Clear(); + if (buffer != null) + buffer.Clear(); } protected override void Dispose(bool disposing) From c733e955142d7bfd7688c1c78724524b65db9bec Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Tue, 6 Jun 2023 10:47:40 +0200 Subject: [PATCH 13/15] Setting succeeded after trying to write. --- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index 35d329b..f9ce76e 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -94,8 +94,11 @@ public override void Flush() // Get how many characters are in the Notepad editor after putting in new text var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); + // Determine if the write succeeded. This will break the loop. + succeeded = textLengthAfter != textLength; + // If no change in the text length, reset editor handle to try to find it again. - if (textLengthAfter == textLength) + if (!succeeded) { _currentNotepadEditorHandle = IntPtr.Zero; attempts++; From 25a4a0300b442f5989803665152a021e1103e27c Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Wed, 7 Jun 2023 14:02:26 +0200 Subject: [PATCH 14/15] Change as per suggestion --- .../Sinks/Notepad/Interop/NotepadTextWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs index f9ce76e..87b5972 100644 --- a/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs +++ b/src/Serilog.Sinks.Notepad/Sinks/Notepad/Interop/NotepadTextWriter.cs @@ -95,7 +95,7 @@ public override void Flush() var textLengthAfter = User32.SendMessage(_currentNotepadEditorHandle, User32.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero); // Determine if the write succeeded. This will break the loop. - succeeded = textLengthAfter != textLength; + succeeded = textLengthAfter > textLength; // If no change in the text length, reset editor handle to try to find it again. if (!succeeded) From 5d43c96d494b0aaf9e249702a44295c65f8cc063 Mon Sep 17 00:00:00 2001 From: Benny Tordrup Date: Wed, 11 Sep 2024 14:11:19 +0200 Subject: [PATCH 15/15] Merge from master --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d46bd92..39a62ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,31 +33,31 @@ jobs: runs-on: ${{ matrix.job.os }} steps: - name: Setup netcoreapp3.1 - uses: actions/setup-dotnet@v3.2.0 + uses: actions/setup-dotnet@v4.0.1 with: dotnet-version: "3.1.425" - name: Setup net5.0 - uses: actions/setup-dotnet@v3.2.0 + uses: actions/setup-dotnet@v4.0.1 with: dotnet-version: "5.0.408" - name: Setup net6.0 - uses: actions/setup-dotnet@v3.2.0 + uses: actions/setup-dotnet@v4.0.1 with: dotnet-version: "6.0.403" - name: Setup net7.0 - uses: actions/setup-dotnet@v3.2.0 + uses: actions/setup-dotnet@v4.0.1 with: dotnet-version: "7.0.100" - name: Run dotnet --info run: dotnet --info - - uses: actions/checkout@v3.5.2 + - uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - name: Build run: ${{ matrix.job.build }} --verbosity=diagnostic --target=pack - name: Publish artifacts if: matrix.job.push && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4.3.6 with: if-no-files-found: warn name: package