diff --git a/Logger.cs b/Logger.cs index ecefe68..0daeaae 100644 --- a/Logger.cs +++ b/Logger.cs @@ -175,13 +175,40 @@ private static string FileLevel(LogLevel level) }; } + /// + /// Strips ANSI colour sequences. A package's own output is written for a terminal + /// and carries escape codes that nothing renders once they are in a log file. + /// + internal static string StripDecoration(string message) + => System.Text.RegularExpressions.Regex.Replace(message, @"\x1b\[[0-9;]*m", string.Empty); + + /// + /// Writes one stamped line per line of . + /// + /// + /// A multi-line message used to be written with a single stamp on the front, so only + /// its first line carried a timestamp and level and the rest landed in the log as bare + /// text. That is how a package's captured stdout ended up sitting unstamped between + /// two properly formatted lines. Blank lines are dropped: captured output is full of + /// them and they carry nothing. + /// private static void WriteToFile(LogLevel level, string message) { if (string.IsNullOrEmpty(LogFile)) return; - + try { - File.AppendAllText(LogFile, FormatLine(level, message, DateTime.Now) + Environment.NewLine); + var now = DateTime.Now; + var builder = new System.Text.StringBuilder(); + foreach (var line in StripDecoration(message).Split('\n')) + { + var text = line.TrimEnd('\r'); + if (string.IsNullOrWhiteSpace(text)) continue; + builder.Append(FormatLine(level, text, now)).Append(Environment.NewLine); + } + + if (builder.Length > 0) + File.AppendAllText(LogFile, builder.ToString()); } catch { @@ -189,6 +216,24 @@ private static void WriteToFile(LogLevel level, string message) } } + /// + /// Records output captured from a package's own process: stdout at INFO, stderr at + /// WARN, one stamped line each, tagged so a reader can tell the package's words from + /// BootstrapMate's own. The tag is uppercase in brackets at the start of the message, + /// matching [PROGRESS] and [SUCCESS], which the log viewer renders as a pill. + /// + public static void WriteCapturedOutput(string package, string output, bool isError = false) + { + if (string.IsNullOrWhiteSpace(output)) return; + var level = isError ? LogLevel.Warning : LogLevel.Info; + foreach (var line in StripDecoration(output).Split('\n')) + { + var text = line.TrimEnd('\r'); + if (string.IsNullOrWhiteSpace(text)) continue; + WriteToFile(level, $"[OUTPUT] {package}: {text}"); + } + } + private static void WriteToConsole(LogLevel level, string message) { // Skip console output in silent mode diff --git a/Program.cs b/Program.cs index f9c77fe..a5d1bbb 100644 --- a/Program.cs +++ b/Program.cs @@ -1231,6 +1231,18 @@ static bool VerifyInstallerSignature(string filePath, string type, JsonElement p return false; } + static string PackageLabel(JsonElement packageInfo) + { + if (packageInfo.ValueKind == JsonValueKind.Object && + packageInfo.TryGetProperty("name", out var nameProp) && + nameProp.ValueKind == JsonValueKind.String) + { + var name = nameProp.GetString(); + if (!string.IsNullOrWhiteSpace(name)) return name!; + } + return "package"; + } + static async Task RunPowerShellScript(string scriptPath, JsonElement packageInfo) { var args = GetArguments(packageInfo); @@ -1267,7 +1279,7 @@ static async Task RunPowerShellScript(string scriptPath, JsonElement packageInfo string output = await process.StandardOutput.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(output)) { - WriteLog($"PowerShell output: {output}"); + Logger.WriteCapturedOutput(PackageLabel(packageInfo), output); } } @@ -1276,7 +1288,7 @@ static async Task RunPowerShellScript(string scriptPath, JsonElement packageInfo string error = await process.StandardError.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(error)) { - WriteLog($"PowerShell error: {error}"); + Logger.WriteCapturedOutput(PackageLabel(packageInfo), error, isError: true); } } @@ -1571,7 +1583,7 @@ static async Task RunExecutable(string exePath, JsonElement packageInfo) string output = await process.StandardOutput.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(output)) { - WriteLog($"Executable output: {output}"); + Logger.WriteCapturedOutput(PackageLabel(packageInfo), output); } } @@ -1580,7 +1592,7 @@ static async Task RunExecutable(string exePath, JsonElement packageInfo) string error = await process.StandardError.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(error)) { - WriteLog($"Executable error: {error}"); + Logger.WriteCapturedOutput(PackageLabel(packageInfo), error, isError: true); } } @@ -2636,12 +2648,12 @@ static async Task RunChocolateyInstall(string nupkgPath, JsonElement packageInfo // Always log ALL output for debugging - this is critical for troubleshooting if (!string.IsNullOrWhiteSpace(stdout)) { - Logger.Debug($"Chocolatey stdout: {stdout.Trim()}"); + Logger.WriteCapturedOutput("Chocolatey", stdout); } if (!string.IsNullOrWhiteSpace(stderr)) { - Logger.Debug($"Chocolatey stderr: {stderr.Trim()}"); + Logger.WriteCapturedOutput("Chocolatey", stderr, isError: true); } if (process.ExitCode != 0)