compute_unit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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