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.
@@ -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.collect do |miner|
26
+ threads = @miners.map do |miner|
18
27
  Thread.new do
19
- begin
20
- miner.query(method, *params)
21
- rescue => e
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 { |thr| thr.join }
28
- threads.collect(&:value)
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 available_miners(force_reload = false)
32
- threads = @miners.collect do |miner|
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
- begin
35
- miner if miner.available?(force_reload)
36
- rescue
37
- nil
38
- end
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 { |thr| thr.join }
42
- threads.collect(&:value).compact
74
+ threads.each(&:join)
75
+ threads.map(&:value).compact
43
76
  end
44
77
 
45
- def unavailable_miners(force_reload = false)
46
- @miners - available_miners(force_reload)
78
+ def unavailable_miners
79
+ @miners - available_miners
47
80
  end
48
81
 
49
- def method_missing(name, *args)
50
- query(name, *args)
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
- miner['host'],
62
- miner['port'],
63
- miner['timeout']
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 IO.select(nil, [socket], nil, timeout)
15
+ if socket.wait_writable(timeout)
14
16
  begin
15
17
  socket.connect_nonblock(sockaddr)
16
18
  rescue Errno::EISCONN
17
- # the socket is connected
18
- rescue
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CgminerApiClient
2
- VERSION = "0.2.6"
4
+ VERSION = "0.4.0"
3
5
  end
@@ -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 "cgminer_api_client/miner"
7
- require "cgminer_api_client/miner/commands"
8
- require "cgminer_api_client/miner_pool"
9
- require "cgminer_api_client/socket_with_timeout"
10
- require "cgminer_api_client/version"
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.2.6
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: 2019-12-15 00:00:00.000000000 Z
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
- - ".gitignore"
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
- post_install_message:
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: '0'
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: 3.0.6
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
@@ -1,16 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- *.bundle
11
- *.so
12
- *.o
13
- *.a
14
- mkmf.log
15
- config/miners.yml
16
- ._*
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --color
2
- --warnings
3
- --require spec_helper
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.6
4
- notifications:
5
- email: false
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
@@ -1,10 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
4
-
5
- group :development do
6
- gem 'pry' , '>= 0.12.0'
7
- gem 'rake' , '>= 13.0.0'
8
- gem 'rspec' , '>= 3.9'
9
- gem 'simplecov' , '>= 0.17.0'
10
- end
data/Rakefile DELETED
@@ -1,7 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require 'rspec/core/rake_task'
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
7
- task :test => :spec