compute_unit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'compute_unit/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'compute_unit'
9
+ spec.version = ComputeUnit::VERSION
10
+ spec.authors = ['Corey Osman']
11
+ spec.email = ['opselite@blockops.party']
12
+ spec.required_ruby_version = '>= 2.5'
13
+ spec.summary = 'A ruby library for compute unit devices'
14
+ spec.description = <<~EOF
15
+
16
+ A ruby library that searches uses the linux sysfs file system for compute unit devices such as
17
+ CPUS, GPUs and other ASIC compute devices. Allows programmatic access collect real time metrics from the kernel or relatated driver toolchain.
18
+ Is meant to be used as a toolchain to future build tooling on. This library also makes use of opencl toolchain and requires
19
+ the opencl_ruby_ffi gem.
20
+
21
+ EOF
22
+
23
+ spec.homepage = 'https://gitlab.com/blockops/compute_unit'
24
+ spec.license = 'MIT'
25
+
26
+ spec.metadata['homepage_uri'] = spec.homepage
27
+ spec.metadata['source_code_uri'] = spec.homepage
28
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/CHANGELOG"
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = 'exe'
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ['lib']
38
+
39
+ spec.add_dependency 'opencl_ruby_ffi', '~> 1.3.0'
40
+ spec.add_development_dependency 'bundler', '~> 2.0'
41
+ spec.add_development_dependency 'rake', '~> 10.0'
42
+ spec.add_development_dependency 'rspec', '~> 3.0'
43
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'compute_unit'
4
+ require 'compute_unit/gpu'
5
+ require 'json'
6
+
7
+ begin
8
+ opencl = !!ARGV.first
9
+ puts JSON.pretty_generate(ComputeUnit.find_all_with_database(opencl))
10
+ rescue Errno::EACCES => e
11
+ puts e.message
12
+ end
13
+
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'compute_unit'
4
+
5
+ begin
6
+ path = ComputeUnit.refresh_pci_database
7
+ puts "#{path} udpated to latest pci database."
8
+ rescue Errno::EACCES => e
9
+ puts e.message
10
+ end
11
+
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compute_unit/utils'
4
+ require 'compute_unit/version'
5
+ require 'compute_unit/exceptions'
6
+ require 'compute_unit/monkey_patches'
7
+
8
+ module ComputeUnit
9
+ CACHE_DIR = File.join('/var', 'run', 'compute_unit_cache')
10
+ SYSFS_PATH = ENV['SYSFS_PATH'] || '/sys'
11
+ SYS_DEVICE_PATH = File.join(SYSFS_PATH, 'bus/pci/devices')
12
+ PCI_DATABASE_PATH = File.join(File.dirname(__dir__), 'pci.ids')
13
+ PCI_DATABASE_URL = 'http://pci-ids.ucw.cz/v2.2/pci.ids'
14
+
15
+ def self.find_all(use_opencl = false)
16
+ require 'compute_unit/gpu'
17
+ require 'compute_unit/cpu'
18
+ require 'compute_unit/asic'
19
+ raise Exceptions::InvalidPCIDatabase.new('Run: ComputeUnit.refresh_pci_database') unless File.exist?(PCI_DATABASE_PATH)
20
+
21
+ ComputeUnit::Gpu.find_all(use_opencl) +
22
+ ComputeUnit::Cpu.find_all +
23
+ ComputeUnit::Asic.find_all
24
+ end
25
+
26
+ def self.find_all_with_database(use_opencl = false)
27
+ refresh_pci_database
28
+ find_all(use_opencl)
29
+ end
30
+
31
+ def self.refresh_pci_database
32
+ ComputeUnit::Utils.check_for_root
33
+ require 'net/http'
34
+ require 'uri'
35
+ uri = URI.parse(PCI_DATABASE_URL)
36
+ response = Net::HTTP.get_response(uri)
37
+ # I can't write to it unless it has correct permissions
38
+ File.chmod(0o644, PCI_DATABASE_PATH) if File.exist?(PCI_DATABASE_PATH)
39
+ File.write(PCI_DATABASE_PATH, response.body) if response.code == '200'
40
+ File.chmod(0o644, PCI_DATABASE_PATH)
41
+ PCI_DATABASE_PATH
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compute_unit/compute_base'
4
+ module ComputeUnit
5
+ class Asic < ComputeBase
6
+ DEVICE_CLASS_NAME = 'Asic'
7
+
8
+ def self.find_all
9
+ []
10
+ end
11
+
12
+ # /sys/devices/platform $ more coretemp.0/hwmon/hwmon1/temp1_input / 1000
13
+ end
14
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compute_unit/logger'
4
+ require 'fileutils'
5
+ require 'yaml/store'
6
+
7
+ module ComputeUnit
8
+ class CacheStore
9
+ attr_accessor :name
10
+ include ComputeUnit::Logger
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ FileUtils.mkdir_p(File.dirname(cache_file))
15
+ FileUtils.mkdir_p(cache_files_dir)
16
+ end
17
+
18
+ def cache_files_dir
19
+ File.join(File.dirname(cache_file), 'files')
20
+ end
21
+
22
+ # @return [YAML::Store]
23
+ def cache_store
24
+ @cache_store ||= ::YAML::Store.new(cache_file)
25
+ end
26
+
27
+ # @return [String] - the file where the cache is stored, different per user, due to permissions issue
28
+ def cache_file
29
+ if Etc.getpwuid.name == 'root'
30
+ File.expand_path(File.join(ComputeUnit::CACHE_DIR, name, "#{name}.db"))
31
+ else
32
+ File.expand_path(File.join('~', '.compute_unit-cache', name, "#{name}.db"))
33
+ end
34
+ end
35
+
36
+ # @return [Object] - returns the stored object given the key, defaults to nil when not found unless specified
37
+ # @param key [String] - the key to retrieve
38
+ # @param default [Object] - the default value if no data is found
39
+ def read_cache(key, default = nil)
40
+ return default unless key
41
+
42
+ begin
43
+ cache_store.transaction do
44
+ cache_store.fetch(key, default)
45
+ end
46
+ rescue StandardError => e
47
+ logger.debug("Error reading from cache with key #{key}: #{e.message}")
48
+ default
49
+ end
50
+ end
51
+
52
+ # @return [?] -
53
+ # @param key [String] - the key to retrieve
54
+ # param value [Object] - the object to store in the cache
55
+ def write_cache(key, value)
56
+ return if ENV['XB_DISABLE_CACHE'] =~ /yes|true/i
57
+
58
+ begin
59
+ cache_store.transaction do
60
+ cache_store[key] = value
61
+ end
62
+ rescue StandardError => e
63
+ logger.debug("Error writing to cache with key #{key}: #{e.message}")
64
+ end
65
+ end
66
+
67
+ def memory_cache
68
+ @memory_cache ||= {}
69
+ end
70
+
71
+ def write_memcache(key, value)
72
+ logger.debug("Caching content with key : #{key}")
73
+ memory_cache[key] = {
74
+ content: value,
75
+ ttl: Time.now.to_i
76
+ }
77
+ value
78
+ end
79
+
80
+ def memory_cache_lookup(key, ttl = 300)
81
+ return unless ttl.positive?
82
+
83
+ logger.debug("Looking up memory cache for #{key}")
84
+ hexkey = Digest::SHA1.hexdigest(key)
85
+ cached_item = memory_cache[hexkey]
86
+ return nil unless cached_item && cached_item[:content]
87
+
88
+ timestamp = cached_item[:ttl] + ttl
89
+ expired = Time.now.to_i
90
+ if timestamp > expired
91
+ logger.debug("Memory cache hit #{hexkey}")
92
+ begin
93
+ cached_item[:content]
94
+ rescue JSON::ParserError
95
+ nil
96
+ end
97
+ else
98
+ logger.debug("Cleaning cache for #{hexkey}")
99
+ memory_cache.delete(hexkey)
100
+ nil
101
+ end
102
+ end
103
+
104
+ # @return [Object] - return the parsed json object or nil only if the item ttl has not expired
105
+ # @param key [String] - the url of request
106
+ # @param ttl [Integer] - time in seconds to keep the request for
107
+ def read_json_cache_file(key, ttl = 300)
108
+ logger.debug("Looking up cache for #{key}")
109
+ hexkey = Digest::SHA1.hexdigest(key)
110
+ cache_file = File.join(cache_files_dir, "#{hexkey}.json")
111
+ return nil unless cache_file
112
+ return nil unless File.exist?(cache_file)
113
+
114
+ timestamp = File.mtime(cache_file).to_i + ttl
115
+ expired = Time.now.to_i
116
+ if timestamp > expired
117
+ logger.debug("Cache hit #{hexkey}")
118
+ begin
119
+ JSON.parse(File.read(cache_file))
120
+ rescue JSON::ParserError
121
+ nil
122
+ end
123
+ else
124
+ logger.debug("Cleaning cache for #{hexkey}")
125
+ FileUtils.rm(cache_file)
126
+ nil
127
+ end
128
+ end
129
+
130
+ # @return [?] -
131
+ # @param key [String] - the key to retrieve
132
+ # @param value [Object] - the object to store in the cache in json format
133
+ def write_json_cache_file(key, value)
134
+ return if ENV['XB_DISABLE_CACHE'] =~ /yes|true/i
135
+
136
+ logger.debug("Caching content with key : #{key}")
137
+ cache_file = File.join(cache_files_dir, "#{key}.json")
138
+ File.open(cache_file, 'w') do |f|
139
+ f.write(value)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'compute_unit/formatters'
5
+ require 'compute_unit/logger'
6
+ require 'compute_unit/device'
7
+
8
+ module ComputeUnit
9
+ class ComputeBase < Device
10
+ attr_reader :type, :serial, :meta, :uuid, :timestamp, :index, :compute_type
11
+ attr_accessor :power_offset
12
+
13
+ include ComputeUnit::Formatters
14
+ include ComputeUnit::Logger
15
+
16
+ # timeout value
17
+ CACHE_TIMEOUT = 30
18
+
19
+ # @param value [Float] a value to offset the power calculation, either a whole number, or decimal
20
+ # @return [Integer] the value set as the offset
21
+ def power_offset=(value)
22
+ @power_offset = value.is_a?(Float) && value.abs < 1 ? (value * power).round(0) : value.to_i
23
+ # Power offset by #{@power_offset} watts for #{compute_type}#{index},
24
+ # future calculations will include this offset")
25
+ end
26
+
27
+ def initialize(device_path, opts = {})
28
+ super
29
+ @timestamp = Time.now.to_i
30
+ end
31
+
32
+ def device_class_name
33
+ self.class.const_get('DEVICE_CLASS_NAME')
34
+ end
35
+
36
+ # @return [Array] - an array of pci bus device locations (every device on the pci bus)
37
+ # @note there is not a filter applied
38
+ def self.devices
39
+ Dir.glob(File.join(ComputeUnit::Device::SYSFS_DEVICES_PATH, '*'))
40
+ end
41
+
42
+ def expired_metadata?
43
+ (timestamp + CACHE_TIMEOUT) < Time.now.to_i
44
+ end
45
+
46
+ def experimental_on?
47
+ unless ENV['XB_EXPERIMENTAL'].to_s == '1'
48
+ logger.warn('You must set environment variable XB_EXPERIMENTAL=1 to use this feature')
49
+ return false
50
+ end
51
+ true
52
+ end
53
+
54
+ def micro_formatter(item, add_unit = false)
55
+ data = {}
56
+ item.each do |key, value|
57
+ if %i[hourly_cost hourly_earnings kwh_cost].include?(key)
58
+ v = (value * 1_000_000).round(4)
59
+ data[key] = add_unit ? "#{v} \u00B5BTC" : v
60
+ end
61
+ end
62
+ item.merge(data)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compute_unit/compute_base'
4
+
5
+ module ComputeUnit
6
+ class Cpu < ComputeBase
7
+ DEVICE_CLASS = '060000'
8
+ DEVICE_CLASS_NAME = 'CPU'
9
+ # @return [Array] - returns a list of device paths of all devices considered for display
10
+ def self.devices
11
+ ComputeUnit::ComputeBase.devices.find_all do |device|
12
+ ComputeUnit::Device.device_class(device) == DEVICE_CLASS
13
+ end
14
+ end
15
+
16
+ def self.create_from_path(device_path, index)
17
+ opts = {
18
+ device_class_id: device_class(device_path),
19
+ device_id: device(device_path),
20
+ device_vendor_id: device_vendor(device_path),
21
+ subsystem_vendor_id: subsystem_vendor(device_path),
22
+ subsystem_device_id: subsystem_device(device_path),
23
+ index: index
24
+ }
25
+ new(device_path, opts)
26
+ end
27
+
28
+ def self.find_all
29
+ return [] # not quite ready to show cpu data
30
+ devices.sort.map.with_index do |device_path, index|
31
+ create_from_path(device_path, index)
32
+ end
33
+ end
34
+ # /sys/devices/platform $ more coretemp.0/hwmon/hwmon1/temp1_input / 1000
35
+ end
36
+ end
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'compute_unit/logger'
4
+ require 'compute_unit/cache_store'
5
+ require 'compute_unit/utils'
6
+
7
+ # This file supports reading from sysfs
8
+ # More information about sysfs can be found here - https://www.kernel.org/doc/Documentation/filesystems/sysfs-pci.txt
9
+ module ComputeUnit
10
+ class Device
11
+ # We can supply a mock sysfs path in order to test with containers and other scenarios
12
+ SYSFS_DEVICES_PATH = File.join(ComputeUnit::SYSFS_PATH, 'bus', 'pci', 'devices')
13
+
14
+ attr_reader :device_class_id,
15
+ :device_id,
16
+ :device_vendor_id,
17
+ :subsystem_vendor_id,
18
+ :subsystem_device_id,
19
+ :device_path,
20
+ :make,
21
+ :model,
22
+ :vendor
23
+
24
+ include ComputeUnit::Utils
25
+
26
+ def initialize(device_path, opts = {})
27
+ @device_path = device_path
28
+ @device_class_id = opts[:device_class_id]
29
+ @device_id = opts[:device_id]
30
+ @device_vendor_id = opts[:device_vendor_id]
31
+ @subsystem_vendor_id = opts[:subsystem_vendor_id]
32
+ @subsystem_device_id = opts[:subsystem_device_id]
33
+ end
34
+
35
+ def to_h
36
+ { chip_make: make, make: vendor, model: model,
37
+ device_id: device_id, vendor_id: device_vendor_id,
38
+ subsystem_device_id: subsystem_device_id,
39
+ subsystem_vendor_id: subsystem_vendor_id,
40
+ device_class: device_class_id }
41
+ end
42
+
43
+ # @return [String] - the path to the rom file if available
44
+ def rom_path
45
+ @rom_path ||= File.join(device_path, 'rom')
46
+ end
47
+
48
+ # @return [String] - writes a 1 to the rom file, therby unlocking it for reading
49
+ # must be root
50
+ def unlock_rom
51
+ File.write(rom_path, '1') if File.exist?(rom_path)
52
+ end
53
+
54
+ # @return [String] - writes a 0 to the rom file, therby locking it
55
+ # must be root
56
+ def lock_rom
57
+ File.write(rom_path, '0') if File.exist?(rom_path)
58
+ end
59
+
60
+ # @return [String::IO] - the contents of the rom file
61
+ def rom_data
62
+ return unless File.exist?(rom_path)
63
+
64
+ begin
65
+ unlock_rom
66
+ IO.read(rom_path, mode: 'rb')
67
+ ensure
68
+ lock_rom
69
+ end
70
+ end
71
+
72
+ # @return [String] - read a kernel setting using the device_path
73
+ # @param [String] - the name of the kernel file in the device path to read
74
+ # @param [Object] - a default value to use if there is a problem reading the setting
75
+ def read_kernel_setting(setting, default = 0)
76
+ self.class.read_kernel_setting(device_path, setting, default)
77
+ end
78
+
79
+ # @return [String] - a reading of the kernel setting using the device_path
80
+ # @param value [String] - the value to assign to the setting
81
+ # @param setting [String] - the name of the kernel file in the device path to read
82
+ def write_kernel_setting(setting, value)
83
+ self.class.write_kernel_setting(device_path, setting, value)
84
+ end
85
+
86
+ # @return [String] - the name of the vendor who made the device
87
+ def make
88
+ @make ||= begin
89
+ name = self.class.vendor_lookup(device_vendor_id)
90
+ name[/\[?(.*)\]?/, 1] if name
91
+ end
92
+ end
93
+
94
+ # @return [String] - the name of the vendor who resold the device
95
+ def vendor
96
+ @vendor ||= begin
97
+ if device_vendor_id == subsystem_vendor_id
98
+ make
99
+ else
100
+ name = self.class.subsystem_vendor_lookup(subsystem_vendor_id)
101
+ name[/\[?(.*)\]?/, 1] if name
102
+ end
103
+ end
104
+ end
105
+
106
+ # @return [String] - the name of the device model (specific name)
107
+ def sysfs_model_name
108
+ name = self.class.subsystem_device_lookup(device_id, subsystem_device_id, subsystem_vendor_id)
109
+ m = name[/\[?(.*)\]?/, 1] if name
110
+ m || generic_model
111
+ end
112
+
113
+ # @return [String] - the name of the device model (specific name)
114
+ def model
115
+ @model ||= begin
116
+ sysfs_model_name
117
+ end
118
+ end
119
+
120
+ # @return [String] - the name of the device model (sometimes not specific)
121
+ def generic_model
122
+ @generic_model ||= begin
123
+ name = self.class.device_lookup(device_id)
124
+ name[/\[?(.*)\]?/, 1] if name
125
+ end
126
+ end
127
+
128
+ def read_file(path, default = nil)
129
+ File.read(path).chomp
130
+ rescue Errno::EINVAL, Errno::EPERM, Errno::ENOENT
131
+ default
132
+ rescue Errno::EACCES
133
+ logger.fatal('run this command as root or with sudo, using default value')
134
+ default
135
+ end
136
+
137
+ # @param item [String] - the name of the hwmon file to read from
138
+ # @param default [Object] - the default value to return if the file is empty or not readable
139
+ # @return [String] - the value of the item looked up
140
+ def read_hwmon_data(item, _default = nil)
141
+ path = File.join(hwmon_path, item)
142
+ read_file(path)
143
+ end
144
+
145
+ # @param item [String] - the name of the hwmon file to write to
146
+ # @param value [String] - the value you want to write to the hwmon file
147
+ def write_hwmon_data(item, value)
148
+ File.write(File.join(hwmon_path, item), value)
149
+ rescue Errno::EACCES => e
150
+ logger.info(e.message)
151
+ check_for_root
152
+ end
153
+
154
+ # @return [String] - base hwmon path of the device
155
+ # @note this is used mainly for easier mocking
156
+ def base_hwmon_path
157
+ File.join(device_path, 'hwmon')
158
+ end
159
+
160
+ # @return [String] - the directory path to the hwmon dir for this device
161
+ # @note this path can be different for each device
162
+ # @note returns the base_hwmon_path if no path is found
163
+ def hwmon_path
164
+ @hwmon_path ||= begin
165
+ paths = Dir.glob(File.join(base_hwmon_path, '*'))
166
+ paths.first || base_hwmon_path
167
+ end
168
+ end
169
+
170
+ def expired_metadata?
171
+ timestamp + CACHE_TIMEOUT < Time.now.to_i
172
+ end
173
+
174
+ def to_json(c = nil)
175
+ to_h.to_json(c)
176
+ end
177
+
178
+ # @return [Device] - creates a device from the given path
179
+ # @param device_path [String] - the sysfs path to of the device
180
+ def self.create_from_path(device_path)
181
+ opts = {
182
+ device_class_id: device_class(device_path),
183
+ device_id: device(device_path),
184
+ device_vendor_id: device_vendor(device_path),
185
+ subsystem_vendor_id: subsystem_vendor(device_path),
186
+ subsystem_device_id: subsystem_device(device_path)
187
+ }
188
+ new(device_path, opts)
189
+ end
190
+
191
+ # @return [Array] - an array of pci bus device locations (every device on the pci bus)
192
+ # @param [String] - the device class for filtering out devices
193
+ # @note there is not a filter applied
194
+ def self.find_all(device_class_id = nil)
195
+ Dir.glob(File.join(ComputeUnit::Device::SYSFS_DEVICES_PATH, '*')).map do |device_path|
196
+ next create_from_path(device_path) unless device_class_id
197
+ next create_from_path(device_path) if device_class(device_path) == device_class_id
198
+ end.compact
199
+ end
200
+
201
+ # @return [String] - translation of vendor names to how we known them
202
+ def self.name_translation(name)
203
+ name_map[name] || name
204
+ end
205
+
206
+ # @return [Hash] - Name translation map
207
+ # sometimes we just wamnt a shorter name to see
208
+ def self.name_map
209
+ @name_map ||= {
210
+ 'ASUSTeK COMPUTER INC.' => 'Asus',
211
+ 'ASUSTeK Computer Inc.' => 'Asus',
212
+ 'Advanced Micro Devices, Inc. [AMD/ATI]' => 'AMD',
213
+ 'XFX Pine Group Inc.' => 'XFX',
214
+ 'NVIDIA Corporation' => 'Nvidia',
215
+ 'Gigabyte Technology Co., Ltd' => 'Gigabyte',
216
+ 'Sapphire Technology Limited' => 'Sapphire',
217
+ 'eVga.com. Corp.' => 'Evga',
218
+ 'Micro-Star International Co., Ltd. [MSI]' => 'MSI',
219
+ 'Micro-Star International Co., Ltd.' => 'MSI',
220
+ 'Intel Corporation' => 'Intel',
221
+ 'ASMedia Technology Inc.' => 'AsMedia Tech',
222
+ 'Advanced Micro Devices, Inc. [AMD]' => 'AMD',
223
+ 'Tul Corporation / PowerColor' => 'PowerColor',
224
+ 'PC Partner Limited / Sapphire Technology' => 'Sapphire',
225
+ 'Realtek Semiconductor Co., Ltd.' => 'Realtek',
226
+ 'Samsung Electronics Co Ltd' => 'Samsung',
227
+ 'ZOTAC International (MCO) Ltd.' => 'Zotac'
228
+ }
229
+ end
230
+
231
+ # @return [String] - the device number ie. 1002 (can be different from vendor)
232
+ def self.subsystem_device(device_path)
233
+ read_kernel_setting(device_path, 'subsystem_device', '').slice(2, 6)
234
+ end
235
+
236
+ # @return [String] - the vendor number ie. 1002 (can be different from vendor)
237
+ # example: XFX is the reseller of the AMD chips
238
+ def self.subsystem_vendor(device_path)
239
+ read_kernel_setting(device_path, 'subsystem_vendor', '').slice(2, 6)
240
+ end
241
+
242
+ # @return [String] - the device number ie. 1002
243
+ def self.device(device_path)
244
+ read_kernel_setting(device_path, 'device', '').slice(2, 6)
245
+ end
246
+
247
+ # @return [String] - the vendor number ie. 1002
248
+ def self.device_vendor(device_path)
249
+ read_kernel_setting(device_path, 'vendor', '').slice(2, 6)
250
+ end
251
+
252
+ # @return [String] - the class number ie. 040300
253
+ # @note helps determine what kind of device this is ie. graphics, storage, ...
254
+ def self.device_class(device_path)
255
+ read_kernel_setting(device_path, 'class', '').slice(2, 8)
256
+ end
257
+
258
+ # @return [String] - read a kernel setting using the device_path
259
+ # @param [String] - the device_path to read from
260
+ # @param [String] - the name of the kernel file in the device path to read
261
+ # @param [Object] - a default value to use if there is a problem reading the setting
262
+ def self.read_kernel_setting(device_path, setting, default = 0)
263
+ path = File.join(device_path, setting)
264
+ logger.debug("reading kernel file #{path}")
265
+ value = begin
266
+ File.read(path).chomp
267
+ rescue Errno::EINVAL, Errno::EPERM
268
+ logger.fatal(e.message)
269
+ default
270
+ rescue Errno::ENOENT
271
+ logger.fatal("File #{path} does not exist")
272
+ default
273
+ rescue Errno::EACCES
274
+ logger.fatal('Run this command as root or with sudo')
275
+ default
276
+ end
277
+ end
278
+
279
+ # @return [String] - read a kernel setting using the device_path
280
+ # @param [String] - the device_path to write to
281
+ # @param [String] - the name of the kernel file in the device path to read
282
+ def self.write_kernel_setting(device_path, setting, value)
283
+ path = File.join(device_path, setting)
284
+ File.write(path, value)
285
+ read_kernel_setting(device_path, setting)
286
+ rescue Errno::EINVAL, Errno::EPERM => e
287
+ logger.fatal(e.message)
288
+ rescue Errno::ENOENT
289
+ logger.fatal("File #{path} does not exist")
290
+ rescue Errno::EACCES
291
+ logger.fatal('Run this command as root or with sudo')
292
+ end
293
+
294
+ # @return [String] - a sha1 digest that represents the pci devices found on the system
295
+ def self.system_checksum
296
+ @system_checksum ||= Dir.chdir(ComputeUnit::SYS_DEVICE_PATH) do
297
+ Digest::SHA1.hexdigest(Dir.glob('*').sort.join)
298
+ end
299
+ end
300
+
301
+ # Syntax:
302
+ # vendor vendor_name
303
+ # device device_name <-- single tab
304
+ # subvendor subdevice subsystem_name <-- two tabs
305
+ # @return [Array] - array of lines of the pci database
306
+ def self.pci_database
307
+ @pci_database ||= begin
308
+ IO.foreach(ComputeUnit::PCI_DATABASE_PATH).lazy
309
+ end
310
+ end
311
+
312
+ def self.manual_device_database
313
+ @manual_device_database ||= {
314
+ '687f_0b36_1002' => 'Radeon RX Vega 64'
315
+ }
316
+ end
317
+
318
+ def self.manual_device_lookup(device_id, subsystem_device_id, vendor_id)
319
+ key = "#{device_id}_#{subsystem_device_id}_#{vendor_id}"
320
+ manual_device_database.fetch(key, nil)
321
+ end
322
+
323
+ # @return [String] - the name of the device
324
+ # @param device_id [String] - the device id of the device
325
+ # @param subsystem_device_id [String] - the subsystem_device_id of the device
326
+ # @param vendor_id [String] - the subsystem vendor id
327
+ def self.subsystem_device_lookup(device_id, subsystem_device_id, vendor_id)
328
+ # "\t\t1002 687f Vega 10 XT [Radeon RX Vega 64]\n"
329
+ p = manual_device_lookup(device_id, subsystem_device_id, vendor_id)
330
+ return p if p
331
+
332
+ re = Regexp.new(/\A\t\t#{vendor_id}\s+#{subsystem_device_id}/)
333
+ d = pci_database.find do |line|
334
+ re.match(line)
335
+ rescue ArgumentError
336
+ next
337
+ end
338
+ return d unless d
339
+
340
+ name = d[/\t\t\w+\s+\w+\s+(.*)\n/, 1]
341
+ name[/.*\[(.*)\]/, 1] || name if name
342
+ end
343
+
344
+ # @return [String] - the name of the subsystem vendor with a possible translation
345
+ def self.subsystem_vendor_lookup(vendor_id)
346
+ # "1002 Advanced Micro Devices, Inc. [AMD/ATI]\n"
347
+ vendor_lookup(vendor_id)
348
+ end
349
+
350
+ # @return [Hash] - a hash of vendors names and ids
351
+ def self.manual_vendors
352
+ @manual_vendors ||= { '196e' => 'PNY' }
353
+ end
354
+
355
+ # @return [String] - return the string of the device vendor
356
+ # @param id [String] - the vendor id
357
+ def self.manual_vendor_lookup(id)
358
+ manual_vendors[id]
359
+ end
360
+
361
+ # @return [String] - the name of the subsystem vendor with a possible translation
362
+ def self.vendor_lookup(vendor_id)
363
+ # "1002 Advanced Micro Devices, Inc. [AMD/ATI]\n"
364
+ re = Regexp.new(/\A#{vendor_id}/)
365
+ d = pci_database.find do |line|
366
+ re.match(line)
367
+ rescue ArgumentError
368
+ next
369
+ end
370
+ return manual_vendor_lookup(vendor_id) unless d
371
+
372
+ name = d[/\w+\s+(.*)\n/, 1]
373
+ name_translation(name)
374
+ end
375
+
376
+ # @param [String] - the device id such as '686f'
377
+ # queries the pci_database that is shipped with this gem and looks up the device
378
+ # @return [String] - the device name
379
+ def self.device_lookup(device_id)
380
+ # "\t687f Vega 10 XT [Radeon RX Vega 64]\n"
381
+ re = Regexp.new(/\A\t#{device_id}/)
382
+ d = pci_database.find do |line|
383
+ re.match(line)
384
+ rescue ArgumentError
385
+ next
386
+ end
387
+ return d unless d
388
+
389
+ name = d[/\t\w+\s+(.*)\n/, 1]
390
+ name[/.*\[(.*)\]/, 1] || name if name
391
+ end
392
+
393
+ def self.logger
394
+ ComputeUnit::Logger.logger
395
+ end
396
+ end
397
+ end