Skip to content

row::get_int throws on NULL WMI values, discarding the whole collection #1391

Description

@Fantu

Version: 0.15.0 (x64 MSI), Windows Server 2019

What happens

The auxiliary collector logs a conversion error every now and then. Two occurrences within 38 minutes on an otherwise healthy machine:

2026-08-10 16:09:08: error:D:\a\nscp\nscp\modules\CheckSystem\pdh_thread.cpp:477: Failed to get CPU frequency metrics: Failed to fetch CPU frequency metrics: Failed to convert LoadPercentage to number:80020005: Incompatibilità tra tipi.
2026-08-10 16:47:57: error:D:\a\nscp\nscp\modules\CheckSystem\pdh_thread.cpp:477: Failed to get CPU frequency metrics: Failed to fetch CPU frequency metrics: Failed to convert LoadPercentage to number:80020005: Incompatibilità tra tipi.

0x80020005 is DISP_E_TYPEMISMATCH; the trailing text is the localized system message ("type mismatch" on an Italian locale).

Root cause

Win32_Processor.LoadPercentage is occasionally NULL — WMI does not always have a sample ready for it. row::get_int has no case for that:

long long row::get_int(const std::string &col) const {
  ...
  hr = vValue.ChangeType(VT_I8);
  if (SUCCEEDED(hr)) return vValue.llVal;
  hr = vValue.ChangeType(VT_UI8);
  if (SUCCEEDED(hr)) return vValue.ullVal;
  throw wmi_exception(hr, "Failed to convert " + col + " to number");
}

ChangeType from VT_NULL fails for both target types, so it throws. Its sibling row::get_string does handle the case, returning "<NULL>":

if (vValue.vt == VT_NULL) return "<NULL>";

(both in include/win/wmi/wmi_query.cpp, at lines 152-161 and 112 respectively, as of 0.15.0)

Why it matters beyond the log noise

The exception escapes cpu_frequency::read_wmi() half-way through the row, propagates through query_wmi() and fetch(), and is only caught by run_fetch in the collector loop. So one NULL field discards the entire CPU frequency collection for that cycle — clock speeds, core counts, everything — not just the field that was missing.

Nothing is disabled permanently (only WBEM_E_INVALID_QUERY / WBEM_E_NOT_FOUND set fetch_cpu_frequency_ = false), so the next cycle recovers. In this particular case the practical impact is small, since these values feed check_cpu_frequency and check_cpu reads from PDH instead.

But get_int is not specific to this collector: it has around 79 call sites across the modules — check_disk_io, check_storagepool, check_shadowcopy, check_share and others — and every one of them turns an optional WMI field into a failed collection. Several of those classes have fields that are legitimately NULL depending on hardware and configuration.

Suggested fix

Whatever the shape, the goal is that a NULL field should not cost the caller the whole row.

The safest option looks like an explicit accessor for optional values, e.g. get_int(col, default) or a get_int_opt() returning boost::optional<long long>, leaving today's get_int throwing for fields that really are mandatory. Callers that read optional fields (LoadPercentage among them) would then opt in.

Just returning 0 from get_int on VT_NULL would also stop the bleeding, but for LoadPercentage zero is a perfectly valid reading, so the data would silently become wrong rather than absent — that is why I would rather see it be explicit at the call site.

Side note

The localized part of the message comes through mangled in the log (Incompatibilità renders with a stray ?? at the end of the line). Probably an encoding conversion on the system error string; happy to open a separate issue if that is worth tracking on its own.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions