Skip to content
Open
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
5 changes: 5 additions & 0 deletions agents/wnx/ohm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Build output of the LibreHardwareMonitor based OpenHardwareMonitorCLI
/external/
/dist/
/OpenHardwareMonitorCLI/bin/
/OpenHardwareMonitorCLI/obj/
31 changes: 31 additions & 0 deletions agents/wnx/ohm/OpenHardwareMonitorCLI/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Optional configuration for OpenHardwareMonitorCLI.exe (LibreHardwareMonitor edition).
Rename to "OpenHardwareMonitorCLI.exe.config" and put it next to the EXE
(Checkmk agent: C:\ProgramData\checkmk\agent\bin\). Restart the agent afterwards.
Without this file the defaults below apply.
-->
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<appSettings>
<!-- Additional hardware groups: cpu, gpu, memory, motherboard, controller, storage,
network, psu, battery, powermonitor, all.
Default enabled (like the original OHM CLI): cpu, memory, motherboard, controller, storage -->
<add key="Enable" value="" />
<add key="Disable" value="" />

<!-- Update interval in milliseconds (default 1000) -->
<add key="IntervalMs" value="1000" />

<!-- true = OpenHardwareMonitor compatible identifiers (/hdd/0, /nvidiagpu/0, /mainboard ...)
-> existing Checkmk services keep their names
false = native LibreHardwareMonitor identifiers (/nvme/0, /gpu-nvidia/0, /motherboard ...) -->
<add key="CompatIdentifiers" value="true" />

<!-- Optional log file (relative paths are relative to the EXE), e.g. OpenHardwareMonitorCLI.log -->
<add key="LogFile" value="" />
<add key="Verbose" value="false" />
</appSettings>
</configuration>
21 changes: 21 additions & 0 deletions agents/wnx/ohm/OpenHardwareMonitorCLI/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
// This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
// conditions defined in the file COPYING, which is part of this source code package.

using System.ComponentModel;
using System.Management.Instrumentation;

// Same WMI namespace as the original OpenHardwareMonitorCLI.exe. The Checkmk Windows agent queries
// "SELECT Index,Name,Parent,SensorType,Value FROM Sensor" in Root\OpenHardwareMonitor.
// TODO(LHM rename): change together with kWmiPathOhm (agents/wnx/include/wnx/section_header.h)
// and the namespace in kResetCommand (agents/wnx/include/providers/ohm.h).
[assembly: Instrumented("root/OpenHardwareMonitor")]

/// <summary>
/// Allows the WMI schema to be registered up-front with InstallUtil.exe
/// (optional; the schema is also registered automatically on first start as administrator/SYSTEM).
/// </summary>
[RunInstaller(true)]
public class InstanceInstaller : DefaultManagementProjectInstaller
{
}
139 changes: 139 additions & 0 deletions agents/wnx/ohm/OpenHardwareMonitorCLI/DataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
// This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
// conditions defined in the file COPYING, which is part of this source code package.

using System;
using System.Threading;
using LibreHardwareMonitor.Hardware;
using OpenHardwareMonitor.WMI;

