cgminer_api_client 0.2.6 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8df86a4b12bb2eef41633534532290139749a0491fb27d89a5c7ce55b596d4a5
4
- data.tar.gz: adce4406c1bcec642a7b6698473a355a211b5c467765aaba466ef34efd563d64
3
+ metadata.gz: 129410d116f95439ef734a1ae6265c047eb7f95007755e5788fc416e78741388
4
+ data.tar.gz: 386bd4f91ce093e290777a1b00be6efaa173f03f951b8bcc00d4b8683ce0c576
5
5
  SHA512:
6
- metadata.gz: 0305613f25ab54883339bb54f7155ff08b09af899d42a57dabd8dd051b6176d4aed3f698584606dd6081045756cb6ffe16035ec8abaca382271289f30859e86c
7
- data.tar.gz: e093f302848547c460865383a7fd65eb48103c1dab9d2991109376ab5bc9c3c3e1d32c66140dcb031da5e65ee634f840d51dfff6a689364426b360f6099cdf91
6
+ metadata.gz: d7ebd6316421e95755768cf0f77dd672e3860c2fd508fd2314cc898f46b9c76d39efc78d3b1f4ac422c873871e1896b5fb8859e681c8ff3712f5f60f12c85351
7
+ data.tar.gz: 99a4578907f13668e8f0bf5cf466298f40774e318d3f470ad38df7f0cfa2455b51c028866084a264201156b6e30505daf7849e1d6a12aec4122ec5092d788fed
data/CHANGELOG.md ADDED
@@ -0,0 +1,286 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-04-07
9
+
10
+ ### Removed
11
+ - Support for Ruby 2.x and Ruby 3.1. The gem now requires Ruby 3.2 or higher.
12
+ - `Miner#query`'s short-circuit on `!available?`. `query` now calls
13
+ `perform_request` directly and lets `ConnectionError` propagate to
14
+ the caller. Previously an unreachable miner caused `query` to
15
+ return `nil` silently.
16
+ - `Miner#available?`'s `force_reload` parameter. The method no
17
+ longer caches, so the parameter is meaningless.
18
+ - `MinerPool#available_miners` and `MinerPool#unavailable_miners`
19
+ `force_reload` parameter. Same reason.
20
+ - The host:port-prefixed stderr warning inside `MinerPool#query`
21
+ (added earlier in the 0.3.0 cycle). Library code should not
22
+ print unsolicited; the error is now structurally available via
23
+ `PoolResult` and the CLI prints it explicitly.
24
+ - `pry` from the development dependencies (it was unused).
25
+ - `.travis.yml` (Travis CI is effectively deprecated for OSS Ruby).
26
+ - `.whitesource` (Mend Bolt for GitHub was sunset; if SCA is wanted
27
+ later, configure Dependabot via `.github/dependabot.yml`).
28
+
29
+ ### Added
30
+ - **`CgminerApiClient::MinerResult`** — immutable value object
31
+ (backed by `Data.define`) wrapping a single per-miner outcome.
32
+ Has `miner`, `value`, `error`, `ok?`, `failed?`, `raise!`,
33
+ plus free `==`, `hash`, `eql?`, `inspect`, `to_h`, and
34
+ `deconstruct_keys` from `Data.define` for pattern matching.
35
+ - **`CgminerApiClient::PoolResult`** — Enumerable wrapper around
36
+ `Array<MinerResult>` returned from every `MinerPool` query.
37
+ Preserves miner order. High-level helpers: `#values`, `#errors`,
38
+ `#successful`, `#failed`, `#all_successful?`, `#any_succeeded?`,
39
+ `#any_failed?`, plus `#[](key)` for lookup by index, Miner
40
+ instance, or `"host:port"` string.
41
+ - `CgminerApiClient::Error`, `CgminerApiClient::ConnectionError`,
42
+ `CgminerApiClient::TimeoutError` (subclass of ConnectionError),
43
+ and `CgminerApiClient::ApiError` exception classes. All subclass
44
+ the base `Error` which itself subclasses `StandardError`, so
45
+ existing rescues keep working.
46
+ - `respond_to_missing?` on `Miner` and `MinerPool` so introspection
47
+ (`respond_to?`, `Object#method`, etc.) reflects the dynamic API
48
+ surface while excluding internal probes (`to_*`, `_*`).
49
+ - `# frozen_string_literal: true` pragma on every Ruby file.
50
+ - `required_ruby_version >= 3.2` in the gemspec.
51
+ - Gemspec metadata (`source_code_uri`, `changelog_uri`,
52
+ `bug_tracker_uri`, `rubygems_mfa_required`).
53
+ - RuboCop with `rubocop-rspec` and `rubocop-rake` plugins,
54
+ integrated into the default Rake task.
55
+ - GitHub Actions CI matrix testing Ruby 3.2, 3.3, 3.4, and 4.0
56
+ (plus `head` as an early-warning, allowed to fail).
57
+ - `.ruby-version` file pinning local development to 4.0.2 (the
58
+ gem itself supports 3.2+; the pin is only for contributors).
59
+ - `CHANGELOG.md` (this file).
60
+ - Unit test coverage brought to 99.66% on `lib/` (was 97.4% before
61
+ the 0.3.0 work). The entire `SocketWithTimeout#open_socket`
62
+ method, `MinerPool#available_miners`, `#unavailable_miners`,
63
+ the thread-rescue branch in `MinerPool#query`, the control-
64
+ character escape path in `Miner#perform_request`, and the new
65
+ `MinerResult` / `PoolResult` value objects all have dedicated
66
+ specs.
67
+ - End-to-end integration test suite at `spec/integration/`.
68
+ `miner_integration_spec.rb` exercises the full request → TCP
69
+ socket → response → parse → result path against a `FakeCgminer`
70
+ server running in a background thread. `cli_spec.rb` spawns
71
+ the real binary via `Open3` and asserts on exit codes,
72
+ stdout/stderr split, and `DEBUG=1` backtrace behavior.
73
+ - `script/fake_cgminer` for manual sandbox testing. Starts the
74
+ same fake cgminer server on a fixed port (default 4028) in the
75
+ foreground, so you can run the CLI against it without hardware.
76
+ Intentionally lives in `script/` rather than `bin/` so it isn't
77
+ packaged with the gem.
78
+
79
+ ### Changed
80
+ - **`MinerPool#query` now returns a `PoolResult`** instead of an
81
+ `Array` with `[]` sentinels for failed miners. Callers iterate
82
+ `MinerResult` instances and explicitly distinguish success from
83
+ failure. Failed miners are no longer confused with successful
84
+ ones returning empty arrays.
85
+ - **`MinerPool` now overrides `summary`, `coin`, `config`,
86
+ `version`, and `check`** to return a `PoolResult` of unwrapped
87
+ hashes. Previously these convenience methods called
88
+ `query(:name)[0]`, which silently returned only the *first*
89
+ miner's hash on multi-miner pools (a pre-existing latent bug).
90
+ - **`Miner#query` now raises `ConnectionError`** instead of
91
+ returning `nil` when a miner is unreachable. Single-Miner
92
+ callers need to rescue; MinerPool callers get the error
93
+ captured in a `MinerResult.failure` automatically.
94
+ - **`Miner#available?` is now a true reachability probe.** No
95
+ cache, narrow rescue list (only `SocketError`,
96
+ `SystemCallError`, and `TimeoutError` — bugs like `ArgumentError`
97
+ propagate). Always re-checks.
98
+ - `SocketWithTimeout` raises `CgminerApiClient::TimeoutError`
99
+ instead of a bare `RuntimeError` on connect timeout.
100
+ - `Miner#perform_request` raises `ConnectionError` (not
101
+ `RuntimeError`) on socket open failure, with a message that
102
+ includes the original error class and message.
103
+ - `Miner#check_status` raises `ApiError` (not `RuntimeError`) for
104
+ cgminer status codes `E` and `F`.
105
+ - **`bin/cgminer_api_client` redesigned**: errors go to stderr
106
+ (not stdout), exit codes follow shell conventions (0 on any
107
+ success, 1 on all-failure, 64/EX_USAGE on unknown command),
108
+ `DEBUG=1` env var prints full backtraces via
109
+ `Exception#full_message`, and output is structured per-miner
110
+ with `host:port:` headers.
111
+ - `YAML.load_file` → `YAML.safe_load_file` for the miners config.
112
+ - `IO.select(nil, [socket], nil, timeout)` →
113
+ `socket.wait_writable(timeout)` in `SocketWithTimeout` —
114
+ Fiber-scheduler compatible.
115
+ - `String#match` → `String#match?` where the result is only used
116
+ as a boolean.
117
+ - `length == 0` → `empty?`, `'%04x' %` → `format`, and other
118
+ small modernizations.
119
+ - Bare `rescue` clauses now specify `StandardError` explicitly.
120
+ - Gemspec file list switched from `git ls-files` to explicit
121
+ `Dir.glob` patterns; `spec/` is no longer packaged in the gem.
122
+ - Bumped minimum versions of `rake`, `rspec`, and `simplecov`.
123
+
124
+ ### Fixed
125
+ - **`Miner#query` parameter escape was a silent no-op for
126
+ backslashes.** The intent was to double literal backslashes so
127
+ they round-trip through cgminer's comma-separated parameter
128
+ syntax, but `gsub('\\', '\\\\')` is parsed as "replace `\` with
129
+ `\`" because in gsub's replacement-string DSL, `\\` denotes a
130
+ single literal backslash. Fixed by switching to
131
+ `gsub('\\') { '\\\\' }` (block form bypasses replacement-string
132
+ interpretation). Verified empirically; locked down by four
133
+ explicit specs.
134
+ - **`Miner::Commands#privileged` mislabeled connection errors as
135
+ access denied.** Previously rescued every `StandardError` and
136
+ returned `false`, which propagated through `access_denied?` to
137
+ raise `'access_denied'` from every privileged command
138
+ (`addpool`, `restart`, `quit`, `save`, `ascset`, etc.) during a
139
+ transient network outage — sending operators chasing phantom
140
+ auth/whitelist bugs. Now rescues only `ApiError` so
141
+ connection-layer failures surface as `ConnectionError` instead.
142
+ - **`MinerPool#summary` / `#coin` / `#config` / `#version` /
143
+ `#check` silently returned only the first miner's hash on
144
+ multi-miner pools.** The inherited `Miner::Commands::ReadOnly`
145
+ methods do `query(:name)[0]` to unwrap cgminer's single-element
146
+ response arrays, which works on a single Miner but drops every
147
+ result except the first on a pool. Fixed by overriding all five
148
+ on `MinerPool` to return a `PoolResult` of unwrapped hashes.
149
+ - **`Miner#available?` permanently cached `false` after any
150
+ transient failure.** Once a brief network blip had marked a
151
+ miner unavailable, the gem refused to talk to it for the
152
+ lifetime of the process. Fixed by dropping the cache entirely.
153
+ - Mismatched indentation in `def privileged` that was producing a
154
+ Ruby parser warning.
155
+
156
+ ## Migration guide: 0.2.x → 0.3.0
157
+
158
+ This release intentionally contains breaking changes to finish
159
+ cleanups that were overdue. The gem is still on 0.x and only
160
+ ~2,600 of 35,000 total downloads are on 0.2.6, so the
161
+ migration surface is small. Here's what to update.
162
+
163
+ ### Ruby 3.2+
164
+
165
+ The gemspec now requires Ruby 3.2. If you're on 3.1 or older, you
166
+ need to upgrade Ruby before you can use this release.
167
+
168
+ ### `MinerPool#query` (and all pool commands) return `PoolResult`
169
+
170
+ ```ruby
171
+ # Before (0.2.x)
172
+ pool.summary.each { |s| puts s[:mhs_av] }
173
+ pool.summary.first[:mhs_av]
174
+
175
+ # After (0.3.0)
176
+ pool.summary.values.each { |s| puts s[:mhs_av] }
177
+ pool.summary.values.first[:mhs_av]
178
+
179
+ # Or iterate per-miner with failure handling:
180
+ pool.summary.each do |result|
181
+ if result.ok?
182
+ puts "#{result.miner.host}: #{result.value[:mhs_av]}"
183
+ else
184
+ warn "#{result.miner.host}: #{result.error.message}"
185
+ end
186
+ end
187
+ ```
188
+
189
+ `PoolResult` includes `Enumerable`, so `.first`, `.map`, `.count`,
190
+ `.find` etc. all work — but now they yield `MinerResult`
191
+ instances, not raw hashes. Use `.values` to get just the
192
+ successful values as an Array.
193
+
194
+ ### `Miner#query` raises `ConnectionError` instead of returning `nil`
195
+
196
+ ```ruby
197
+ # Before (0.2.x)
198
+ result = miner.summary
199
+ return unless result # nil meant unreachable
200
+ puts result[:mhs_av]
201
+
202
+ # After (0.3.0)
203
+ begin
204
+ result = miner.summary
205
+ puts result[:mhs_av]
206
+ rescue CgminerApiClient::ConnectionError => e
207
+ warn "miner unreachable: #{e.message}"
208
+ end
209
+ ```
210
+
211
+ If you're using `MinerPool` instead of `Miner` directly, you
212
+ don't need to change anything — `MinerPool` catches the
213
+ `ConnectionError` per miner and turns it into a
214
+ `MinerResult.failure`.
215
+
216
+ ### `MinerPool#summary` etc. now return every miner, not just the first
217
+
218
+ If you were calling `pool.summary` on a multi-miner pool and
219
+ relying on getting back a single Hash, you were actually being
220
+ bitten by a pre-existing bug (the result was silently dropping
221
+ every miner except the first). Now you get a `PoolResult` with
222
+ one entry per miner. Use `.values.first` if you really want
223
+ just the first miner.
224
+
225
+ ### `force_reload` parameter removed
226
+
227
+ ```ruby
228
+ # Before (0.2.x)
229
+ pool.available_miners(true) # force re-check
230
+ miner.available?(true)
231
+
232
+ # After (0.3.0)
233
+ pool.available_miners # always re-checks
234
+ miner.available?
235
+ ```
236
+
237
+ There's no cache to flush anymore.
238
+
239
+ ### `MinerPool#query` no longer writes to stderr
240
+
241
+ Previously, a failed per-miner query would print
242
+ `[host:port] ErrorClass: message` to stderr as a side effect of
243
+ calling `pool.query`. That was an earlier 0.3.0 addition; it's
244
+ removed because library code shouldn't print. The error is now
245
+ carried structurally in the `MinerResult.failure` inside the
246
+ `PoolResult`, and you can display it however you want. The CLI
247
+ (`bin/cgminer_api_client`) does display it on stderr.
248
+
249
+ ### CLI exit codes changed
250
+
251
+ ```
252
+ # Before (0.2.x)
253
+ cgminer_api_client summary # exit 0 whether or not it worked
254
+ # errors written to stdout
255
+
256
+ # After (0.3.0)
257
+ cgminer_api_client summary # exit 0 if any miner succeeded,
258
+ # 1 if all failed,
259
+ # 64 for unknown command.
260
+ # errors on stderr.
261
+ DEBUG=1 cgminer_api_client summary # also prints full backtraces
262
+ ```
263
+
264
+ If you had a shell pipeline that captured the CLI's stdout
265
+ expecting the whole data blob (including errors), your script
266
+ needs updating. Errors now go to stderr like they should.
267
+
268
+ ### Exception class hierarchy
269
+
270
+ All gem-specific errors now inherit from
271
+ `CgminerApiClient::Error < StandardError`:
272
+
273
+ ```
274
+ CgminerApiClient::Error
275
+ ├── CgminerApiClient::ConnectionError
276
+ │ └── CgminerApiClient::TimeoutError
277
+ └── CgminerApiClient::ApiError
278
+ ```
279
+
280
+ Existing `rescue StandardError` clauses keep working. If you
281
+ want to catch everything the gem raises, `rescue CgminerApiClient::Error`
282
+ now works.
283
+
284
+ ## [0.2.6] - earlier
285
+
286
+ See git history for changes prior to 0.3.0.
data/README.md CHANGED
@@ -1,28 +1,15 @@
1
- # CgminerApiClient [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b59cb27aeae64f7c992f7072c5909788)](https://app.codacy.com/app/justin_28/cgminer_api_client?utm_source=github.com&utm_medium=referral&utm_content=jramos/cgminer_api_client&utm_campaign=Badge_Grade_Dashboard) [![Build Status](https://travis-ci.org/jramos/cgminer_api_client.png?branch=master)](https://travis-ci.org/jramos/cgminer_api_client) [![DepShield Badge](https://depshield.sonatype.org/badges/jramos/cgminer_api_client/depshield.svg)](https://depshield.github.io)
1
+ # CgminerApiClient
2
2
 
3
- A gem that allows sending API commands to a pool of cgminer instances.
3
+ [![CI](https://github.com/jramos/cgminer_api_client/actions/workflows/ci.yml/badge.svg)](https://github.com/jramos/cgminer_api_client/actions/workflows/ci.yml)
4
4
 
5
- ## Requirements
6
-
7
- - Ruby (~> 2.0.0, ~> 2.1.0)
8
- - YAML
9
- - JSON
10
- - Socket
11
- - Thread
12
- - cgminer (~> 3.12.0)
5
+ A gem that allows sending API commands to a pool of [cgminer](https://github.com/ckolivas/cgminer) instances.
13
6
 
14
- ## GUI
7
+ ## Requirements
15
8
 
16
- - <https://github.com/jramos/cgminer_manager>
9
+ Ruby 3.2 or higher.
17
10
 
18
11
  ## Installation Options
19
12
 
20
- ### Bundler
21
-
22
- Add the following to your `Gemfile`:
23
-
24
- gem 'cgminer_api_client', '~> 0.2.6'
25
-
26
13
  ### RubyGems
27
14
 
28
15
  $ gem install cgminer_api_client
@@ -32,18 +19,18 @@ Add the following to your `Gemfile`:
32
19
  $ git clone git@github.com:jramos/cgminer_api_client.git
33
20
  $ cd cgminer_api_client
34
21
  $ gem build cgminer_api_client.gemspec
35
- $ gem install cgminer_api_client-0.2.6.gem
22
+ $ gem install cgminer_api_client-<VERSION>.gem
36
23
 
37
24
  ## Configuration
38
25
 
39
26
  Copy [`config/miners.yml.example`](https://github.com/jramos/cgminer_api_client/blob/master/config/miners.yml.example) to `config/miners.yml` and update with the IP addresses (and optional ports and timeouts) of your cgminer instances. E.g.:
40
27
 
41
- # connect to localhost on the default port (4028) with the default timeout (5 seconds)
28
+ # connect to localhost on default port (4028) with default timeout (5 seconds)
42
29
  - host: 127.0.0.1
43
- # connect to 192.168.1.1 on a non-standard port (1234) with a custom timeout (1 second)
30
+ # connect to 192.168.1.1 on port (1234) with custom timeout (3 seconds)
44
31
  - host: 192.168.1.1
45
32
  port: 1234
46
- timeout: 1
33
+ timeout: 3
47
34
 
48
35
  ### Remote API Access
49
36
 
@@ -75,29 +62,74 @@ Restart cgminer:
75
62
 
76
63
  ## Gem Usage
77
64
 
78
- require 'cgminer_api_client'
79
-
80
- # change the default timeout and port for the miners
81
- CgminerApiClient.config do |config|
82
- config.default_port = 4023
83
- config.default_timeout = 3
84
- end
85
-
86
- pool = CgminerApiClient::MinerPool.new
87
-
88
- # run 'devs' on each miner in the pool; returns an array of response hashes
89
- devices = pool.devs
90
-
91
- # run 'summary' on each miner in the pool; returns an array of response hashes
92
- summaries = pool.summary
93
-
94
- # run 'devs' on available miners; returns an array of response hashes
95
- pool.available_miners.collect do |miner|
96
- miner.devs
97
- end
98
-
99
- # restart the pool
100
- pool.restart
65
+ ```ruby
66
+ require 'cgminer_api_client'
67
+
68
+ # Change the defaults for any miners whose config doesn't set them.
69
+ CgminerApiClient.config do |config|
70
+ config.default_port = 4028
71
+ config.default_timeout = 3
72
+ end
73
+
74
+ pool = CgminerApiClient::MinerPool.new
75
+ ```
76
+
77
+ ### Pool queries return a `PoolResult`
78
+
79
+ Every pool query returns a `PoolResult` — an `Enumerable` wrapper
80
+ around one `MinerResult` per miner, in pool order. Each
81
+ `MinerResult` is either a success (carrying a parsed value) or a
82
+ failure (carrying the exception). Callers choose how much detail
83
+ they care about.
84
+
85
+ ```ruby
86
+ # Just give me the data, ignore failures:
87
+ pool.summary.values.each do |s|
88
+ puts "hashrate: #{s[:mhs_av]}"
89
+ end
90
+
91
+ # Handle successes and failures explicitly:
92
+ pool.summary.each do |result|
93
+ if result.ok?
94
+ puts "#{result.miner.host}: #{result.value[:mhs_av]}"
95
+ else
96
+ warn "#{result.miner.host}: #{result.error.message}"
97
+ end
98
+ end
99
+
100
+ # Quick checks:
101
+ pool.summary.all_successful? # true if every miner responded
102
+ pool.summary.any_failed? # true if any miner failed
103
+ pool.summary.errors # [<ConnectionError>, ...]
104
+ pool.summary['10.0.0.5:4028'] # lookup by host:port string
105
+ ```
106
+
107
+ ### Single-miner access
108
+
109
+ If you want to talk to one specific miner without the pool
110
+ wrapping, use `Miner` directly. Unreachable miners raise
111
+ `CgminerApiClient::ConnectionError`:
112
+
113
+ ```ruby
114
+ miner = CgminerApiClient::Miner.new('10.0.0.5', 4028)
115
+ begin
116
+ puts miner.summary[:mhs_av]
117
+ rescue CgminerApiClient::ConnectionError => e
118
+ warn "miner unreachable: #{e.message}"
119
+ end
120
+ ```
121
+
122
+ ### Privileged commands
123
+
124
+ Commands like `restart`, `quit`, `save`, `addpool`, `removepool`,
125
+ `ascset`, etc. require privileged API access on the cgminer side.
126
+ They propagate `CgminerApiClient::ApiError` on rejection and
127
+ `CgminerApiClient::ConnectionError` on network failure — two
128
+ distinct conditions, unlike in 0.2.x where they were conflated.
129
+
130
+ ```ruby
131
+ pool.restart # PoolResult of per-miner outcomes
132
+ ```
101
133
 
102
134
  ## CLI Usage
103
135
 
@@ -181,7 +213,7 @@ Any cgminer API commands not explictly defined above are implemented using `meth
181
213
 
182
214
  If you find this gem useful, please consider donating.
183
215
 
184
- BTC: `18HFFqZv2KJMHPNwPes839PJd5GZc4cT3U`
216
+ BTC: `bc1q00genlpcpcglgd4rezqcurf4t4taz0acmm9vea`
185
217
 
186
218
  ## License
187
219
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib/")
4
5
 
@@ -8,15 +9,33 @@ require 'pp'
8
9
  command = ARGV.shift&.to_sym
9
10
  commands = CgminerApiClient::Miner::Commands.instance_methods
10
11
 
11
- unless command and commands.include?(command)
12
- puts "USAGE: cgminer_api_client command (arguments)"
13
- puts "commands: " + commands.sort.collect(&:to_s).join(', ')
14
- exit(0)
12
+ unless command && commands.include?(command)
13
+ warn 'USAGE: cgminer_api_client command (arguments)'
14
+ warn "commands: #{commands.sort.join(', ')}"
15
+ warn ''
16
+ warn 'Set DEBUG=1 to see full backtraces on errors.'
17
+ exit 64 # EX_USAGE
15
18
  end
16
19
 
17
20
  begin
18
- miner_pool = CgminerApiClient::MinerPool.new
19
- pp(ARGV.empty? ? miner_pool.query(command) : miner_pool.query(command, *ARGV))
20
- rescue => e
21
- puts e.message
22
- end
21
+ pool = CgminerApiClient::MinerPool.new
22
+ result = ARGV.empty? ? pool.query(command) : pool.query(command, *ARGV)
23
+
24
+ result.each do |r|
25
+ if r.ok?
26
+ puts "#{r.miner.host}:#{r.miner.port}:"
27
+ pp r.value
28
+ else
29
+ warn "#{r.miner.host}:#{r.miner.port}: #{r.error.class}: #{r.error.message}"
30
+ end
31
+ end
32
+
33
+ # Exit 0 if at least one miner succeeded; 1 only if every miner
34
+ # failed. Matches how fleet-oriented CLIs (ping, ssh -o) typically
35
+ # behave — partial failures don't trigger alerts.
36
+ exit(result.any_succeeded? ? 0 : 1)
37
+ rescue StandardError => e
38
+ warn "cgminer_api_client: #{e.class}: #{e.message}"
39
+ warn e.full_message(highlight: false) if ENV['DEBUG']
40
+ exit 1
41
+ end
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'cgminer_api_client/version'
5
6
 
@@ -8,13 +9,31 @@ Gem::Specification.new do |spec|
8
9
  spec.version = CgminerApiClient::VERSION
9
10
  spec.authors = ["Justin Ramos"]
10
11
  spec.email = ["justin.ramos@gmail.com"]
11
- spec.summary = %q{A gem that allows sending API commands to a pool of cgminer instances}
12
- spec.description = %q{}
12
+ spec.summary = "A gem that allows sending API commands to a pool of cgminer instances"
13
+ spec.description = "Ruby client for the cgminer JSON API. Supports querying a single miner " \
14
+ "or a pool of miners in parallel, with full coverage of read-only and " \
15
+ "privileged commands."
13
16
  spec.homepage = "https://github.com/jramos/cgminer_api_client"
14
17
  spec.license = "MIT"
15
18
 
16
- spec.files = `git ls-files -z`.split("\x0")
19
+ spec.required_ruby_version = ">= 3.2"
20
+
21
+ spec.metadata = {
22
+ "source_code_uri" => spec.homepage,
23
+ "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md",
24
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
25
+ "rubygems_mfa_required" => "true"
26
+ }
27
+
28
+ spec.files = Dir.glob([
29
+ "lib/**/*.rb",
30
+ "bin/*",
31
+ "config/*.example",
32
+ "README.md",
33
+ "LICENSE.txt",
34
+ "CHANGELOG.md",
35
+ "cgminer_api_client.gemspec"
36
+ ])
17
37
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
38
  spec.require_paths = ["lib"]
20
39
  end
@@ -1,2 +1,3 @@
1
1
  - host: 127.0.0.1
2
- port: 4028
2
+ port: 4028
3
+ timeout: 5
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CgminerApiClient
4
+ # Base class for all errors raised by the gem. Catch this if you want
5
+ # to handle every cgminer-specific failure together.
6
+ class Error < StandardError; end
7
+
8
+ # Raised when the gem cannot reach a miner: socket open failure, DNS
9
+ # failure, connect timeout, or any other transport-level problem.
10
+ # Distinct from ApiError so callers can tell "I never spoke to the
11
+ # miner" apart from "the miner spoke to me and refused."
12
+ class ConnectionError < Error; end
13
+
14
+ # Raised specifically for connect-timeout failures, as a subclass
15
+ # of ConnectionError. Lets callers distinguish "the miner took too
16
+ # long to answer the SYN" from other connection-layer problems.
17
+ class TimeoutError < ConnectionError; end
18
+
19
+ # Raised when the miner returned a response whose STATUS field
20
+ # indicates an error (cgminer status code 'E' or 'F'). The message
21
+ # contains the cgminer code and message verbatim.
22
+ class ApiError < Error; end
23
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CgminerApiClient
2
4
  class Miner
3
5
  module Commands
@@ -43,11 +45,14 @@ module CgminerApiClient
43
45
  end
44
46
 
45
47
  def privileged
46
- query(:privileged)
47
- true
48
- rescue
49
- false
50
-
48
+ query(:privileged)
49
+ true
50
+ rescue CgminerApiClient::ApiError
51
+ # The miner answered and rejected: not privileged.
52
+ false
53
+ # ConnectionError and any other StandardError propagate so
54
+ # callers don't misinterpret a transient network blip as
55
+ # "access denied".
51
56
  end
52
57
 
53
58
  def notify
@@ -86,9 +91,9 @@ module CgminerApiClient
86
91
  end
87
92
 
88
93
  def ascset(number, option, value = nil)
89
- unless access_denied?
90
- value ? query(:ascset, number, option, value) : query(:ascset, number, option)
91
- end
94
+ return if access_denied?
95
+
96
+ value ? query(:ascset, number, option, value) : query(:ascset, number, option)
92
97
  end
93
98
  end
94
99
 
@@ -106,9 +111,9 @@ module CgminerApiClient
106
111
  end
107
112
 
108
113
  def pgaset(number, option, value = nil)
109
- unless access_denied?
110
- value ? query(:pgaset, number, option, value) : query(:pgaset, number, option)
111
- end
114
+ return if access_denied?
115
+
116
+ value ? query(:pgaset, number, option, value) : query(:pgaset, number, option)
112
117
  end
113
118
  end
114
119
 
@@ -164,9 +169,9 @@ module CgminerApiClient
164
169
  end
165
170
 
166
171
  def save(filename = nil)
167
- unless access_denied?
168
- filename ? query(:save, filename) : query(:save)
169
- end
172
+ return if access_denied?
173
+
174
+ filename ? query(:save, filename) : query(:save)
170
175
  end
171
176
 
172
177
  def setconfig(name, value)
@@ -181,11 +186,9 @@ module CgminerApiClient
181
186
  private
182
187
 
183
188
  def access_denied?
184
- if !privileged
185
- raise 'access_denied'
186
- else
187
- return false
188
- end
189
+ raise 'access_denied' unless privileged
190
+
191
+ false
189
192
  end
190
193
 
191
194
  include Miner::Commands::Privileged::Asc
@@ -198,4 +201,4 @@ module CgminerApiClient
198
201
  include Miner::Commands::Privileged
199
202
  end
200
203
  end
201
- end
204
+ end