From 966e6ba9340dcebe51da59fd80a89afe3762cc33 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 1 Aug 2024 16:24:32 +0200 Subject: [PATCH 1/5] - support HTTP server also for WASI - forward exit code via stderr for WASI --- .../WASI/WasiTestCommandArguments.cs | 19 ++++++++++- .../Commands/WASI/Engine/WasiTestCommand.cs | 33 +++++++++++++++++++ .../WASM/WasmTestMessagesProcessor.cs | 7 ++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs index 852360589..d4a2308c2 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs @@ -4,10 +4,11 @@ using System; using System.Collections.Generic; +using Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasm; namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasi; -internal class WasiTestCommandArguments : XHarnessCommandArguments +internal class WasiTestCommandArguments : XHarnessCommandArguments, IWebServerArguments { public WasmEngineArgument Engine { get; } = new(); public WasmEngineLocationArgument EnginePath { get; } = new(); @@ -16,6 +17,14 @@ internal class WasiTestCommandArguments : XHarnessCommandArguments public OutputDirectoryArgument OutputDirectory { get; } = new(); public TimeoutArgument Timeout { get; } = new(TimeSpan.FromMinutes(15)); + public WebServerMiddlewareArgument WebServerMiddlewarePathsAndTypes { get; } = new(); + public WebServerHttpEnvironmentVariables WebServerHttpEnvironmentVariables { get; } = new(); + public WebServerHttpsEnvironmentVariables WebServerHttpsEnvironmentVariables { get; } = new(); + public WebServerUseHttpsArguments WebServerUseHttps { get; } = new(); + public WebServerUseCorsArguments WebServerUseCors { get; } = new(); + public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); + public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + protected override IEnumerable GetArguments() => new Argument[] { Engine, @@ -24,5 +33,13 @@ internal class WasiTestCommandArguments : XHarnessCommandArguments OutputDirectory, Timeout, ExpectedExitCode, + + WebServerMiddlewarePathsAndTypes, + WebServerHttpEnvironmentVariables, + WebServerHttpsEnvironmentVariables, + WebServerUseHttps, + WebServerUseCors, + WebServerUseCrossOriginPolicy, + WebServerUseDefaultFiles, }; } diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs index 84dd26a3c..8b128cd67 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs @@ -65,8 +65,36 @@ protected override async Task InvokeInternal(ILogger logger) logger.LogInformation($"Using wasm engine {Arguments.Engine.Value} from path {engineBinary}"); await PrintVersionAsync(Arguments.Engine.Value.Value, engineBinary); + ServerURLs? serverURLs = null; + if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0) + { + serverURLs = await WebServer.Start( + Arguments, + logger, + cts.Token); + cts.CancelAfter(Arguments.Timeout); + } + var engineArgs = new List(); engineArgs.AddRange(Arguments.EngineArgs.Value); + + if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0) + { + foreach (var envVariable in Arguments.WebServerHttpEnvironmentVariables.Value) + { + engineArgs.Add($"--env"); + engineArgs.Add($"{envVariable}={serverURLs!.Http}"); + } + if (Arguments.WebServerUseHttps) + { + foreach (var envVariable in Arguments.WebServerHttpsEnvironmentVariables.Value) + { + engineArgs.Add($"--env"); + engineArgs.Add($"{envVariable}={serverURLs!.Https}"); + } + } + } + engineArgs.AddRange(PassThroughArguments); var xmlResultsFilePath = Path.Combine(Arguments.OutputDirectory, "testResults.xml"); @@ -115,6 +143,11 @@ protected override async Task InvokeInternal(ILogger logger) // process should be done too, or about to be done! var result = await processTask; ExitCode logProcessorExitCode = await logProcessor.CompleteAndFlushAsync(); + if (logProcessor.ForwardedExitCode != null) + { + // until WASI can work with unix exit code https://github.com/WebAssembly/wasi-cli/pull/44 + result.ExitCode = logProcessor.ForwardedExitCode.Value; + } if (result.ExitCode != Arguments.ExpectedExitCode) { diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs index 722bb2ab5..073b22ee9 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs @@ -36,6 +36,7 @@ public class WasmTestMessagesProcessor // Set once `WASM EXIT` message is received public TaskCompletionSource WasmExitReceivedTcs { get; } = new (); + public int? ForwardedExitCode {get; private set; } public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null) { @@ -224,6 +225,12 @@ private void ProcessMessage(string message, bool isError = false) if (line.StartsWith("WASM EXIT")) { _logger.LogDebug("Reached wasm exit"); + + // until WASI can work with unix exit code https://github.com/WebAssembly/wasi-cli/pull/44 + if (line.Length > 10) + { + ForwardedExitCode = int.Parse(line.Substring(10)); + } if (!WasmExitReceivedTcs.TrySetResult()) _logger.LogDebug("Got a duplicate exit message."); } From 68a0a854046b389a9e761c7c44751d359a74785d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 Aug 2024 18:52:17 +0200 Subject: [PATCH 2/5] feedback --- .../CommandArguments/WASI/WasiTestCommandArguments.cs | 1 + .../CommandArguments/WASM/IWebServerArguments.cs | 1 + .../CommandArguments/WASM/WasmTestBrowserCommandArguments.cs | 1 + .../CommandArguments/WASM/WasmTestCommandArguments.cs | 1 + .../CommandArguments/WASM/WebServerCommandArguments.cs | 1 + .../Commands/WASI/Engine/WasiTestCommand.cs | 4 ++-- .../Commands/WASM/JS/WasmTestCommand.cs | 4 ++-- 7 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs index d4a2308c2..441eb954d 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs @@ -24,6 +24,7 @@ internal class WasiTestCommandArguments : XHarnessCommandArguments, IWebServerAr public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; protected override IEnumerable GetArguments() => new Argument[] { diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs index 461983cb1..b4c672157 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs @@ -6,6 +6,7 @@ namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasm; internal interface IWebServerArguments { + bool IsWebServerEnabled { get; } WebServerMiddlewareArgument WebServerMiddlewarePathsAndTypes { get; } WebServerHttpEnvironmentVariables WebServerHttpEnvironmentVariables { get; } WebServerHttpsEnvironmentVariables WebServerHttpsEnvironmentVariables { get; } diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs index 6a438113b..503281a65 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs @@ -37,6 +37,7 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; protected override IEnumerable GetArguments() => new Argument[] { diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs index 7108f697d..71339054b 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs @@ -29,6 +29,7 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; protected override IEnumerable GetArguments() => new Argument[] { diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs index d405d8fdc..d4d1d3c18 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs @@ -18,6 +18,7 @@ internal class WebServerCommandArguments : XHarnessCommandArguments, IWebServerA public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; public TimeoutArgument Timeout { get; } = new(TimeSpan.FromMinutes(15)); diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs index 8b128cd67..f8118a044 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs @@ -66,7 +66,7 @@ protected override async Task InvokeInternal(ILogger logger) await PrintVersionAsync(Arguments.Engine.Value.Value, engineBinary); ServerURLs? serverURLs = null; - if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0) + if (Arguments.IsWebServerEnabled) { serverURLs = await WebServer.Start( Arguments, @@ -78,7 +78,7 @@ protected override async Task InvokeInternal(ILogger logger) var engineArgs = new List(); engineArgs.AddRange(Arguments.EngineArgs.Value); - if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0) + if (Arguments.IsWebServerEnabled) { foreach (var envVariable in Arguments.WebServerHttpEnvironmentVariables.Value) { diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs index 7e9eb2881..d2541dddd 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs @@ -75,7 +75,7 @@ protected override async Task InvokeInternal(ILogger logger) diagnosticsData.TargetOS = versionString; ServerURLs? serverURLs = null; - if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0) + if (Arguments.IsWebServerEnabled) { serverURLs = await WebServer.Start( Arguments, @@ -101,7 +101,7 @@ protected override async Task InvokeInternal(ILogger logger) engineArgs.Add("--"); } - if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0) + if (Arguments.IsWebServerEnabled) { foreach (var envVariable in Arguments.WebServerHttpEnvironmentVariables.Value) { From eeeeb328f0d51f45cd3b7e58f3435cf8e944ab31 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 Aug 2024 18:58:28 +0200 Subject: [PATCH 3/5] feedback + CRLF --- .../WASM/WasmTestMessagesProcessor.cs | 510 +++++++++--------- 1 file changed, 256 insertions(+), 254 deletions(-) diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs index 073b22ee9..9bde4fcc0 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs @@ -1,257 +1,259 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Microsoft.DotNet.XHarness.Common; -using Microsoft.DotNet.XHarness.Common.CLI; -using Microsoft.Extensions.Logging; - -namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm; - -public class WasmTestMessagesProcessor -{ - private static Regex xmlRx = new Regex(@"^STARTRESULTXML ([0-9]*) ([^ ]*) ENDRESULTXML", RegexOptions.Compiled | RegexOptions.CultureInvariant); - private readonly StreamWriter _stdoutFileWriter; - private readonly string _xmlResultsFilePath; - private static TimeSpan s_logMessagesTimeout = TimeSpan.FromMinutes(2); - - private readonly ILogger _logger; - private readonly Lazy? _errorScanner; - private readonly WasmSymbolicatorBase? _symbolicator; - private readonly ChannelReader<(string, bool)> _channelReader; - private readonly ChannelWriter<(string, bool)> _channelWriter; - private readonly TaskCompletionSource _completed = new (); - private bool _isRunning => !_completed.Task.IsCompleted; - private bool _loggedProcessorStopped = false; - - public string? LineThatMatchedErrorPattern { get; private set; } - - // Set once `WASM EXIT` message is received - public TaskCompletionSource WasmExitReceivedTcs { get; } = new (); - public int? ForwardedExitCode {get; private set; } - - public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null) - { - _xmlResultsFilePath = xmlResultsFilePath; - _stdoutFileWriter = File.CreateText(stdoutFilePath); - _stdoutFileWriter.AutoFlush = true; - _logger = logger; - - if (errorPatternsFile != null) - { - if (!File.Exists(errorPatternsFile)) - throw new ArgumentException($"Cannot find error patterns file {errorPatternsFile}"); - - _errorScanner = new Lazy(() => new ErrorPatternScanner(errorPatternsFile, logger)); - } - - _symbolicator = symbolicator; - - var channel = Channel.CreateUnbounded<(string, bool)>(new UnboundedChannelOptions { SingleReader = true }); - _channelWriter = channel.Writer; - _channelReader = channel.Reader; - } - - public async Task RunAsync(CancellationToken token) - { - try - { - await foreach ((string line, bool isError) in _channelReader.ReadAllAsync(token)) - { - ProcessMessage(line, isError); - } - _completed.SetResult(); - } - catch (Exception ex) - { - _channelWriter.TryComplete(ex); - - // surface the exception from task for this method - // and from _completed - _completed.SetException(ex); - throw; - } - } - - public void Invoke(string message, bool isError = false) - { - WarnOnceIfStopped(); - - if (_isRunning && _channelWriter.TryWrite((message, isError))) - return; - - LogMessage(message.TrimEnd(), isError); - } - - public Task InvokeAsync(string message, CancellationToken token, bool isError = false) - { - string? logMsg; - try - { - WarnOnceIfStopped(); - if (_isRunning) - return _channelWriter.WriteAsync((message, isError), token).AsTask(); - - logMsg = message.TrimEnd(); - } - catch (ChannelClosedException cce) - { - logMsg = $"Failed to write to the channel - {cce.Message}. Message: {message}"; - } - - LogMessage(logMsg, isError); - return Task.CompletedTask; - } - - private void WarnOnceIfStopped() - { - if (!_isRunning && !_loggedProcessorStopped) - { - _logger.LogWarning($"Message processor is not running anymore."); - _loggedProcessorStopped = true; - } - } - - private void LogMessage(string message, bool isError) - { - if (isError) - _logger.LogError(message); - else - _logger.LogInformation(message); - } - - public async Task CompleteAndFlushAsync(TimeSpan? timeout = null) - { - timeout ??= s_logMessagesTimeout; - _logger.LogInformation($"Waiting to flush log messages with a timeout of {timeout.Value.TotalSeconds} secs .."); - - try - { - _channelWriter.TryComplete(); - await _completed.Task.WaitAsync(timeout.Value); - return ExitCode.SUCCESS; - } - catch (TimeoutException) - { - _logger.LogError($"Flushing log messages timed out after {s_logMessagesTimeout.TotalSeconds}secs"); - return ExitCode.TIMED_OUT; - } - catch (Exception ex) - { - _logger.LogError($"Flushing log messages failed with: {ex}. Ignoring."); - return ExitCode.GENERAL_FAILURE; - } - } - - private void ProcessMessage(string message, bool isError = false) - { - WasmLogMessage? logMessage = null; - string line; - - if (message.StartsWith("{")) - { - try - { - logMessage = JsonSerializer.Deserialize(message); - if (logMessage != null) - { - line = logMessage.payload + " " + string.Join(" ", logMessage.arguments ?? Enumerable.Empty()); - } - else - { - line = message; - } - } - catch (JsonException) - { - line = message; - } - } - else - { - line = message; - } - - line = line.TrimEnd(); - - var match = xmlRx.Match(line); - if (match.Success) - { - var expectedLength = Int32.Parse(match.Groups[1].Value); - using (var stream = new FileStream(_xmlResultsFilePath, FileMode.Create)) - { - var bytes = System.Convert.FromBase64String(match.Groups[2].Value); - stream.Write(bytes); - if (bytes.Length == expectedLength) - { - _logger.LogInformation($"Received expected {bytes.Length} of {_xmlResultsFilePath}"); - } - else - { - _logger.LogInformation($"Received {bytes.Length} of {_xmlResultsFilePath} but expected {expectedLength}"); - } - } - } - else - { - ScanMessageForErrorPatterns(line); - line = Symbolicate(line); - - switch (logMessage?.method?.ToLowerInvariant()) - { - case "console.debug": _logger.LogDebug(line); break; - case "console.error": _logger.LogError(line); break; - case "console.warn": _logger.LogWarning(line); break; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.DotNet.XHarness.Common; +using Microsoft.DotNet.XHarness.Common.CLI; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm; + +public class WasmTestMessagesProcessor +{ + private static Regex xmlRx = new Regex(@"^STARTRESULTXML ([0-9]*) ([^ ]*) ENDRESULTXML", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private readonly StreamWriter _stdoutFileWriter; + private readonly string _xmlResultsFilePath; + private static TimeSpan s_logMessagesTimeout = TimeSpan.FromMinutes(2); + + private readonly ILogger _logger; + private readonly Lazy? _errorScanner; + private readonly WasmSymbolicatorBase? _symbolicator; + private readonly ChannelReader<(string, bool)> _channelReader; + private readonly ChannelWriter<(string, bool)> _channelWriter; + private readonly TaskCompletionSource _completed = new (); + private bool _isRunning => !_completed.Task.IsCompleted; + private bool _loggedProcessorStopped = false; + + public string? LineThatMatchedErrorPattern { get; private set; } + + // Set once `WASM EXIT` message is received + public TaskCompletionSource WasmExitReceivedTcs { get; } = new (); + public int? ForwardedExitCode {get; private set; } + + public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null) + { + _xmlResultsFilePath = xmlResultsFilePath; + _stdoutFileWriter = File.CreateText(stdoutFilePath); + _stdoutFileWriter.AutoFlush = true; + _logger = logger; + + if (errorPatternsFile != null) + { + if (!File.Exists(errorPatternsFile)) + throw new ArgumentException($"Cannot find error patterns file {errorPatternsFile}"); + + _errorScanner = new Lazy(() => new ErrorPatternScanner(errorPatternsFile, logger)); + } + + _symbolicator = symbolicator; + + var channel = Channel.CreateUnbounded<(string, bool)>(new UnboundedChannelOptions { SingleReader = true }); + _channelWriter = channel.Writer; + _channelReader = channel.Reader; + } + + public async Task RunAsync(CancellationToken token) + { + try + { + await foreach ((string line, bool isError) in _channelReader.ReadAllAsync(token)) + { + ProcessMessage(line, isError); + } + _completed.SetResult(); + } + catch (Exception ex) + { + _channelWriter.TryComplete(ex); + + // surface the exception from task for this method + // and from _completed + _completed.SetException(ex); + throw; + } + } + + public void Invoke(string message, bool isError = false) + { + WarnOnceIfStopped(); + + if (_isRunning && _channelWriter.TryWrite((message, isError))) + return; + + LogMessage(message.TrimEnd(), isError); + } + + public Task InvokeAsync(string message, CancellationToken token, bool isError = false) + { + string? logMsg; + try + { + WarnOnceIfStopped(); + if (_isRunning) + return _channelWriter.WriteAsync((message, isError), token).AsTask(); + + logMsg = message.TrimEnd(); + } + catch (ChannelClosedException cce) + { + logMsg = $"Failed to write to the channel - {cce.Message}. Message: {message}"; + } + + LogMessage(logMsg, isError); + return Task.CompletedTask; + } + + private void WarnOnceIfStopped() + { + if (!_isRunning && !_loggedProcessorStopped) + { + _logger.LogWarning($"Message processor is not running anymore."); + _loggedProcessorStopped = true; + } + } + + private void LogMessage(string message, bool isError) + { + if (isError) + _logger.LogError(message); + else + _logger.LogInformation(message); + } + + public async Task CompleteAndFlushAsync(TimeSpan? timeout = null) + { + timeout ??= s_logMessagesTimeout; + _logger.LogInformation($"Waiting to flush log messages with a timeout of {timeout.Value.TotalSeconds} secs .."); + + try + { + _channelWriter.TryComplete(); + await _completed.Task.WaitAsync(timeout.Value); + return ExitCode.SUCCESS; + } + catch (TimeoutException) + { + _logger.LogError($"Flushing log messages timed out after {s_logMessagesTimeout.TotalSeconds}secs"); + return ExitCode.TIMED_OUT; + } + catch (Exception ex) + { + _logger.LogError($"Flushing log messages failed with: {ex}. Ignoring."); + return ExitCode.GENERAL_FAILURE; + } + } + + private void ProcessMessage(string message, bool isError = false) + { + WasmLogMessage? logMessage = null; + string line; + + if (message.StartsWith("{")) + { + try + { + logMessage = JsonSerializer.Deserialize(message); + if (logMessage != null) + { + line = logMessage.payload + " " + string.Join(" ", logMessage.arguments ?? Enumerable.Empty()); + } + else + { + line = message; + } + } + catch (JsonException) + { + line = message; + } + } + else + { + line = message; + } + + line = line.TrimEnd(); + + var match = xmlRx.Match(line); + if (match.Success) + { + var expectedLength = Int32.Parse(match.Groups[1].Value); + using (var stream = new FileStream(_xmlResultsFilePath, FileMode.Create)) + { + var bytes = System.Convert.FromBase64String(match.Groups[2].Value); + stream.Write(bytes); + if (bytes.Length == expectedLength) + { + _logger.LogInformation($"Received expected {bytes.Length} of {_xmlResultsFilePath}"); + } + else + { + _logger.LogInformation($"Received {bytes.Length} of {_xmlResultsFilePath} but expected {expectedLength}"); + } + } + } + else + { + ScanMessageForErrorPatterns(line); + line = Symbolicate(line); + + switch (logMessage?.method?.ToLowerInvariant()) + { + case "console.debug": _logger.LogDebug(line); break; + case "console.error": _logger.LogError(line); break; + case "console.warn": _logger.LogWarning(line); break; case "console.trace": _logger.LogTrace(line); break; case "console.log": default: _logger.LogInformation(line); break; - } - - if (_stdoutFileWriter.BaseStream.CanWrite) - _stdoutFileWriter.WriteLine(line); - } - - // the test runner writes this as the last line, - // after the tests have run, and the xml results file - // has been written to the console - if (line.StartsWith("WASM EXIT")) - { - _logger.LogDebug("Reached wasm exit"); - - // until WASI can work with unix exit code https://github.com/WebAssembly/wasi-cli/pull/44 - if (line.Length > 10) - { - ForwardedExitCode = int.Parse(line.Substring(10)); - } - if (!WasmExitReceivedTcs.TrySetResult()) - _logger.LogDebug("Got a duplicate exit message."); - } - } - - private string Symbolicate(string msg) - { - if (_symbolicator is null) - return msg; - - return _symbolicator.Symbolicate(msg); - } - - private void ScanMessageForErrorPatterns(string message) - { - if (LineThatMatchedErrorPattern != null || _errorScanner == null) - return; - - if (_errorScanner.Value.IsError(message, out string? _)) - LineThatMatchedErrorPattern = message; - } - - public void ProcessErrorMessage(string message) => Invoke(message, isError: true); -} + } + + if (_stdoutFileWriter.BaseStream.CanWrite) + _stdoutFileWriter.WriteLine(line); + } + + // the test runner writes this as the last line, + // after the tests have run, and the xml results file + // has been written to the console + if (line.StartsWith("WASM EXIT")) + { + _logger.LogDebug("Reached wasm exit"); + + // until WASI can work with unix exit code https://github.com/WebAssembly/wasi-cli/pull/44 + if (line.Length > 10) + { + // the message on WASI looks like WASM EXIT 123 + // here we strip the first 10 characters and parse the rest + ForwardedExitCode = int.Parse(line.Substring(10)); + } + if (!WasmExitReceivedTcs.TrySetResult()) + _logger.LogDebug("Got a duplicate exit message."); + } + } + + private string Symbolicate(string msg) + { + if (_symbolicator is null) + return msg; + + return _symbolicator.Symbolicate(msg); + } + + private void ScanMessageForErrorPatterns(string message) + { + if (LineThatMatchedErrorPattern != null || _errorScanner == null) + return; + + if (_errorScanner.Value.IsError(message, out string? _)) + LineThatMatchedErrorPattern = message; + } + + public void ProcessErrorMessage(string message) => Invoke(message, isError: true); +} From de29552cbdf0f4c5da64dc19ce372894c55fcb3b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 Aug 2024 19:01:34 +0200 Subject: [PATCH 4/5] fix back --- .../WASM/WasmTestMessagesProcessor.cs | 510 +++++++++--------- 1 file changed, 255 insertions(+), 255 deletions(-) diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs index 9bde4fcc0..91fd916f3 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs @@ -1,259 +1,259 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Microsoft.DotNet.XHarness.Common; -using Microsoft.DotNet.XHarness.Common.CLI; -using Microsoft.Extensions.Logging; - -namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm; - -public class WasmTestMessagesProcessor -{ - private static Regex xmlRx = new Regex(@"^STARTRESULTXML ([0-9]*) ([^ ]*) ENDRESULTXML", RegexOptions.Compiled | RegexOptions.CultureInvariant); - private readonly StreamWriter _stdoutFileWriter; - private readonly string _xmlResultsFilePath; - private static TimeSpan s_logMessagesTimeout = TimeSpan.FromMinutes(2); - - private readonly ILogger _logger; - private readonly Lazy? _errorScanner; - private readonly WasmSymbolicatorBase? _symbolicator; - private readonly ChannelReader<(string, bool)> _channelReader; - private readonly ChannelWriter<(string, bool)> _channelWriter; - private readonly TaskCompletionSource _completed = new (); - private bool _isRunning => !_completed.Task.IsCompleted; - private bool _loggedProcessorStopped = false; - - public string? LineThatMatchedErrorPattern { get; private set; } - - // Set once `WASM EXIT` message is received - public TaskCompletionSource WasmExitReceivedTcs { get; } = new (); - public int? ForwardedExitCode {get; private set; } - - public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null) - { - _xmlResultsFilePath = xmlResultsFilePath; - _stdoutFileWriter = File.CreateText(stdoutFilePath); - _stdoutFileWriter.AutoFlush = true; - _logger = logger; - - if (errorPatternsFile != null) - { - if (!File.Exists(errorPatternsFile)) - throw new ArgumentException($"Cannot find error patterns file {errorPatternsFile}"); - - _errorScanner = new Lazy(() => new ErrorPatternScanner(errorPatternsFile, logger)); - } - - _symbolicator = symbolicator; - - var channel = Channel.CreateUnbounded<(string, bool)>(new UnboundedChannelOptions { SingleReader = true }); - _channelWriter = channel.Writer; - _channelReader = channel.Reader; - } - - public async Task RunAsync(CancellationToken token) - { - try - { - await foreach ((string line, bool isError) in _channelReader.ReadAllAsync(token)) - { - ProcessMessage(line, isError); - } - _completed.SetResult(); - } - catch (Exception ex) - { - _channelWriter.TryComplete(ex); - - // surface the exception from task for this method - // and from _completed - _completed.SetException(ex); - throw; - } - } - - public void Invoke(string message, bool isError = false) - { - WarnOnceIfStopped(); - - if (_isRunning && _channelWriter.TryWrite((message, isError))) - return; - - LogMessage(message.TrimEnd(), isError); - } - - public Task InvokeAsync(string message, CancellationToken token, bool isError = false) - { - string? logMsg; - try - { - WarnOnceIfStopped(); - if (_isRunning) - return _channelWriter.WriteAsync((message, isError), token).AsTask(); - - logMsg = message.TrimEnd(); - } - catch (ChannelClosedException cce) - { - logMsg = $"Failed to write to the channel - {cce.Message}. Message: {message}"; - } - - LogMessage(logMsg, isError); - return Task.CompletedTask; - } - - private void WarnOnceIfStopped() - { - if (!_isRunning && !_loggedProcessorStopped) - { - _logger.LogWarning($"Message processor is not running anymore."); - _loggedProcessorStopped = true; - } - } - - private void LogMessage(string message, bool isError) - { - if (isError) - _logger.LogError(message); - else - _logger.LogInformation(message); - } - - public async Task CompleteAndFlushAsync(TimeSpan? timeout = null) - { - timeout ??= s_logMessagesTimeout; - _logger.LogInformation($"Waiting to flush log messages with a timeout of {timeout.Value.TotalSeconds} secs .."); - - try - { - _channelWriter.TryComplete(); - await _completed.Task.WaitAsync(timeout.Value); - return ExitCode.SUCCESS; - } - catch (TimeoutException) - { - _logger.LogError($"Flushing log messages timed out after {s_logMessagesTimeout.TotalSeconds}secs"); - return ExitCode.TIMED_OUT; - } - catch (Exception ex) - { - _logger.LogError($"Flushing log messages failed with: {ex}. Ignoring."); - return ExitCode.GENERAL_FAILURE; - } - } - - private void ProcessMessage(string message, bool isError = false) - { - WasmLogMessage? logMessage = null; - string line; - - if (message.StartsWith("{")) - { - try - { - logMessage = JsonSerializer.Deserialize(message); - if (logMessage != null) - { - line = logMessage.payload + " " + string.Join(" ", logMessage.arguments ?? Enumerable.Empty()); - } - else - { - line = message; - } - } - catch (JsonException) - { - line = message; - } - } - else - { - line = message; - } - - line = line.TrimEnd(); - - var match = xmlRx.Match(line); - if (match.Success) - { - var expectedLength = Int32.Parse(match.Groups[1].Value); - using (var stream = new FileStream(_xmlResultsFilePath, FileMode.Create)) - { - var bytes = System.Convert.FromBase64String(match.Groups[2].Value); - stream.Write(bytes); - if (bytes.Length == expectedLength) - { - _logger.LogInformation($"Received expected {bytes.Length} of {_xmlResultsFilePath}"); - } - else - { - _logger.LogInformation($"Received {bytes.Length} of {_xmlResultsFilePath} but expected {expectedLength}"); - } - } - } - else - { - ScanMessageForErrorPatterns(line); - line = Symbolicate(line); - - switch (logMessage?.method?.ToLowerInvariant()) - { - case "console.debug": _logger.LogDebug(line); break; - case "console.error": _logger.LogError(line); break; - case "console.warn": _logger.LogWarning(line); break; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.DotNet.XHarness.Common; +using Microsoft.DotNet.XHarness.Common.CLI; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm; + +public class WasmTestMessagesProcessor +{ + private static Regex xmlRx = new Regex(@"^STARTRESULTXML ([0-9]*) ([^ ]*) ENDRESULTXML", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private readonly StreamWriter _stdoutFileWriter; + private readonly string _xmlResultsFilePath; + private static TimeSpan s_logMessagesTimeout = TimeSpan.FromMinutes(2); + + private readonly ILogger _logger; + private readonly Lazy? _errorScanner; + private readonly WasmSymbolicatorBase? _symbolicator; + private readonly ChannelReader<(string, bool)> _channelReader; + private readonly ChannelWriter<(string, bool)> _channelWriter; + private readonly TaskCompletionSource _completed = new (); + private bool _isRunning => !_completed.Task.IsCompleted; + private bool _loggedProcessorStopped = false; + + public string? LineThatMatchedErrorPattern { get; private set; } + + // Set once `WASM EXIT` message is received + public TaskCompletionSource WasmExitReceivedTcs { get; } = new (); + public int? ForwardedExitCode {get; private set; } + + public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null) + { + _xmlResultsFilePath = xmlResultsFilePath; + _stdoutFileWriter = File.CreateText(stdoutFilePath); + _stdoutFileWriter.AutoFlush = true; + _logger = logger; + + if (errorPatternsFile != null) + { + if (!File.Exists(errorPatternsFile)) + throw new ArgumentException($"Cannot find error patterns file {errorPatternsFile}"); + + _errorScanner = new Lazy(() => new ErrorPatternScanner(errorPatternsFile, logger)); + } + + _symbolicator = symbolicator; + + var channel = Channel.CreateUnbounded<(string, bool)>(new UnboundedChannelOptions { SingleReader = true }); + _channelWriter = channel.Writer; + _channelReader = channel.Reader; + } + + public async Task RunAsync(CancellationToken token) + { + try + { + await foreach ((string line, bool isError) in _channelReader.ReadAllAsync(token)) + { + ProcessMessage(line, isError); + } + _completed.SetResult(); + } + catch (Exception ex) + { + _channelWriter.TryComplete(ex); + + // surface the exception from task for this method + // and from _completed + _completed.SetException(ex); + throw; + } + } + + public void Invoke(string message, bool isError = false) + { + WarnOnceIfStopped(); + + if (_isRunning && _channelWriter.TryWrite((message, isError))) + return; + + LogMessage(message.TrimEnd(), isError); + } + + public Task InvokeAsync(string message, CancellationToken token, bool isError = false) + { + string? logMsg; + try + { + WarnOnceIfStopped(); + if (_isRunning) + return _channelWriter.WriteAsync((message, isError), token).AsTask(); + + logMsg = message.TrimEnd(); + } + catch (ChannelClosedException cce) + { + logMsg = $"Failed to write to the channel - {cce.Message}. Message: {message}"; + } + + LogMessage(logMsg, isError); + return Task.CompletedTask; + } + + private void WarnOnceIfStopped() + { + if (!_isRunning && !_loggedProcessorStopped) + { + _logger.LogWarning($"Message processor is not running anymore."); + _loggedProcessorStopped = true; + } + } + + private void LogMessage(string message, bool isError) + { + if (isError) + _logger.LogError(message); + else + _logger.LogInformation(message); + } + + public async Task CompleteAndFlushAsync(TimeSpan? timeout = null) + { + timeout ??= s_logMessagesTimeout; + _logger.LogInformation($"Waiting to flush log messages with a timeout of {timeout.Value.TotalSeconds} secs .."); + + try + { + _channelWriter.TryComplete(); + await _completed.Task.WaitAsync(timeout.Value); + return ExitCode.SUCCESS; + } + catch (TimeoutException) + { + _logger.LogError($"Flushing log messages timed out after {s_logMessagesTimeout.TotalSeconds}secs"); + return ExitCode.TIMED_OUT; + } + catch (Exception ex) + { + _logger.LogError($"Flushing log messages failed with: {ex}. Ignoring."); + return ExitCode.GENERAL_FAILURE; + } + } + + private void ProcessMessage(string message, bool isError = false) + { + WasmLogMessage? logMessage = null; + string line; + + if (message.StartsWith("{")) + { + try + { + logMessage = JsonSerializer.Deserialize(message); + if (logMessage != null) + { + line = logMessage.payload + " " + string.Join(" ", logMessage.arguments ?? Enumerable.Empty()); + } + else + { + line = message; + } + } + catch (JsonException) + { + line = message; + } + } + else + { + line = message; + } + + line = line.TrimEnd(); + + var match = xmlRx.Match(line); + if (match.Success) + { + var expectedLength = Int32.Parse(match.Groups[1].Value); + using (var stream = new FileStream(_xmlResultsFilePath, FileMode.Create)) + { + var bytes = System.Convert.FromBase64String(match.Groups[2].Value); + stream.Write(bytes); + if (bytes.Length == expectedLength) + { + _logger.LogInformation($"Received expected {bytes.Length} of {_xmlResultsFilePath}"); + } + else + { + _logger.LogInformation($"Received {bytes.Length} of {_xmlResultsFilePath} but expected {expectedLength}"); + } + } + } + else + { + ScanMessageForErrorPatterns(line); + line = Symbolicate(line); + + switch (logMessage?.method?.ToLowerInvariant()) + { + case "console.debug": _logger.LogDebug(line); break; + case "console.error": _logger.LogError(line); break; + case "console.warn": _logger.LogWarning(line); break; case "console.trace": _logger.LogTrace(line); break; case "console.log": default: _logger.LogInformation(line); break; - } - - if (_stdoutFileWriter.BaseStream.CanWrite) - _stdoutFileWriter.WriteLine(line); - } - - // the test runner writes this as the last line, - // after the tests have run, and the xml results file - // has been written to the console - if (line.StartsWith("WASM EXIT")) - { - _logger.LogDebug("Reached wasm exit"); - - // until WASI can work with unix exit code https://github.com/WebAssembly/wasi-cli/pull/44 - if (line.Length > 10) - { + } + + if (_stdoutFileWriter.BaseStream.CanWrite) + _stdoutFileWriter.WriteLine(line); + } + + // the test runner writes this as the last line, + // after the tests have run, and the xml results file + // has been written to the console + if (line.StartsWith("WASM EXIT")) + { + _logger.LogDebug("Reached wasm exit"); + + // until WASI can work with unix exit code https://github.com/WebAssembly/wasi-cli/pull/44 + if (line.Length > 10) + { // the message on WASI looks like WASM EXIT 123 - // here we strip the first 10 characters and parse the rest - ForwardedExitCode = int.Parse(line.Substring(10)); - } - if (!WasmExitReceivedTcs.TrySetResult()) - _logger.LogDebug("Got a duplicate exit message."); - } - } - - private string Symbolicate(string msg) - { - if (_symbolicator is null) - return msg; - - return _symbolicator.Symbolicate(msg); - } - - private void ScanMessageForErrorPatterns(string message) - { - if (LineThatMatchedErrorPattern != null || _errorScanner == null) - return; - - if (_errorScanner.Value.IsError(message, out string? _)) - LineThatMatchedErrorPattern = message; - } - - public void ProcessErrorMessage(string message) => Invoke(message, isError: true); -} + // here we strip the first 10 characters and parse the rest + ForwardedExitCode = int.Parse(line.Substring(10)); + } + if (!WasmExitReceivedTcs.TrySetResult()) + _logger.LogDebug("Got a duplicate exit message."); + } + } + + private string Symbolicate(string msg) + { + if (_symbolicator is null) + return msg; + + return _symbolicator.Symbolicate(msg); + } + + private void ScanMessageForErrorPatterns(string message) + { + if (LineThatMatchedErrorPattern != null || _errorScanner == null) + return; + + if (_errorScanner.Value.IsError(message, out string? _)) + LineThatMatchedErrorPattern = message; + } + + public void ProcessErrorMessage(string message) => Invoke(message, isError: true); +} From 580aa92025ba22c437d20548fe3b8f0bb15e1d74 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 8 Aug 2024 15:43:40 +0200 Subject: [PATCH 5/5] feedback --- .../Commands/WASI/Engine/WasiTestCommand.cs | 1 - .../Commands/WASM/JS/WasmTestCommand.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs index f8118a044..5adf7dddc 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs @@ -72,7 +72,6 @@ protected override async Task InvokeInternal(ILogger logger) Arguments, logger, cts.Token); - cts.CancelAfter(Arguments.Timeout); } var engineArgs = new List(); diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs index d2541dddd..7c52e26a8 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs @@ -81,7 +81,6 @@ protected override async Task InvokeInternal(ILogger logger) Arguments, logger, cts.Token); - cts.CancelAfter(Arguments.Timeout); } var engineArgs = new List();