namespace OpenHardwareMonitor.CLI
{
/// <summary>
/// Owns the LibreHardwareMonitor Computer object, the WMI provider and the update timer.
/// </summary>
internal sealed class DataProvider : IDisposable
{
private readonly Computer _computer;
private readonly UpdateVisitor _visitor = new UpdateVisitor();
private readonly Options _options;
private WmiProvider _wmiProvider;
private Timer _timer;
private int _updating;
private bool _disposed;

public DataProvider(Options options, bool publishToWmi)
{
_options = options;

_computer = new Computer
{
IsCpuEnabled = options.IsEnabled("cpu"),
IsGpuEnabled = options.IsEnabled("gpu"),
IsMemoryEnabled = options.IsEnabled("memory"),
IsMotherboardEnabled = options.IsEnabled("motherboard"),
IsControllerEnabled = options.IsEnabled("controller"),
IsStorageEnabled = options.IsEnabled("storage"),
IsNetworkEnabled = options.IsEnabled("network"),
IsPsuEnabled = options.IsEnabled("psu"),
IsBatteryEnabled = options.IsEnabled("battery"),
IsPowerMonitorEnabled = options.IsEnabled("powermonitor")
};

Log.Debug("Opening LibreHardwareMonitor (groups: " + string.Join(",", options.Groups) + ")");
_computer.Open();

UpdateNow();

_wmiProvider = new WmiProvider(_computer, new IdentifierMapper(options.CompatIdentifiers), publishToWmi);
_wmiProvider.Update();
}

public IComputer Computer
{
get { return _computer; }
}

public WmiProvider Wmi
{
get { return _wmiProvider; }
}

public void Start()
{
if (_timer == null)
_timer = new Timer(OnTick, null, _options.IntervalMs, _options.IntervalMs);
else
_timer.Change(_options.IntervalMs, _options.IntervalMs);
}

public void Stop()
{
if (_timer != null)
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}

/// <summary>Runs one full update cycle synchronously.</summary>
public void UpdateNow()
{
_computer.Accept(_visitor);
if (_wmiProvider != null)
_wmiProvider.Update();
}

private void OnTick(object state)
{
// Never run two updates in parallel (slow SMART/GPU reads can exceed the interval).
if (Interlocked.CompareExchange(ref _updating, 1, 0) != 0)
return;

try
{
if (!_disposed)
UpdateNow();
}
catch (Exception e)
{
Log.Warn("Update cycle failed: " + e.Message);
}
finally
{
Interlocked.Exchange(ref _updating, 0);
}
}

public void Dispose()
{
if (_disposed)
return;

_disposed = true;

if (_timer != null)
{
using (ManualResetEvent done = new ManualResetEvent(false))
{
_timer.Dispose(done);
done.WaitOne(5000);
}

_timer = null;
}

if (_wmiProvider != null)
{
_wmiProvider.Dispose();
_wmiProvider = null;
}

try
{
_computer.Close();
}
catch (Exception e)
{
Log.Debug("Computer.Close failed: " + e.Message);
}
}
}
}
146 changes: 146 additions & 0 deletions agents/wnx/ohm/OpenHardwareMonitorCLI/EmbeddedAssemblyResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
// This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
// conditions defined in the file COPYING, which is part of this source code package.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace OpenHardwareMonitor.CLI
{
/// <summary>
/// Resolves the dependencies of the CLI without additional files on disk:
///
/// * LibreHardwareMonitorLib -> OpenHardwareMonitorLib.dll next to the EXE
/// (this is the file name the Checkmk agent bakery deploys)
/// * everything else (HidSharp, DiskInfoToolkit, RAMSPDToolkit, System.Memory, ...)
/// -> embedded resources "deps.&lt;name&gt;.dll" inside this EXE
///
/// Regular .NET probing still wins: if a real LibreHardwareMonitorLib.dll / HidSharp.dll
/// lies next to the EXE, it is used and this resolver is never asked.
/// </summary>
internal static class EmbeddedAssemblyResolver
{
private const string LhmAssemblyName = "LibreHardwareMonitorLib";
// TODO(LHM rename): LibreHardwareMonitorLib.dll is shipped as OpenHardwareMonitorLib.dll only because
// the bakery plugin (cmk/base/plugins/bakery/win_openhardwaremonitor.py) deploys that file name.
private const string LhmFileName = "OpenHardwareMonitorLib.dll";

private static readonly Dictionary<string, Assembly> Loaded = new Dictionary<string, Assembly>(StringComparer.OrdinalIgnoreCase);
private static readonly object Sync = new object();
private static bool _installed;

public static void Install()
{
if (_installed)
return;

_installed = true;
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
}

private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
AssemblyName requested = new AssemblyName(args.Name);
string name = requested.Name;

// Localized satellite assemblies are not shipped on purpose (stable, English sensor
// names on every host). Returning null makes ResourceManager use the neutral resources.
if (name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
return null;

lock (Sync)
{
Assembly assembly;
if (Loaded.TryGetValue(name, out assembly))
return assembly;

// Already loaded under the same simple name (e.g. different version requested)?
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
{
if (string.Equals(a.GetName().Name, name, StringComparison.OrdinalIgnoreCase))
{
Loaded[name] = a;
return a;
}
}

assembly = LoadAssembly(name);
if (assembly != null)
Loaded[name] = assembly;

return assembly;
}
}

private static Assembly LoadAssembly(string name)
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;

if (string.Equals(name, LhmAssemblyName, StringComparison.OrdinalIgnoreCase))
{
string path = Path.Combine(baseDir, LhmFileName);
if (File.Exists(path))
return Assembly.LoadFrom(path);
}

Assembly self = typeof(EmbeddedAssemblyResolver).Assembly;
string resourceName = "deps." + name + ".dll";

byte[] data = ReadResource(self, resourceName);
if (data != null)
return Assembly.Load(data);

// Fallback: the embedded file name may differ from the assembly name.
// Load the remaining embedded assemblies and look for a matching identity.
foreach (string candidate in self.GetManifestResourceNames())
{
if (!candidate.StartsWith("deps.", StringComparison.Ordinal))
continue;

byte[] bytes = ReadResource(self, candidate);
if (bytes == null)
continue;

Assembly assembly = Assembly.Load(bytes);
string simpleName = assembly.GetName().Name;
if (!Loaded.ContainsKey(simpleName))
Loaded[simpleName] = assembly;

if (string.Equals(simpleName, name, StringComparison.OrdinalIgnoreCase))
return assembly;
}

return null;
}

private static readonly HashSet<string> LoadedResources = new HashSet<string>(StringComparer.Ordinal);

private static byte[] ReadResource(Assembly assembly, string resourceName)
{
if (LoadedResources.Contains(resourceName))
return null;

using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
return null;

LoadedResources.Add(resourceName);

byte[] data = new byte[stream.Length];
int offset = 0;
while (offset < data.Length)
{
int read = stream.Read(data, offset, data.Length - offset);
if (read <= 0)
break;
offset += read;
}

return data;
}
}
}
}
Loading
Loading