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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

All notable changes to Chromatics are documented here.

## 4.3.36

- Updated to Sharlayan 9.2.1.
- A device provider that loses one device during start-up no longer reports the whole provider as failed. The skipped device is named and the rest of that provider's devices load as normal.
- The first-run wizard no longer saves a provider as enabled when App Control blocked its library.

## 4.3.35

- Fixed a crash when turning a device provider on or off if Windows App Control had blocked that provider's library. The toggle now returns to its previous position and explains what happened.
- Fixed a crash on the Continue button of the first-run wizard for the same reason.
- Chromatics no longer reports App Control blocks and Hue bridge address problems as application errors, since both are settings on your machine or network rather than faults in Chromatics.

## 4.3.34

- The console tab now shows which version of Chromatics you are running at startup.
- A device provider that fails to start now says so in the console tab, naming the provider and the reason. Providers that fail no longer look like they loaded, and the rest of your devices carry on loading as normal.

## 4.3.33

- Fixed lighting stopping for the rest of the session when the Screen Capture base layer was in use and you returned to the title or character select screen.
- Yeelight, LIFX, Nanoleaf, and Alienware lights that are switched off or off the network at startup now log as device notices instead of errors.

## 4.3.32

- Improved the accuracy and speed of game-data reads on FFXIV patch 7.55, including inventory, chat log, job gauges, and player stats.

## 4.3.31

- Added support for FFXIV patch 7.55.
Expand Down
2 changes: 1 addition & 1 deletion Chromatics.Tests/Chromatics.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Sharlayan" Version="9.1.3" />
<PackageReference Include="Sharlayan" Version="9.2.1" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions Chromatics.Tests/Core/ScreenCaptureProcessorLifetimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Chromatics.Layers;

namespace Chromatics.Tests.Core;

public class ScreenCaptureProcessorLifetimeTests
{
[Fact]
public void Instance_IsRebuiltAfterDispose()
{
// DisposeAll runs every time the player returns to the title screen and
// nulls the processor's surface. Handing the disposed object back out
// left the Screen Capture base layer attaching to a dead surface on
// every later tick, which killed lighting for the rest of the session.
var first = ScreenCaptureProcessor.Instance;
first.Dispose();

var second = ScreenCaptureProcessor.Instance;

Assert.NotSame(first, second);
}
}
64 changes: 64 additions & 0 deletions Chromatics.Tests/Helpers/AssemblyLoadGuardTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Reflection;
using Chromatics.Helpers;

namespace Chromatics.Tests.Helpers;

public class AssemblyLoadGuardTests
{
private static Exception AppControlBlock()
=> new FileLoadException("An Application Control policy has blocked this file. (0x800711C7)");

[Fact]
public void DirectLoadFailure_IsReported()
{
Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", AppControlBlock()));
Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", new FileNotFoundException("missing")));
Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", new BadImageFormatException("bad")));
Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Razer", new TypeLoadException("type")));
}

[Fact]
public void LoadFailureWrappedByAnEventHandler_IsReported()
{
// How it arrives from a routed-event handler (CHROMATICS-1N).
var ex = new TargetInvocationException(AppControlBlock());

Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Logitech", ex));
}

[Fact]
public void LoadFailureInsideAggregate_IsReported()
{
// How it arrives from an unobserved task (CHROMATICS-1G / 1H).
var ex = new AggregateException(new InvalidOperationException("unrelated"), AppControlBlock());

Assert.True(AssemblyLoadGuard.TryReportLoadFailure("OpenRGB", ex));
}

[Fact]
public void TypeInitializerCarryingALoadFailure_IsReported()
{
var ex = new TypeInitializationException("SomeType", AppControlBlock());

Assert.True(AssemblyLoadGuard.TryReportLoadFailure("Corsair", ex));
}

[Fact]
public void UnrelatedFailures_AreNotReportedAsLoadFailures()
{
Assert.False(AssemblyLoadGuard.TryReportLoadFailure("Hue", new InvalidOperationException()));
Assert.False(AssemblyLoadGuard.TryReportLoadFailure("Hue", new TimeoutException()));
Assert.False(AssemblyLoadGuard.TryReportLoadFailure("Hue",
new InvalidOperationException("outer", new ArgumentOutOfRangeException())));
}

[Fact]
public void TryRun_ReturnsFalseWhenTheActionFaults()
{
Assert.True(AssemblyLoadGuard.TryRun("Razer", () => { }));
Assert.False(AssemblyLoadGuard.TryRun("Razer", () => throw AppControlBlock()));
Assert.False(AssemblyLoadGuard.TryRun("Razer", () => throw new InvalidOperationException()));
}
}
53 changes: 53 additions & 0 deletions Chromatics.Tests/Helpers/NetworkFailureHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Sockets;
using Chromatics.Helpers;

namespace Chromatics.Tests.Helpers;

public class NetworkFailureHelperTests
{
[Fact]
public void ConnectionRefused_IsUnreachable()
{
var ex = new SocketException((int)SocketError.ConnectionRefused);

Assert.True(NetworkFailureHelper.IsUnreachable(ex));
}

[Fact]
public void SocketFailureWrappedInIoException_IsUnreachable()
{
var ex = new IOException("write failed", new SocketException((int)SocketError.HostUnreachable));

Assert.True(NetworkFailureHelper.IsUnreachable(ex));
}

[Fact]
public void HttpAndTimeoutFailures_AreUnreachable()
{
Assert.True(NetworkFailureHelper.IsUnreachable(new HttpRequestException("no route")));
Assert.True(NetworkFailureHelper.IsUnreachable(new TimeoutException()));
Assert.True(NetworkFailureHelper.IsUnreachable(new TaskCanceledException()));
}

[Fact]
public void SocketFailureInsideAggregate_IsUnreachable()
{
var ex = new AggregateException(
new InvalidOperationException("unrelated"),
new SocketException((int)SocketError.TimedOut));

Assert.True(NetworkFailureHelper.IsUnreachable(ex));
}

[Fact]
public void ApplicationBugs_AreNotUnreachable()
{
Assert.False(NetworkFailureHelper.IsUnreachable(new NullReferenceException()));
Assert.False(NetworkFailureHelper.IsUnreachable(new InvalidOperationException()));
Assert.False(NetworkFailureHelper.IsUnreachable(
new InvalidOperationException("outer", new ArgumentOutOfRangeException())));
}
}
2 changes: 1 addition & 1 deletion Chromatics.Tests/Helpers/UpdateNotesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void EmptyOrWhitespaceNotes_ReturnEmpty()
{
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection("", "4.3.26"));
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(" ", "4.3.26"));
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null, "4.3.26"));
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null!, "4.3.26"));
}

[Fact]
Expand Down
63 changes: 63 additions & 0 deletions Chromatics.Tests/ViewModels/DeviceToggleItemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Chromatics.ViewModels;

namespace Chromatics.Tests.ViewModels;

