Skip to content
38 changes: 36 additions & 2 deletions lib/train/platforms/detect/helpers/os_windows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,44 @@ def windows_uuid_from_wmic_or_cim
end

def windows_uuid_from_wmic
result = @backend.run_command("wmic csproduct get UUID")
# Switched from `wmic csproduct get UUID` to `wmic csproduct get UUID /value`
# to make the parsing of the UUID more reliable and consistent.
#
# When using the original `wmic csproduct get UUID` command, the output includes
# a header line and spacing that can vary depending on the system, making it harder
# to reliably extract the UUID. In some cases, splitting by line and taking the last
# element returns an empty string, even when exit_status is 0.
#
# Example:
#
# (byebug) result = @backend.run_command("wmic csproduct get UUID")
# #<struct Train::Extras::CommandResult stdout="UUID \r\r\nEC20EBD7-8E03-06A8-645F-2D22E5A3BA4B \r\r\n\r\r\n", stderr="", exit_status=0>
# (byebug) result.stdout
# "UUID \r\r\nEC20EBD7-8E03-06A8-645F-2D22E5A3BA4B \r\r\n\r\r\n"
# (byebug) result.exit_status
# 0
# (byebug) result.stdout.split("\r\n")[-1].strip
# ""
#
# In contrast, `wmic csproduct get UUID /value` returns a consistent `UUID=<value>` format,
# which is more suitable for regex matching.
#
# Example:
#
# byebug) result = @backend.run_command("wmic csproduct get UUID /value")
# #<struct Train::Extras::CommandResult stdout="\r\r\n\r\r\nUUID=EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B\r\r\n\r\r\n\r\r\n\r\r\n", stderr="", exit_status=0>
# (byebug) result.stdout
# "\r\r\n\r\r\nUUID=EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B\r\r\n\r\r\n\r\r\n\r\r\n"
# (byebug) result.stdout&.match(/UUID=([A-F0-9\-]+)/i)&.captures&.first
# "EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B"
#
# This change improves parsing reliability and handles edge cases where the previous
# approach would return `nil` or raise errors on empty output lines.

result = @backend.run_command("wmic csproduct get UUID /value")
return unless result.exit_status == 0

result.stdout.split("\r\n")[-1].strip
result.stdout&.match(/UUID=([A-F0-9\-]+)/i)&.captures&.first
end

def windows_uuid_from_registry
Expand Down
31 changes: 31 additions & 0 deletions test/unit/platforms/detect/os_windows_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,34 @@ def initialize
_(detector.platform[:arch]).must_equal("x86_64")
end
end

describe "windows_uuid_from_wmic when wmic csproduct get UUID /value returns a valid UUID in stdout" do
let(:detector) do
detector = OsDetectWindowsTester.new
detector.backend.mock_command("wmic csproduct get UUID /value", "\r\r\n\r\r\nUUID=EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B\r\r\n\r\r\n\r\r\n\r\r\n", "", 0)
detector
end

it "retrieves the correct UUID from wmic" do
_(detector.windows_uuid_from_wmic).must_equal("EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B")
end
end

describe "windows_uuid_from_wmic when stdout is empty even though wmic csproduct get UUID /value exits successfully" do

# This is a highly unlikely scenario, but there have been cases where customers
# observed an empty stdout from `wmic csproduct get UUID` despite a successful exit status.
# This test case is to ensure that we handle this gracefully.
# In such cases, we should return nil (which is expected) instead of raising an error.

let(:detector) do
detector = OsDetectWindowsTester.new
detector.backend.mock_command("wmic csproduct get UUID /value", "", "", 0)
detector
end

it "retrieves the correct UUID from wmic" do
assert_nil(detector.windows_uuid_from_wmic)
end
end

2 changes: 1 addition & 1 deletion test/unit/platforms/detect/uuid_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def mock_platform(name, commands = {}, files = {}, plat_options = {})
end

it "finds a windows uuid from wmic" do
commands = { "wmic csproduct get UUID" => "UUID\r\nd400073f-0920-41aa-8dd3-2ea59b18f5ce\r\n", "wmic /?" => "" } # Mocking wmic command to simulate its availability
commands = { "wmic csproduct get UUID /value" => "\r\r\n\r\r\nUUID=d400073f-0920-41aa-8dd3-2ea59b18f5ce\r\r\n\r\r\n\r\r\n\r\r\n", "wmic /?" => "" } # Mocking wmic command to simulate its availability
plat = mock_platform("windows", commands)
_(plat.uuid).must_equal "d400073f-0920-41aa-8dd3-2ea59b18f5ce"
end
Expand Down