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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.gitlab-ci.yml +45 -0
- data/.rspec +3 -0
- data/.rubocop.yml +12 -0
- data/.rubocop_todo.yml +139 -0
- data/.ruby_version +1 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/compute_unit.gemspec +43 -0
- data/exe/list_computes +13 -0
- data/exe/update_pcidb +11 -0
- data/lib/compute_unit.rb +43 -0
- data/lib/compute_unit/asic.rb +14 -0
- data/lib/compute_unit/cache_store.rb +143 -0
- data/lib/compute_unit/compute_base.rb +65 -0
- data/lib/compute_unit/cpu.rb +36 -0
- data/lib/compute_unit/device.rb +397 -0
- data/lib/compute_unit/exceptions.rb +14 -0
- data/lib/compute_unit/formatters.rb +21 -0
- data/lib/compute_unit/gpu.rb +338 -0
- data/lib/compute_unit/gpus/amd_gpu.rb +525 -0
- data/lib/compute_unit/gpus/nvidia_gpu.rb +223 -0
- data/lib/compute_unit/logger.rb +70 -0
- data/lib/compute_unit/monkey_patches.rb +101 -0
- data/lib/compute_unit/utils.rb +26 -0
- data/lib/compute_unit/version.rb +5 -0
- metadata +142 -0
data/bin/setup
ADDED
@@ -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
|
data/exe/list_computes
ADDED
@@ -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
|
+
|
data/exe/update_pcidb
ADDED
data/lib/compute_unit.rb
ADDED
@@ -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
|