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