Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Spice86.Core/Emulator/Devices/Video/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Spice86.Core.Emulator.Devices.Video;
using Spice86.Core.Emulator.Devices.Video.Registers;
using Spice86.Core.Emulator.Devices.Video.Registers.Graphics;
using Spice86.Core.Emulator.Memory;
using Spice86.Logging;
using Spice86.Shared.Interfaces;

using ClockSelect = Registers.General.MiscellaneousOutput.ClockSelectValue;

Expand Down Expand Up @@ -74,7 +74,7 @@ public class Renderer : IVgaRenderer {
/// <param name="blinkState">Shared blink state for text-mode attribute blinking.</param>
/// <param name="loggerService">The logger service implementation.</param>
/// <param name="renderer256Color">The 256-color scanline renderer selected for the current CPU.</param>
public Renderer(IMemory memory, IVideoState state, VgaBlinkState blinkState, LoggerService loggerService, IVgaRenderer256Color renderer256Color) {
public Renderer(IMemory memory, IVideoState state, VgaBlinkState blinkState, ILoggerService loggerService, IVgaRenderer256Color renderer256Color) {
_state = state;
_blinkState = blinkState;
_renderer256Color = renderer256Color;
Expand Down
38 changes: 12 additions & 26 deletions src/Spice86.Logging/LoggerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ public class LoggerService : ILoggerService, IDisposable {

private static readonly object?[] EmptyProperties = [];

private LoggerConfiguration _loggerConfiguration;
private Logger? _logger;
private LoggingLevelSwitch _logLevelSwitch;
private bool _disposed;
Expand All @@ -25,16 +24,13 @@ public class LoggerService : ILoggerService, IDisposable {
public LoggerService() {
_logLevelSwitch = new LoggingLevelSwitch();
LoggerPropertyBag = new LoggerPropertyBag();
_loggerConfiguration = CreateLoggerConfiguration();
_loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch);
}

/// <inheritdoc />
public LoggingLevelSwitch LogLevelSwitch {
get => _logLevelSwitch;
set {
_logLevelSwitch = value ?? throw new ArgumentNullException(nameof(value));
_loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch);
ResetLogger();
}
}
Expand All @@ -49,28 +45,14 @@ public LoggingLevelSwitch LogLevelSwitch {
public LoggerConfiguration CreateLoggerConfiguration() {
LoggerConfiguration configuration = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new LoggerPropertyBagEnricher(LoggerPropertyBag))
.WriteTo.Async(conf => conf.Console(outputTemplate: LogFormat))
.WriteTo.Async(conf2 => conf2.Debug(outputTemplate: LogFormat))
.WriteTo.Async(conf3 =>
conf3.File("logs/log-.txt", outputTemplate: LogFormat, rollingInterval: RollingInterval.Day));
.Enrich.With(new LoggerPropertyBagEnricher(LoggerPropertyBag));
configuration.WriteTo.Async(conf => conf.Console(outputTemplate: LogFormat));
configuration.WriteTo.Async(conf2 => conf2.Debug(outputTemplate: LogFormat));
configuration.WriteTo.Async(conf3 =>
conf3.File("logs/log-.txt", outputTemplate: LogFormat, rollingInterval: RollingInterval.Day));
return configuration;
}

/// <inheritdoc />
public void UseStderrForConsoleOutput() {
_logger?.Dispose();
_logger = null;
_loggerConfiguration = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.With(new LoggerPropertyBagEnricher(LoggerPropertyBag))
.WriteTo.Async(conf => conf.Console(outputTemplate: LogFormat, standardErrorFromLevel: Serilog.Events.LogEventLevel.Verbose))
.WriteTo.Async(conf2 => conf2.Debug(outputTemplate: LogFormat))
.WriteTo.Async(conf3 =>
conf3.File("logs/log-.txt", outputTemplate: LogFormat, rollingInterval: RollingInterval.Day));
_loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch);
}

public void Write(LogEventLevel level, string messageTemplate) {
GetLoggerForLevel(level)?.Write(level, messageTemplate);
}
Expand Down Expand Up @@ -139,19 +121,23 @@ public void Dispose() {
private void ResetLogger() {
_logger?.Dispose();
_logger = null;
_loggerConfiguration = CreateLoggerConfiguration();
_loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch);
}

private Logger? GetLoggerForLevel(LogEventLevel level) {
if (_disposed || AreLogsSilenced || !IsEnabled(level)) {
return null;
}

_logger ??= _loggerConfiguration.CreateLogger();
_logger ??= BuildLogger();
return _logger;
}

private Logger BuildLogger() {
LoggerConfiguration configuration = CreateLoggerConfiguration();
configuration.MinimumLevel.ControlledBy(_logLevelSwitch);
return configuration.CreateLogger();
}

private static object?[] Normalize(object?[]? properties) {
return properties is { Length: > 0 } ? properties : EmptyProperties;
}
Expand Down
5 changes: 0 additions & 5 deletions src/Spice86.Shared/Interfaces/ILoggerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,4 @@ public interface ILoggerService : ILogger {
/// </summary>
/// <returns>The new <see cref="LoggerConfiguration"/></returns>
LoggerConfiguration CreateLoggerConfiguration();

/// <summary>
/// Redirects console log output to stderr, freeing stdout for protocol transports (e.g., MCP stdio).
/// </summary>
void UseStderrForConsoleOutput();
}
34 changes: 18 additions & 16 deletions src/Spice86/Spice86DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -726,24 +726,26 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai

McpHttpHost? mcpHttpTransport = null;

// Collect additional MCP tool assemblies and services from override supplier
IEnumerable<Assembly>? additionalToolAssemblies = null;
IEnumerable<object>? additionalMcpServices = null;
if (configuration.OverrideSupplier is IMcpToolSupplier mcpToolSupplier) {
additionalToolAssemblies = mcpToolSupplier.GetMcpToolAssemblies();
additionalMcpServices = mcpToolSupplier.GetMcpServices();
}
if (configuration.McpHttpPort != 0) {
// Collect additional MCP tool assemblies and services from override supplier
IEnumerable<Assembly>? additionalToolAssemblies = null;
IEnumerable<object>? additionalMcpServices = null;
if (configuration.OverrideSupplier is IMcpToolSupplier mcpToolSupplier) {
additionalToolAssemblies = mcpToolSupplier.GetMcpToolAssemblies();
additionalMcpServices = mcpToolSupplier.GetMcpServices();
}

mcpHttpTransport = new McpHttpHost(loggerService);
try {
mcpHttpTransport.Start(emulatorMcpServices, configuration.McpHttpPort, additionalToolAssemblies, additionalMcpServices);
if (loggerService.IsEnabled(LogEventLevel.Information)) {
loggerService.Information("MCP HTTP transport started on port {Port}", configuration.McpHttpPort);
mcpHttpTransport = new McpHttpHost(loggerService);
try {
mcpHttpTransport.Start(emulatorMcpServices, configuration.McpHttpPort, additionalToolAssemblies, additionalMcpServices);
if (loggerService.IsEnabled(LogEventLevel.Information)) {
loggerService.Information("MCP HTTP transport started on port {Port}", configuration.McpHttpPort);
}
} catch (InvalidOperationException ex) {
loggerService.Warning(ex, "Failed to configure MCP HTTP transport on port {Port}; MCP HTTP will be unavailable", configuration.McpHttpPort);
mcpHttpTransport.Dispose();
mcpHttpTransport = null;
}
} catch (InvalidOperationException ex) {
loggerService.Warning(ex, "Failed to configure MCP HTTP transport on port {Port}; MCP HTTP will be unavailable", configuration.McpHttpPort);
mcpHttpTransport.Dispose();
mcpHttpTransport = null;
}

if (loggerService.IsEnabled(LogEventLevel.Information)) {
Expand Down
15 changes: 8 additions & 7 deletions tests/Spice86.Tests/CfgCpu/CfgGraphReloadTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Spice86.Tests.CfgCpu;

using NSubstitute;

using Spice86.Core.CLI;
using Spice86.Core.Emulator.CPU;
using Spice86.Core.Emulator.CPU.CfgCpu;
Expand All @@ -12,7 +14,6 @@ namespace Spice86.Tests.CfgCpu;
using Spice86.Core.Emulator.StateSerialization;
using Spice86.Core.Emulator.StateSerialization.CfgReload;
using Spice86.Core.Emulator.VM;
using Spice86.Logging;
using Spice86.Shared.Emulator.Memory;
using Spice86.Shared.Interfaces;
using Spice86.Tests.Utility;
Expand Down Expand Up @@ -95,7 +96,7 @@ public void ReloadedGraphMatchesOriginal(string binName) {

// Reload into a fresh machine (no execution) and export.
string reloadedJson;
using (LoggerService loggerService = new())
ILoggerService loggerService = Substitute.For<ILoggerService>();
using (Spice86Creator creator = CreateCreator(binName))
using (Spice86DependencyInjection di = creator.Create()) {
using CfgNodeExecutionCompiler compiler = NewCompiler(loggerService);
Expand All @@ -120,7 +121,7 @@ public void ReloadedGraphReExportsToSameDump(string binName) {
// restore: typed instruction edges, per-node MaxSucc, selector dispatch edges and ids.
CfgReloadDump original = CaptureDump(binName);

using LoggerService loggerService = new();
ILoggerService loggerService = Substitute.For<ILoggerService>();
using Spice86Creator creator = CreateCreator(binName);
using Spice86DependencyInjection di = creator.Create();
using CfgNodeExecutionCompiler compiler = NewCompiler(loggerService);
Expand All @@ -144,7 +145,7 @@ public void ReloadPreservesIdsAndSeedsAllocator(string binName) {
HashSet<int> expectedIds = new(scaledDump.Nodes.Select(n => n.Id).Concat(scaledDump.Blocks.Select(b => b.Id)));
int maxId = expectedIds.Count == 0 ? -1 : expectedIds.Max();

using LoggerService loggerService = new();
ILoggerService loggerService = Substitute.For<ILoggerService>();
using Spice86Creator creator = CreateCreator(binName);
using Spice86DependencyInjection di = creator.Create();
using CfgNodeExecutionCompiler compiler = NewCompiler(loggerService);
Expand Down Expand Up @@ -173,7 +174,7 @@ public void NewDiscoveryAfterReloadGetsNonCollidingId(string binName) {
HashSet<int> reloadedIds = new(scaledDump.Nodes.Select(n => n.Id).Concat(scaledDump.Blocks.Select(b => b.Id)));
int maxReloadedId = reloadedIds.Max();

using LoggerService loggerService = new();
ILoggerService loggerService = Substitute.For<ILoggerService>();
using Spice86Creator creator = CreateCreator(binName);
using Spice86DependencyInjection di = creator.Create();
Machine machine = di.Machine;
Expand Down Expand Up @@ -227,7 +228,7 @@ public void ResumeAfterReloadReconnectsAndPromotesToLive(string binName) {
.Select(n => n.Id)
.Single();

using LoggerService loggerService = new();
ILoggerService loggerService = Substitute.For<ILoggerService>();
using Spice86Creator creator = CreateCreator(binName);
using Spice86DependencyInjection di = creator.Create();
Machine machine = di.Machine;
Expand Down Expand Up @@ -262,7 +263,7 @@ public void PromotedReloadedNodeHasMemoryWriteBreakpoint(string binName) {
(CfgReloadDump dump, byte[] memoryImage) = CaptureDumpAndMemory(binName);
SegmentedAddress entryAddress = ParseAddress(dump.EntryPoints[0]);

using LoggerService loggerService = new();
ILoggerService loggerService = Substitute.For<ILoggerService>();
using Spice86Creator creator = CreateCreator(binName);
using Spice86DependencyInjection di = creator.Create();
Machine machine = di.Machine;
Expand Down
2 changes: 1 addition & 1 deletion tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private InstructionsFeeder CreateInstructionsFeeder() {
}

private InstructionsFeeder CreateInstructionsFeeder(IMmu mmu) {
ILoggerService loggerService = Substitute.For<LoggerService>();
ILoggerService loggerService = Substitute.For<ILoggerService>();
State state = new(CpuModel.INTEL_80286);
AddressReadWriteBreakpoints memoryBreakpoints = new();
AddressReadWriteBreakpoints ioBreakpoints = new();
Expand Down
17 changes: 17 additions & 0 deletions tests/Spice86.Tests/CollectibleAssemblyLoadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Spice86.Tests;

using System.Reflection;
using System.Runtime.Loader;

/// <summary>
/// Collectible load context used to host a single compiled generated-override assembly so it can be
/// unloaded once the test that produced it completes, instead of permanently growing the default context.
/// </summary>
internal sealed class CollectibleAssemblyLoadContext : AssemblyLoadContext {
public CollectibleAssemblyLoadContext() : base(isCollectible: true) {
}

protected override Assembly? Load(AssemblyName assemblyName) {
return null;
}
}
19 changes: 17 additions & 2 deletions tests/Spice86.Tests/CompiledGeneratedOverride.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ namespace Spice86.Tests;

using Spice86.Core.Emulator.Function;

internal sealed class CompiledGeneratedOverride(IOverrideSupplier supplier) {
public IOverrideSupplier Supplier { get; } = supplier;
using System.Runtime.Loader;

internal sealed class CompiledGeneratedOverride : IDisposable {
private readonly AssemblyLoadContext _loadContext;

public CompiledGeneratedOverride(AssemblyLoadContext loadContext, IOverrideSupplier supplier) {
_loadContext = loadContext;
Supplier = supplier;
}

public IOverrideSupplier Supplier { get; }

public void Dispose() {
if (_loadContext.IsCollectible) {
_loadContext.Unload();
}
}
}
2 changes: 1 addition & 1 deletion tests/Spice86.Tests/GeneratedCodeMachineTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public void WideSegmentSpanProgramInitializesSegmentFieldsFromObservedConstants(
source.Should().NotContain("relocationBaseSegment");
source.Should().NotContain("relocated from the runtime entry segment");
// The generated source still compiles.
new GeneratedOverrideCompiler().CompileSupplier(source);
using CompiledGeneratedOverride compiledOverride = new GeneratedOverrideCompiler().CompileSupplier(source);
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void TestGeneratedCode(string binName, byte[] expected, long maxCycles =
}

public void TestGeneratedCode(string binName, byte[] expected, GeneratedCodeRunOptions options, Action<Machine>? assertions = null) {
CompiledGeneratedOverride compiledOverride = GenerateAndCompileSupplier(binName, options);
using CompiledGeneratedOverride compiledOverride = GenerateAndCompileSupplier(binName, options);

using Spice86Creator creator = new(binName: binName, maxCycles: options.MaxCycles, enablePit: options.EnablePit,
installInterruptVectors: options.InstallInterruptVectors, failOnUnhandledPort: options.FailOnUnhandledPort,
Expand Down
22 changes: 15 additions & 7 deletions tests/Spice86.Tests/GeneratedOverrideCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ namespace Spice86.Tests;
using System.Runtime.Loader;

internal sealed class GeneratedOverrideCompiler {
private static readonly MetadataReference[] PlatformMetadataReferences = CreateMetadataReferences();

public CompiledGeneratedOverride CompileSupplier(string source) {
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview));
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName: "Spice86.GeneratedCode.Tests." + Guid.NewGuid().ToString("N"),
syntaxTrees: [syntaxTree],
references: GetMetadataReferences(),
references: PlatformMetadataReferences,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

using MemoryStream peStream = new();
Expand All @@ -30,14 +32,20 @@ public CompiledGeneratedOverride CompileSupplier(string source) {
}

peStream.Position = 0;
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(peStream);
Type supplierType = assembly.GetTypes().Single(type => typeof(IOverrideSupplier).IsAssignableFrom(type) && !type.IsAbstract);
object supplierInstance = Activator.CreateInstance(supplierType)
?? throw new InvalidOperationException($"Could not instantiate generated supplier type {supplierType.FullName}.");
return new CompiledGeneratedOverride((IOverrideSupplier)supplierInstance);
CollectibleAssemblyLoadContext loadContext = new();
try {
Assembly assembly = loadContext.LoadFromStream(peStream);
Type supplierType = assembly.GetTypes().Single(type => typeof(IOverrideSupplier).IsAssignableFrom(type) && !type.IsAbstract);
object supplierInstance = Activator.CreateInstance(supplierType)
?? throw new InvalidOperationException($"Could not instantiate generated supplier type {supplierType.FullName}.");
return new CompiledGeneratedOverride(loadContext, (IOverrideSupplier)supplierInstance);
} catch {
loadContext.Unload();
throw;
}
}

private static MetadataReference[] GetMetadataReferences() {
private static MetadataReference[] CreateMetadataReferences() {
string trustedAssemblies = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? string.Empty;
IEnumerable<string> trustedPaths = trustedAssemblies.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
IEnumerable<string> loadedPaths = AppDomain.CurrentDomain.GetAssemblies()
Expand Down
11 changes: 10 additions & 1 deletion tests/Spice86.Tests/Spice86.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
<ImplicitUsings>enable</ImplicitUsings>
<WarningsAsErrors>nullable</WarningsAsErrors>
<IsPackable>false</IsPackable>
<!--
Use workstation GC for the test host. Server GC is lazy about reclaiming
collectible memory while RAM is abundant, which lets per-test allocations
(the multi-MB Ram array, audio reverb buffers, CFG graphs) pile up and push
RSS into the tens of GB over a full run eventually OOM-killing the host on
memory-constrained machines/CI.
-->
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ErrorProne.NET.CoreAnalyzers">
Expand Down Expand Up @@ -84,4 +93,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
</Project>
1 change: 1 addition & 0 deletions tests/Spice86.Tests/Spice86Creator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public Spice86Creator(string binName, bool enablePit = false,
RecordedDataDirectory = exportFolder,
SilencedLogs = true,
HttpApiPort = 0,
McpHttpPort = 0,
// Deterministic cycle-based clock (CyclesClock) to avoid
// wall-clock non-determinism in tests.
// 333333 is the value that allowed most wall clock based tests to pass without changing all the expected values.
Expand Down
4 changes: 2 additions & 2 deletions tests/Spice86.Tests/Video/RendererTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Spice86.Core.Emulator.Devices.Video.Registers.CrtController;
using Spice86.Core.Emulator.Devices.Video.Registers.Graphics;
using Spice86.Core.Emulator.Memory;
using Spice86.Logging;
using Spice86.Shared.Interfaces;

using Xunit;

Expand Down Expand Up @@ -1408,7 +1408,7 @@ public void HorizontalPixelPanning_TextMode_9DotWithLineGraphics_AddsOnePan() {
mockMemory.When(m => m.RegisterMapping(Arg.Any<uint>(), Arg.Any<uint>(), Arg.Any<IMemoryDevice>()))
.Do(callInfo => captured = (VideoMemory)callInfo.ArgAt<IMemoryDevice>(2));

LoggerService loggerService = new();
ILoggerService loggerService = Substitute.For<ILoggerService>();
IVgaRenderer256Color renderer256Color = Create256ColorRenderer();
Renderer renderer = new(mockMemory, state, blinkState, loggerService, renderer256Color);

Expand Down
Loading
Loading