public class DeviceToggleItemTests
{
// App Control blocking a vendor DLL threw out of the fire-and-forget task
// the property setter starts, so the finalizer rethrew it as a crash
// (CHROMATICS-1G / 1H). The toggle has to absorb it and show the real state.
private static Exception AppControlBlock()
=> new FileLoadException("An Application Control policy has blocked this file. (0x800711C7)");

[Fact]
public void EnableFailure_RevertsToOff()
{
var item = new DeviceToggleItem("Razer", "tooltip", false,
() => throw AppControlBlock(),
() => { });

item.IsEnabled = true;

Assert.False(item.IsEnabled);
}

[Fact]
public void DisableFailure_LeavesToggleOn()
{
var item = new DeviceToggleItem("OpenRGB", "tooltip", true,
() => Task.FromResult(true),
() => throw AppControlBlock());

item.IsEnabled = false;

Assert.True(item.IsEnabled);
}

[Fact]
public void EnableVeto_LeavesToggleOff()
{
var item = new DeviceToggleItem("Hue", "tooltip", false,
() => Task.FromResult(false),
() => { });

item.IsEnabled = true;

Assert.False(item.IsEnabled);
}

[Fact]
public void SuccessfulEnable_TurnsToggleOn()
{
var item = new DeviceToggleItem("LIFX", "tooltip", false,
() => Task.FromResult(true),
() => { });

item.IsEnabled = true;

Assert.True(item.IsEnabled);
}
}
6 changes: 3 additions & 3 deletions Chromatics/Chromatics.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<StartupObject>Chromatics.Program</StartupObject>
<Version>4.3.31.0</Version>
<Version>4.3.36.0</Version>
<Authors>Danielle Thompson</Authors>
<!-- ApplicationManifest is conditional: local Debug + Release builds embed
app.manifest (no fusion-identity <msix> element) so VS debug runs and
Expand Down Expand Up @@ -97,7 +97,7 @@
<PackageReference Include="System.Drawing.Common" Version="10.0.8" />
<PackageReference Include="NAudio" Version="2.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NLog" Version="6.1.4" />
<PackageReference Include="NLog" Version="6.2.0" />
<!-- RGB.NET.Core comes from a local DLL (Build Dependencies/RGB.NET/)
that carries the CHROMATICS-15 disposed-singleton fix. The
PackageReference stays so transitive references from the other
Expand Down Expand Up @@ -135,7 +135,7 @@
<PackageReference Include="RGB.NET.Devices.Wooting" Version="3.2.0" />
<PackageReference Include="RGB.NET.HID" Version="3.2.0" />
<PackageReference Include="RGB.NET.Presets" Version="3.2.0" />
<PackageReference Include="Sharlayan" Version="9.1.3" />
<PackageReference Include="Sharlayan" Version="9.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
89 changes: 86 additions & 3 deletions Chromatics/Core/RGBController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,14 +1315,23 @@ public static bool LoadDeviceProvider(IRGBDeviceProvider provider, out Exception
showErrors = true;
#endif

provider.DevicesChanged += DevicesChanged;

var initError = LoadProviderWithDiagnostics(provider);

// Subscribed after the load so start-up failures are
// reported once, by the probe, with the provider named.
// This handler covers runtime errors from here on, and
// stays behind the user's preference — a load failure is
// actionable and reports either way.
if (showErrors)
provider.Exception += deviceExceptionEventHandler;

provider.DevicesChanged += DevicesChanged;

surface.Load(provider);
loadedDeviceProviders.Add(provider);

if (initError != null)
loadError ??= initError;

// Warn the user when a freshly-loaded provider gives us
// devices whose hardware/SDK can't accept per-LED writes
// (zone-only or single-colour fallback). Effects that
Expand Down Expand Up @@ -1421,6 +1430,80 @@ public static bool LoadDeviceProvider(IRGBDeviceProvider provider, out Exception

}

// Stand-in for surface.Load(provider) that reports what went wrong
// instead of swallowing it, without changing which failures are fatal.
//
// RGB.NET's Load calls Initialize(throwExceptions: false), and its
// Throw() only rethrows when that flag is set — otherwise it raises
// the Exception event and returns to the caller, which carries on.
// A provider whose native SDK is missing or refused to start ends up
// reporting success: the HID scan still lists the hardware, so the
// devices show up in the Mapping tab and never light. Nothing reaches
// the console tab and nothing reaches our caller.
//
// Initializing with throwExceptions: true lets the probe below decide
// per exception. Non-critical ones keep today's behaviour exactly
// (logged, provider keeps going); critical ones abort the provider and
// are returned to the caller. Either way the exception is caught here,
// so one bad provider never stops the others from loading.
private static Exception LoadProviderWithDiagnostics(IRGBDeviceProvider provider)
{
Exception captured = null;
var label = provider.GetType().Name;

void Probe(object sender, ExceptionEventArgs args)
{
// Only a critical exception means the provider failed. RGB.NET
// also raises this event per device from GetLoadedDevices, with
// isCritical false, when one device of many fails to add - the
// load carries on and the rest attach normally. Treating those
// as the verdict reported a working provider as failed and
// handed the caller a load error it should not have had.
if (args.IsCritical)
captured ??= args.Exception;
else
Logger.WriteConsole(Enums.LoggerTypes.Devices,
$"[{label}] a device was skipped: {args.Exception.Message}", forwardToSentry: false);

args.Throw = args.IsCritical;
}

provider.Exception += Probe;
try
{
if (!provider.IsInitialized)
provider.Initialize(RGBDeviceType.All, throwExceptions: true);
}
catch (Exception ex)
{
captured ??= ex;
}
finally
{
provider.Exception -= Probe;
}

surface?.Attach(provider.Devices);

if (captured != null)
{
Logger.WriteConsole(Enums.LoggerTypes.Error,
$"[{label}] failed to start: {captured.Message}", forwardToSentry: false);
LogLogitechSdkHintIfNeeded(captured);
}
else if (provider.Devices.Count == 0)
{
// Loaded without complaint and handed us nothing. Benign for
// the smart-light providers when the user owns no bulbs, but
// for an SDK provider it usually means the vendor software
// isn't running, so say so rather than leave a dead toggle.
Logger.WriteConsole(Enums.LoggerTypes.Devices,
$"[{label}] loaded but reported no devices.", forwardToSentry: false);
}

return captured;
}

// Surface a one-time console warning per provider load when devices
// can't accept per-LED writes — zone-based or single-colour SDK
// fallbacks. Effects that depend on per-LED spatial position
Expand Down
Loading
Loading