cgminer_api_client 0.2.6 → 0.4.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 +4 -4
- data/CHANGELOG.md +385 -0
- data/README.md +124 -43
- data/bin/cgminer_api_client +70 -9
- data/cgminer_api_client.gemspec +25 -6
- data/config/miners.yml.example +2 -1
- data/lib/cgminer_api_client/errors.rb +94 -0
- data/lib/cgminer_api_client/miner/commands.rb +31 -20
- data/lib/cgminer_api_client/miner.rb +123 -44
- data/lib/cgminer_api_client/miner_pool.rb +84 -31
- data/lib/cgminer_api_client/miner_result.rb +51 -0
- data/lib/cgminer_api_client/pool_result.rb +94 -0
- data/lib/cgminer_api_client/socket_with_timeout.rb +11 -5
- data/lib/cgminer_api_client/version.rb +3 -1
- data/lib/cgminer_api_client.rb +9 -7
- metadata +17 -26
- data/.gitignore +0 -16
- data/.rspec +0 -3
- data/.travis.yml +0 -5
- data/.whitesource +0 -8
- data/Gemfile +0 -10
- data/Rakefile +0 -7
- data/spec/cgminer_api_client/miner/commands_spec.rb +0 -501
- data/spec/cgminer_api_client/miner_pool_spec.rb +0 -134
- data/spec/cgminer_api_client/miner_spec.rb +0 -296
- data/spec/cgminer_api_client_spec.rb +0 -41
- data/spec/spec_helper.rb +0 -25
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CgminerApiClient
|
|
2
4
|
class MinerPool
|
|
3
5
|
include Miner::Commands
|
|
4
6
|
|
|
5
7
|
attr_accessor :miners
|
|
6
8
|
|
|
7
|
-
def initialize
|
|
9
|
+
def initialize(on_wire: nil)
|
|
10
|
+
@on_wire = on_wire
|
|
8
11
|
load_miners!
|
|
9
12
|
end
|
|
10
13
|
|
|
@@ -13,56 +16,106 @@ module CgminerApiClient
|
|
|
13
16
|
load_miners!
|
|
14
17
|
end
|
|
15
18
|
|
|
19
|
+
# Runs `method` against every miner in the pool in parallel and
|
|
20
|
+
# returns a PoolResult — an Enumerable wrapper around a
|
|
21
|
+
# MinerResult per miner, in pool order. Successes and failures
|
|
22
|
+
# are both captured structurally; this method does NOT write
|
|
23
|
+
# to stderr. Callers that want to display failures should
|
|
24
|
+
# iterate the result (or use PoolResult#errors).
|
|
16
25
|
def query(method, *params)
|
|
17
|
-
threads = @miners.
|
|
26
|
+
threads = @miners.map do |miner|
|
|
18
27
|
Thread.new do
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
$stderr.puts "#{e.class}: #{e}"
|
|
23
|
-
[]
|
|
24
|
-
end
|
|
28
|
+
MinerResult.success(miner, miner.query(method, *params))
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
MinerResult.failure(miner, e)
|
|
25
31
|
end
|
|
26
32
|
end
|
|
27
|
-
threads.each
|
|
28
|
-
threads.
|
|
33
|
+
threads.each(&:join)
|
|
34
|
+
PoolResult.new(threads.map(&:value))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The Commands::ReadOnly convenience methods that unwrap
|
|
38
|
+
# single-element cgminer responses with `query(:name)[0]` make
|
|
39
|
+
# sense on a single Miner, but on a MinerPool they silently
|
|
40
|
+
# returned only the first miner's hash — a pre-existing latent
|
|
41
|
+
# bug masked by always calling .first on the result.
|
|
42
|
+
#
|
|
43
|
+
# Override them here to return a PoolResult where each
|
|
44
|
+
# successful MinerResult carries the unwrapped hash instead of
|
|
45
|
+
# the one-element array. Failures pass through unchanged.
|
|
46
|
+
%i[summary coin config version].each do |cmd|
|
|
47
|
+
define_method(cmd) do
|
|
48
|
+
unwrap_first(query(cmd))
|
|
49
|
+
end
|
|
29
50
|
end
|
|
30
51
|
|
|
31
|
-
def
|
|
32
|
-
|
|
52
|
+
def check(command)
|
|
53
|
+
unwrap_first(query(:check, command))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def available_miners
|
|
57
|
+
threads = @miners.map do |miner|
|
|
33
58
|
Thread.new do
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
# Suppress Ruby's default "auto-print unhandled thread
|
|
60
|
+
# exceptions to stderr" behavior. Bugs that propagate
|
|
61
|
+
# here will be re-raised by Thread#value to the caller of
|
|
62
|
+
# #available_miners; we don't want them double-reported
|
|
63
|
+
# to stderr in the meantime.
|
|
64
|
+
Thread.current.report_on_exception = false
|
|
65
|
+
miner if miner.available?
|
|
66
|
+
rescue SocketError, SystemCallError, CgminerApiClient::TimeoutError
|
|
67
|
+
# Miner#available? already returns false for these; this
|
|
68
|
+
# rescue is defensive against a future refactor. Bugs
|
|
69
|
+
# (NoMethodError, ArgumentError) still propagate via
|
|
70
|
+
# Thread#value re-raising.
|
|
71
|
+
nil
|
|
39
72
|
end
|
|
40
73
|
end
|
|
41
|
-
threads.each
|
|
42
|
-
threads.
|
|
74
|
+
threads.each(&:join)
|
|
75
|
+
threads.map(&:value).compact
|
|
43
76
|
end
|
|
44
77
|
|
|
45
|
-
def unavailable_miners
|
|
46
|
-
@miners - available_miners
|
|
78
|
+
def unavailable_miners
|
|
79
|
+
@miners - available_miners
|
|
47
80
|
end
|
|
48
81
|
|
|
49
|
-
def method_missing(name, *
|
|
50
|
-
query(name, *
|
|
82
|
+
def method_missing(name, *)
|
|
83
|
+
query(name, *)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# See Miner#respond_to_missing? for the rationale.
|
|
87
|
+
def respond_to_missing?(name, _include_private = false)
|
|
88
|
+
!name.to_s.start_with?('to_', '_')
|
|
51
89
|
end
|
|
52
90
|
|
|
53
91
|
private
|
|
54
92
|
|
|
93
|
+
# Rebuild a PoolResult where each successful MinerResult's
|
|
94
|
+
# value is replaced with value.first (i.e. the unwrapped
|
|
95
|
+
# single-element response). Failures pass through unchanged.
|
|
96
|
+
def unwrap_first(pool_result)
|
|
97
|
+
PoolResult.new(pool_result.results.map do |r|
|
|
98
|
+
r.ok? ? MinerResult.success(r.miner, r.value.first) : r
|
|
99
|
+
end)
|
|
100
|
+
end
|
|
101
|
+
|
|
55
102
|
def load_miners!
|
|
56
|
-
raise 'Please create config/miners.yml' unless File.exist?('config/miners.yml')
|
|
103
|
+
raise CgminerApiClient::Error, 'Please create config/miners.yml' unless File.exist?('config/miners.yml')
|
|
104
|
+
|
|
105
|
+
miners_config = YAML.safe_load_file('config/miners.yml')
|
|
106
|
+
@miners = miners_config.each_with_index.map do |entry, index|
|
|
107
|
+
unless entry.is_a?(Hash) && entry['host']
|
|
108
|
+
raise CgminerApiClient::Error,
|
|
109
|
+
"config/miners.yml: entry #{index} is missing 'host'"
|
|
110
|
+
end
|
|
57
111
|
|
|
58
|
-
miners_config = YAML.load_file('config/miners.yml')
|
|
59
|
-
@miners = miners_config.collect{|miner|
|
|
60
112
|
CgminerApiClient::Miner.new(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
113
|
+
entry['host'],
|
|
114
|
+
entry['port'],
|
|
115
|
+
entry['timeout'],
|
|
116
|
+
on_wire: @on_wire
|
|
64
117
|
)
|
|
65
|
-
|
|
118
|
+
end
|
|
66
119
|
end
|
|
67
120
|
end
|
|
68
|
-
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CgminerApiClient
|
|
4
|
+
# A single per-miner outcome from a MinerPool query. Either a
|
|
5
|
+
# successful response (error is nil, value holds the parsed data)
|
|
6
|
+
# or a failure (value is nil, error holds the exception).
|
|
7
|
+
#
|
|
8
|
+
# Immutable value object backed by Data.define, which gives us
|
|
9
|
+
# ==, hash, eql?, inspect, to_h, and deconstruct_keys for pattern
|
|
10
|
+
# matching for free.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# result = pool.summary.first
|
|
14
|
+
# if result.ok?
|
|
15
|
+
# puts result.value[:mhs_av]
|
|
16
|
+
# else
|
|
17
|
+
# warn "#{result.miner.host}: #{result.error.message}"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Or with pattern matching:
|
|
21
|
+
# case result
|
|
22
|
+
# in { ok?: true, value: } then use(value)
|
|
23
|
+
# in { ok?: false, error: } then log(error)
|
|
24
|
+
# end
|
|
25
|
+
MinerResult = Data.define(:miner, :value, :error) do
|
|
26
|
+
def self.success(miner, value)
|
|
27
|
+
new(miner: miner, value: value, error: nil)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.failure(miner, error)
|
|
31
|
+
new(miner: miner, value: nil, error: error)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ok?
|
|
35
|
+
error.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def failed?
|
|
39
|
+
!ok?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Re-raise the captured error, or return the value if successful.
|
|
43
|
+
# Equivalent to `result.ok? ? result.value : raise(result.error)`
|
|
44
|
+
# but shorter at call sites.
|
|
45
|
+
def raise!
|
|
46
|
+
raise error if failed?
|
|
47
|
+
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CgminerApiClient
|
|
4
|
+
# An Enumerable wrapper around Array<MinerResult> returned from
|
|
5
|
+
# every MinerPool query. Preserves miner order (position N in the
|
|
6
|
+
# pool maps to position N in the result). Callers can either
|
|
7
|
+
# iterate per-Result for full control, or use the high-level
|
|
8
|
+
# helpers for common cases.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# result = pool.summary
|
|
12
|
+
# result.values # [{mhs_av: ..., elapsed: ...}] — successes only
|
|
13
|
+
# result.errors # [<ConnectionError>, ...] — failures only
|
|
14
|
+
# result.successful # [<MinerResult>, ...] — ok? Results
|
|
15
|
+
# result.failed # [<MinerResult>, ...] — !ok? Results
|
|
16
|
+
# result.all_successful? # true if every miner succeeded
|
|
17
|
+
# result.any_succeeded? # true if at least one miner succeeded
|
|
18
|
+
# result.any_failed? # true if any miner failed
|
|
19
|
+
# result[0] # MinerResult at index 0
|
|
20
|
+
# result[miner] # MinerResult for a specific Miner instance
|
|
21
|
+
# result["10.0.0.1:4028"] # MinerResult by "host:port" string
|
|
22
|
+
# result.each { |r| r.miner.host } # iterates MinerResult instances
|
|
23
|
+
class PoolResult
|
|
24
|
+
include Enumerable
|
|
25
|
+
|
|
26
|
+
attr_reader :results
|
|
27
|
+
|
|
28
|
+
def initialize(results)
|
|
29
|
+
@results = results.freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def each(&)
|
|
33
|
+
@results.each(&)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def size
|
|
37
|
+
@results.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def empty?
|
|
41
|
+
@results.empty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_a
|
|
45
|
+
@results.dup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def values
|
|
49
|
+
@results.select(&:ok?).map(&:value)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def errors
|
|
53
|
+
@results.reject(&:ok?).map(&:error)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def successful
|
|
57
|
+
@results.select(&:ok?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def failed
|
|
61
|
+
@results.reject(&:ok?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def all_successful?
|
|
65
|
+
@results.all?(&:ok?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def any_succeeded?
|
|
69
|
+
@results.any?(&:ok?)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def any_failed?
|
|
73
|
+
@results.any?(&:failed?)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Lookup by integer index, Miner instance, or "host:port" string.
|
|
77
|
+
def [](key)
|
|
78
|
+
case key
|
|
79
|
+
when Integer then @results[key]
|
|
80
|
+
when String then @results.find { |r| "#{r.miner.host}:#{r.miner.port}" == key }
|
|
81
|
+
else @results.find { |r| r.miner == key }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ==(other)
|
|
86
|
+
other.is_a?(self.class) && @results == other.results
|
|
87
|
+
end
|
|
88
|
+
alias eql? ==
|
|
89
|
+
|
|
90
|
+
def hash
|
|
91
|
+
@results.hash
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CgminerApiClient
|
|
2
4
|
module SocketWithTimeout
|
|
3
5
|
def open_socket(host, port, timeout)
|
|
@@ -10,21 +12,25 @@ module CgminerApiClient
|
|
|
10
12
|
begin
|
|
11
13
|
socket.connect_nonblock(sockaddr)
|
|
12
14
|
rescue IO::WaitWritable
|
|
13
|
-
if
|
|
15
|
+
if socket.wait_writable(timeout)
|
|
14
16
|
begin
|
|
15
17
|
socket.connect_nonblock(sockaddr)
|
|
16
18
|
rescue Errno::EISCONN
|
|
17
|
-
# the
|
|
18
|
-
|
|
19
|
+
# On Linux, the second connect_nonblock on a now-writable
|
|
20
|
+
# socket reports EISCONN to mean "the connection completed
|
|
21
|
+
# while we were waiting." Treat as success. On other
|
|
22
|
+
# platforms the second call returns 0 cleanly and this
|
|
23
|
+
# branch never fires.
|
|
24
|
+
rescue StandardError
|
|
19
25
|
socket.close
|
|
20
26
|
raise
|
|
21
27
|
end
|
|
22
28
|
else
|
|
23
29
|
socket.close
|
|
24
|
-
raise "Connection timeout"
|
|
30
|
+
raise CgminerApiClient::TimeoutError, "Connection to #{host}:#{port} timed out after #{timeout}s"
|
|
25
31
|
end
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
|
-
end
|
|
36
|
+
end
|
data/lib/cgminer_api_client.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'json'
|
|
2
4
|
require 'socket'
|
|
3
|
-
require 'thread'
|
|
4
5
|
require 'yaml'
|
|
5
6
|
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
7
|
+
require 'cgminer_api_client/errors'
|
|
8
|
+
require 'cgminer_api_client/miner_result'
|
|
9
|
+
require 'cgminer_api_client/pool_result'
|
|
10
|
+
require 'cgminer_api_client/miner'
|
|
11
|
+
require 'cgminer_api_client/miner_pool'
|
|
12
|
+
require 'cgminer_api_client/version'
|
|
11
13
|
|
|
12
14
|
module CgminerApiClient
|
|
13
15
|
def self.default_host
|
|
@@ -37,4 +39,4 @@ module CgminerApiClient
|
|
|
37
39
|
def self.config
|
|
38
40
|
yield self if block_given?
|
|
39
41
|
end
|
|
40
|
-
end
|
|
42
|
+
end
|
metadata
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cgminer_api_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Ramos
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
|
-
description:
|
|
12
|
+
description: Ruby client for the cgminer JSON API. Supports querying a single miner
|
|
13
|
+
or a pool of miners in parallel, with full coverage of read-only and privileged
|
|
14
|
+
commands.
|
|
14
15
|
email:
|
|
15
16
|
- justin.ramos@gmail.com
|
|
16
17
|
executables:
|
|
@@ -18,33 +19,29 @@ executables:
|
|
|
18
19
|
extensions: []
|
|
19
20
|
extra_rdoc_files: []
|
|
20
21
|
files:
|
|
21
|
-
-
|
|
22
|
-
- ".rspec"
|
|
23
|
-
- ".travis.yml"
|
|
24
|
-
- ".whitesource"
|
|
25
|
-
- Gemfile
|
|
22
|
+
- CHANGELOG.md
|
|
26
23
|
- LICENSE.txt
|
|
27
24
|
- README.md
|
|
28
|
-
- Rakefile
|
|
29
25
|
- bin/cgminer_api_client
|
|
30
26
|
- cgminer_api_client.gemspec
|
|
31
27
|
- config/miners.yml.example
|
|
32
28
|
- lib/cgminer_api_client.rb
|
|
29
|
+
- lib/cgminer_api_client/errors.rb
|
|
33
30
|
- lib/cgminer_api_client/miner.rb
|
|
34
31
|
- lib/cgminer_api_client/miner/commands.rb
|
|
35
32
|
- lib/cgminer_api_client/miner_pool.rb
|
|
33
|
+
- lib/cgminer_api_client/miner_result.rb
|
|
34
|
+
- lib/cgminer_api_client/pool_result.rb
|
|
36
35
|
- lib/cgminer_api_client/socket_with_timeout.rb
|
|
37
36
|
- lib/cgminer_api_client/version.rb
|
|
38
|
-
- spec/cgminer_api_client/miner/commands_spec.rb
|
|
39
|
-
- spec/cgminer_api_client/miner_pool_spec.rb
|
|
40
|
-
- spec/cgminer_api_client/miner_spec.rb
|
|
41
|
-
- spec/cgminer_api_client_spec.rb
|
|
42
|
-
- spec/spec_helper.rb
|
|
43
37
|
homepage: https://github.com/jramos/cgminer_api_client
|
|
44
38
|
licenses:
|
|
45
39
|
- MIT
|
|
46
|
-
metadata:
|
|
47
|
-
|
|
40
|
+
metadata:
|
|
41
|
+
source_code_uri: https://github.com/jramos/cgminer_api_client
|
|
42
|
+
changelog_uri: https://github.com/jramos/cgminer_api_client/blob/master/CHANGELOG.md
|
|
43
|
+
bug_tracker_uri: https://github.com/jramos/cgminer_api_client/issues
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
48
45
|
rdoc_options: []
|
|
49
46
|
require_paths:
|
|
50
47
|
- lib
|
|
@@ -52,20 +49,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
52
49
|
requirements:
|
|
53
50
|
- - ">="
|
|
54
51
|
- !ruby/object:Gem::Version
|
|
55
|
-
version: '
|
|
52
|
+
version: '3.2'
|
|
56
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
54
|
requirements:
|
|
58
55
|
- - ">="
|
|
59
56
|
- !ruby/object:Gem::Version
|
|
60
57
|
version: '0'
|
|
61
58
|
requirements: []
|
|
62
|
-
rubygems_version:
|
|
63
|
-
signing_key:
|
|
59
|
+
rubygems_version: 4.0.6
|
|
64
60
|
specification_version: 4
|
|
65
61
|
summary: A gem that allows sending API commands to a pool of cgminer instances
|
|
66
|
-
test_files:
|
|
67
|
-
- spec/cgminer_api_client/miner/commands_spec.rb
|
|
68
|
-
- spec/cgminer_api_client/miner_pool_spec.rb
|
|
69
|
-
- spec/cgminer_api_client/miner_spec.rb
|
|
70
|
-
- spec/cgminer_api_client_spec.rb
|
|
71
|
-
- spec/spec_helper.rb
|
|
62
|
+
test_files: []
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.travis.yml
DELETED
data/.whitesource
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
##########################################################
|
|
2
|
-
#### WhiteSource "Bolt for Github" configuration file ####
|
|
3
|
-
##########################################################
|
|
4
|
-
|
|
5
|
-
# Configuration #
|
|
6
|
-
#---------------#
|
|
7
|
-
ws.repo.scan=true
|
|
8
|
-
vulnerable.check.run.conclusion.level=failure
|
data/Gemfile
DELETED