From 5fc16059524ba637c3ef2093c03d8dc0cec2b901 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Thu, 27 Aug 2026 23:56:16 +1200 Subject: [PATCH 1/2] fix: decide the console state instead of catching the throw PrintHeader stopped crashing headless startups in 6.0.1 by wrapping the width read in catch (IOException). The state it was catching is directly observable, so it is an if now. Console.WindowWidth needs one of the three std handles to be a console - .NET falls back stdout, stderr, stdin before giving up - so that, and not "is a console attached", is the predicate. Measured on the packed 6.0.2: no console all three redirected WindowWidth throws header 21 wide AllocConsole stdout is a console WindowWidth 120 header 120 wide AllocConsole, all handles piped all three redirected WindowWidth throws header 21 wide The third row is why GetConsoleWindow() is wrong here: a console is attached, and the width read still throws. ConsoleHelper carries the title side too, where the predicate is the opposite one - GetConsoleTitle takes no handle and needs the process to own a console - so NosCore, Injector and ParserInputGenerator can drop their own try { Console.Title = ... } catch blocks. Tested: builds clean, tests green (2 tests, both new). --- src/NosCore.Shared/Helpers/ConsoleHelper.cs | 38 +++++++++++++++++++ src/NosCore.Shared/I18N/Logger.cs | 13 ++----- src/NosCore.Shared/NosCore.Shared.csproj | 2 +- .../ConsoleHelperTests.cs | 29 ++++++++++++++ 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 src/NosCore.Shared/Helpers/ConsoleHelper.cs create mode 100644 test/NosCore.Shared.Tests/ConsoleHelperTests.cs diff --git a/src/NosCore.Shared/Helpers/ConsoleHelper.cs b/src/NosCore.Shared/Helpers/ConsoleHelper.cs new file mode 100644 index 0000000..e2dfe1d --- /dev/null +++ b/src/NosCore.Shared/Helpers/ConsoleHelper.cs @@ -0,0 +1,38 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System; +using System.Runtime.InteropServices; + +namespace NosCore.Shared.Helpers +{ + public static class ConsoleHelper + { + public static bool HasWindowSize => + !Console.IsOutputRedirected || !Console.IsErrorRedirected || !Console.IsInputRedirected; + + public static void SetTitle(string title) + { + if (OperatingSystem.IsWindows() && CanCarryTitle) + { + Console.Title = title; + } + } + + public static void AppendTitle(string suffix) + { + if (OperatingSystem.IsWindows() && CanCarryTitle) + { + Console.Title += suffix; + } + } + + private static bool CanCarryTitle => !Console.IsOutputRedirected && GetConsoleWindow() != IntPtr.Zero; + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + } +} diff --git a/src/NosCore.Shared/I18N/Logger.cs b/src/NosCore.Shared/I18N/Logger.cs index 080e65b..45f7f31 100644 --- a/src/NosCore.Shared/I18N/Logger.cs +++ b/src/NosCore.Shared/I18N/Logger.cs @@ -6,8 +6,8 @@ using System; using System.Globalization; -using System.IO; using Microsoft.Extensions.Configuration; +using NosCore.Shared.Helpers; using Serilog; namespace NosCore.Shared.I18N @@ -57,14 +57,9 @@ public static void PrintHeader(string text) private static int GetWindowWidth() { - try - { - return Console.WindowHeight > 0 ? Console.WindowWidth : HeadlessWindowWidth; - } - catch (IOException) - { - return HeadlessWindowWidth; - } + return ConsoleHelper.HasWindowSize && Console.WindowHeight > 0 + ? Console.WindowWidth + : HeadlessWindowWidth; } } } \ No newline at end of file diff --git a/src/NosCore.Shared/NosCore.Shared.csproj b/src/NosCore.Shared/NosCore.Shared.csproj index 42a8d25..45ac12b 100644 --- a/src/NosCore.Shared/NosCore.Shared.csproj +++ b/src/NosCore.Shared/NosCore.Shared.csproj @@ -12,7 +12,7 @@ https://github.com/NosCoreIO/NosCore.Dao.git nostale, noscore, nostale private server source, nostale emulator - 6.0.1 + 6.0.2 false NosCore's Shared Components diff --git a/test/NosCore.Shared.Tests/ConsoleHelperTests.cs b/test/NosCore.Shared.Tests/ConsoleHelperTests.cs new file mode 100644 index 0000000..a9a1080 --- /dev/null +++ b/test/NosCore.Shared.Tests/ConsoleHelperTests.cs @@ -0,0 +1,29 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NosCore.Shared.Helpers; +using NosCore.Shared.I18N; + +namespace NosCore.Shared.Tests +{ + [TestClass] + public class ConsoleHelperTests + { + [TestMethod] + public void TitleIsSkippedWhenNoConsoleCanCarryIt() + { + ConsoleHelper.SetTitle("NosCore"); + ConsoleHelper.AppendTitle(" - Port : 4000"); + } + + [TestMethod] + public void HeaderPrintsWhateverTheWindowSizeIs() + { + Logger.PrintHeader("NosCore"); + } + } +} From 35b663d7c133179d25a5478ba98107985ce6e31b Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 28 Aug 2026 00:36:49 +1200 Subject: [PATCH 2/2] fix: stdin cannot answer the window-size question HasWindowSize counted stdin, and a console input handle has no screen buffer, so Console.WindowWidth still threw for a process whose output is piped while stdin is on the console - Server.exe > log.txt 2>&1 from a shell. Measured against the packed 6.0.2: stdout+stderr piped, stdin on the console WindowWidth throws PrintHeader threw Console.WindowWidth reads the buffer through stdout, then stderr, then stdin, and only the first two can succeed, so the predicate is those two. Same case now falls back to the headless width and prints. The tests asserted nothing beyond "did not throw". A test process cannot choose whether it owns a console, so each case now asserts the half its own state can prove and stands aside for the other: the title is unchanged when nothing can carry it, and is "NosCore - Port : 4000" when something can; the banner is 20 wide with no readable size, and Console.WindowWidth - 1 with one. CanCarryTitle is public so a test can tell which half applies. Tested: 4 tests, the two that need an owned console skip under dotnet test. All four console states re-measured against the packed 6.0.2, all print. --- src/NosCore.Shared/Helpers/ConsoleHelper.cs | 8 +- .../ConsoleHelperTests.cs | 85 ++++++++++++++++++- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/NosCore.Shared/Helpers/ConsoleHelper.cs b/src/NosCore.Shared/Helpers/ConsoleHelper.cs index e2dfe1d..7c0007c 100644 --- a/src/NosCore.Shared/Helpers/ConsoleHelper.cs +++ b/src/NosCore.Shared/Helpers/ConsoleHelper.cs @@ -11,8 +11,10 @@ namespace NosCore.Shared.Helpers { public static class ConsoleHelper { - public static bool HasWindowSize => - !Console.IsOutputRedirected || !Console.IsErrorRedirected || !Console.IsInputRedirected; + // Console.WindowWidth reads the screen buffer through stdout, then stderr. Its third + // try, stdin, cannot answer: a console input handle has no screen buffer, so a process + // whose output is piped while stdin is still on the console has no width to read. + public static bool HasWindowSize => !Console.IsOutputRedirected || !Console.IsErrorRedirected; public static void SetTitle(string title) { @@ -30,7 +32,7 @@ public static void AppendTitle(string suffix) } } - private static bool CanCarryTitle => !Console.IsOutputRedirected && GetConsoleWindow() != IntPtr.Zero; + public static bool CanCarryTitle => !Console.IsOutputRedirected && GetConsoleWindow() != IntPtr.Zero; [DllImport("kernel32.dll")] private static extern IntPtr GetConsoleWindow(); diff --git a/test/NosCore.Shared.Tests/ConsoleHelperTests.cs b/test/NosCore.Shared.Tests/ConsoleHelperTests.cs index a9a1080..0eb2641 100644 --- a/test/NosCore.Shared.Tests/ConsoleHelperTests.cs +++ b/test/NosCore.Shared.Tests/ConsoleHelperTests.cs @@ -4,26 +4,105 @@ // |_|\__|\__/ |___/ \__/\__/|_|_\___| // ----------------------------------- +using System; +using System.IO; using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.Shared.Helpers; using NosCore.Shared.I18N; namespace NosCore.Shared.Tests { + // A test process cannot choose whether it owns a console, so each case asserts the + // half of the contract its own state can prove and stands aside for the other. [TestClass] public class ConsoleHelperTests { + private const int HeadlessSeparatorWidth = 20; + [TestMethod] - public void TitleIsSkippedWhenNoConsoleCanCarryIt() + public void TitleIsLeftAloneWhenNothingCanCarryIt() { + if (ConsoleHelper.CanCarryTitle) + { + Assert.Inconclusive("This process owns a console, so the title is expected to change."); + } + + var before = ReadTitle(); + ConsoleHelper.SetTitle("NosCore"); ConsoleHelper.AppendTitle(" - Port : 4000"); + + Assert.AreEqual(before, ReadTitle()); + } + + [TestMethod] + public void TitleIsSetWhenAConsoleCanCarryIt() + { + if (!ConsoleHelper.CanCarryTitle) + { + Assert.Inconclusive("This process owns no console to carry a title."); + } + + ConsoleHelper.SetTitle("NosCore"); + ConsoleHelper.AppendTitle(" - Port : 4000"); + + Assert.AreEqual("NosCore - Port : 4000", ReadTitle()); + } + + [TestMethod] + public void HeaderFallsBackToAFixedWidthWhenTheSizeIsUnreadable() + { + if (ConsoleHelper.HasWindowSize) + { + Assert.Inconclusive("This process can read a console width, so the fallback does not apply."); + } + + Assert.AreEqual(new string('=', HeadlessSeparatorWidth), PrintHeaderAndReadSeparator()); } [TestMethod] - public void HeaderPrintsWhateverTheWindowSizeIs() + public void HeaderUsesTheConsoleWidthWhenThereIsOne() { - Logger.PrintHeader("NosCore"); + if (!ConsoleHelper.HasWindowSize || Console.WindowHeight <= 0) + { + Assert.Inconclusive("This process cannot read a console width."); + } + + Assert.AreEqual(new string('=', Console.WindowWidth - 1), PrintHeaderAndReadSeparator()); + } + + private static string PrintHeaderAndReadSeparator() + { + var captured = new StringWriter(); + var original = Console.Out; + try + { + Console.SetOut(captured); + Logger.PrintHeader("NosCore"); + } + finally + { + Console.SetOut(original); + } + + return captured.ToString().Split('\n')[0].TrimEnd('\r'); + } + + private static string? ReadTitle() + { + if (!OperatingSystem.IsWindows()) + { + return null; + } + + try + { + return Console.Title; + } + catch (IOException) + { + return null; + } } } }