File: os_windows.rb

package info (click to toggle)
ruby-train 3.16.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,272 kB
  • sloc: ruby: 10,507; sh: 17; makefile: 8
file content (310 lines) | stat: -rw-r--r-- 12,559 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
module Train::Platforms::Detect::Helpers
  module Windows
    def detect_windows
      check_cmd || check_powershell
    end

    def check_cmd
      # try to detect windows, use cmd.exe to also support Microsoft OpenSSH
      res = @backend.run_command("cmd.exe /c ver")

      return false if (res.exit_status != 0) || res.stdout.empty?

      # if the ver contains `Windows`, we know its a Windows system
      version = res.stdout.strip
      return false unless version.downcase =~ /windows/

      @platform[:family] = "windows"

      # try to extract release from eg. `Microsoft Windows [Version 6.3.9600]`
      release = /\[(?<name>.*)\]/.match(version)
      if release[:name]
        # release is 6.3.9600 now
        @platform[:release] = release[:name].downcase.gsub("version", "").strip
        # fallback, if we are not able to extract the name from wmic later
        @platform[:name] = "Windows #{@platform[:release]}"
      end

      # Prefer retrieving the OS details via `wmic` if available on the system to retain existing behavior.
      # If `wmic` is not available, fall back to using cmd-only commands as an alternative method.
      wmic_available? ? read_wmic : read_cmd_os
      true
    end

    def check_powershell
      command = @backend.run_command(
        "Get-WmiObject Win32_OperatingSystem | Select Caption,Version | ConvertTo-Json"
      )
      # some targets (e.g. Cisco) may return 0 and print an error to stdout
      return false if (command.exit_status != 0) || command.stdout.downcase !~ /window/

      begin
        payload = JSON.parse(command.stdout)
        @platform[:family] = "windows"
        @platform[:release] = payload["Version"]
        @platform[:name] = payload["Caption"]

        # Prefer retrieving the OS details via `wmic` if available on the system to retain existing behavior.
        # If `wmic` is not available, fall back to using CIM as an alternative method.
        wmic_available? ? read_wmic : read_cim_os
        true
      rescue
        false
      end
    end

    def local_windows?
      @backend.class.to_s == "Train::Transports::Local::Connection" &&
        ruby_host_os(/mswin|mingw|windows/)
    end

    # reads os name and version from wmic
    # @see https://msdn.microsoft.com/en-us/library/bb742610.aspx#EEAA
    # Thanks to Matt Wrock (https://github.com/mwrock) for this hint
    def read_wmic
      res = @backend.run_command("wmic os get * /format:list")
      if res.exit_status == 0
        sys_info = {}
        res.stdout.lines.each do |line|
          m = /^\s*([^=]*?)\s*=\s*(.*?)\s*$/.match(line)
          sys_info[m[1].to_sym] = m[2] unless m.nil? || m[1].nil?
        end

        @platform[:release] = sys_info[:Version]
        # additional info on windows
        @platform[:build] = sys_info[:BuildNumber]
        @platform[:name] = sys_info[:Caption]
        @platform[:name] = @platform[:name].gsub("Microsoft", "").strip unless @platform[:name].empty?
        @platform[:arch] = read_wmic_cpu
      end
    end

    # `OSArchitecture` from `read_wmic` does not match a normal standard
    # For example, `x86_64` shows as `64-bit`
    def read_wmic_cpu
      res = @backend.run_command("wmic cpu get architecture /format:list")
      if res.exit_status == 0
        sys_info = {}
        res.stdout.lines.each do |line|
          m = /^\s*([^=]*?)\s*=\s*(.*?)\s*$/.match(line)
          sys_info[m[1].to_sym] = m[2] unless m.nil? || m[1].nil?
        end
      end

      # This converts `wmic os get architecture` output to a normal standard
      # https://msdn.microsoft.com/en-us/library/aa394373(VS.85).aspx
      arch_map = {
        0 => "i386",
        1 => "mips",
        2 => "alpha",
        3 => "powerpc",
        5 => "arm",
        6 => "ia64",
        9 => "x86_64",
      }

      # The value of `wmic cpu get architecture` is always a number between 0-9
      arch_number = sys_info[:Architecture].to_i
      arch_map[arch_number]
    end

    # This method scans the target os for a unique uuid to use
    def windows_uuid
      uuid = windows_uuid_from_chef
      uuid = windows_uuid_from_machine_file if uuid.nil?
      uuid = windows_uuid_from_wmic_or_cim if uuid.nil?
      uuid = windows_uuid_from_registry if uuid.nil?
      raise Train::TransportError, "Cannot find a UUID for your node." if uuid.nil?

      uuid
    end

    def windows_uuid_from_machine_file
      %W{
        #{ENV["SYSTEMDRIVE"]}\\chef\\chef_guid
        #{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}\\.chef\\chef_guid
      }.each do |path|
        file = @backend.file(path)
        return file.content.chomp if file.exist? && file.size != 0
      end
      nil
    end

    def windows_uuid_from_chef
      file = @backend.file("#{ENV["SYSTEMDRIVE"]}\\chef\\cache\\data_collector_metadata.json")
      return if !file.exist? || file.size == 0

      json = JSON.parse(file.content)
      json["node_uuid"]
    end

    def windows_uuid_from_wmic_or_cim
      # Retrieve the Windows UUID using `wmic` if it is available and not marked as deprecated, maintaining compatibility with older systems.
      # If `wmic` is unavailable or deprecated, use the `Get-CimInstance` command, which is the modern and recommended approach by Microsoft.
      wmic_available? ? windows_uuid_from_wmic : windows_uuid_from_cim
    end

    def windows_uuid_from_wmic
      # 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&.match(/UUID=([A-F0-9\-]+)/i)&.captures&.first
    end

    def windows_uuid_from_registry
      cmd = '(Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography" -Name "MachineGuid")."MachineGuid"'
      result = @backend.run_command(cmd)
      return unless result.exit_status == 0

      result.stdout.chomp
    end

    # Checks if `wmic` is available and not deprecated
    def wmic_available?
      # Return memoized value if already checked
      return @wmic_available unless @wmic_available.nil?

      # Runs the `wmic /?`` command, which provides help information for the WMIC (Windows Management Instrumentation Command-line) tool.
      # It displays a list of available global switches and aliases, as well as details about their usage.
      # The output also includes information about deprecated status for the 'wmic' tool.
      result = @backend.run_command("wmic /?")

      # Check if command ran successfully and output does not contain 'wmic is deprecated'
      @wmic_available = result.exit_status == 0 && !(result.stdout.downcase.include?("wmic is deprecated"))
    end

    def read_cim_os
      cmd = 'powershell -Command "Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, BuildNumber | ConvertTo-Json"'
      res = @backend.run_command(cmd)
      return unless res.exit_status == 0

      begin
        sys_info = JSON.parse(res.stdout)
        @platform[:release] = sys_info["Version"]
        @platform[:build] = sys_info["BuildNumber"]
        @platform[:name] = sys_info["Caption"]
        @platform[:name] = @platform[:name].gsub("Microsoft", "").strip unless @platform[:name].empty?
        @platform[:arch] = read_cim_cpu
      rescue
        nil
      end
    end

    def read_cim_cpu
      cmd = 'powershell -Command "(Get-CimInstance Win32_Processor).Architecture"'
      res = @backend.run_command(cmd)
      return unless res.exit_status == 0

      arch_map = {
        0 => "i386",
        1 => "mips",
        2 => "alpha",
        3 => "powerpc",
        5 => "arm",
        6 => "ia64",
        9 => "x86_64",
      }

      arch_map[res.stdout.strip.to_i]
    end

    # Fallback method for reading OS info using cmd-only commands when wmic is not available
    def read_cmd_os
      # Try to get architecture from PROCESSOR_ARCHITECTURE environment variable
      # This covers the same architectures as wmic CPU detection but uses environment variables
      # which are available on all Windows versions since NT
      arch_res = @backend.run_command("echo %PROCESSOR_ARCHITECTURE%")
      if arch_res.exit_status == 0
        arch_string = arch_res.stdout.strip.downcase
        # Only set architecture if we got actual output
        unless arch_string.empty?
          @platform[:arch] = case arch_string
                            when "x86"
                              "i386"
                            when "amd64", "x64"
                              "x86_64"
                            when "ppc", "powerpc"
                              "powerpc"
                            else
                              # For any unknown architecture, preserve the original value
                              # This handles: arm64, ia64, arm, mips, alpha, and future architectures
                              arch_string
                             end
        end
      end
      # If PROCESSOR_ARCHITECTURE fails, architecture remains unset (consistent with other methods)

      # Try to get more detailed OS info from systeminfo command as fallback
      # This is slower than wmic but works without PowerShell
      # Only override the basic info from check_cmd if systeminfo provides better data
      sysinfo_res = @backend.run_command("systeminfo")
      if sysinfo_res.exit_status == 0
        sysinfo_res.stdout.lines.each do |line|
          line = line.strip
          if line =~ /^OS Name:\s*(.+)$/i
            os_name = $1.strip
            # Only override if we get a more detailed name than the basic "Windows X.X.X" from check_cmd
            detailed_name = os_name.gsub("Microsoft", "").strip
            @platform[:name] = detailed_name unless detailed_name.empty?
          elsif line =~ /^OS Version:\s*(.+)$/i
            version_info = $1.strip
            # Extract version number from format like "10.0.19044 N/A Build 19044"
            if version_info =~ /^(\d+\.\d+\.\d+)/
              # Only override release if systeminfo provides the same or more detailed version
              systeminfo_release = $1
              @platform[:release] = systeminfo_release if systeminfo_release
            end
            # Extract build number (this is additional info not available from check_cmd)
            if version_info =~ /Build (\d+)/
              @platform[:build] = $1
            end
          end
        end
      end
      # If systeminfo fails, we keep the basic info from check_cmd method
    end

    def windows_uuid_from_cim
      cmd = 'powershell -Command "(Get-CimInstance -Class Win32_ComputerSystemProduct).UUID"'
      res = @backend.run_command(cmd)
      return unless res.exit_status == 0

      res.stdout.strip
    end

  end
end