omq-cli 0.14.1 → 0.14.3

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: 5352d1d8bdbab1b64aa1e7a36afcc1c70619eb0ae8373d78e95c77669d6a4fec
4
- data.tar.gz: 38172d3bbff810e3be322e3b8aa4e6d9e09520976d76b34e6721606dd8a15c95
3
+ metadata.gz: a207b74cefe2b70d8f5910610be8ef955a2d466ae78e3b620860a5425361af8b
4
+ data.tar.gz: 7a15e05713459c741f2d780c89057bbfee435e63767464245041bcdfe5b31c59
5
5
  SHA512:
6
- metadata.gz: 158eb775b0ccea84275a69dc39e8279f41fab5f9bfeb94e8ca0c9e5dd7342b6472980e925850b7d82eceb509981942c42cd80abcb52a9e871b335075898a05b6
7
- data.tar.gz: 8885731e2d99e87038bb6cffe9f4dc7edd480d285d4b2ac5b5f2244772433f70123e1ed49e9da7a9aa55944aab938bbbf10d7acc98975a04df6b45fd0a1d6b7c
6
+ metadata.gz: d38d7f5548147b2b5a4fdfe391180b29dba07975fe8fbd391586eaf580d739f66c05f66e0fbc488e976d47afaabbced2322940d39c1b783ed4c5636e5ce4d194
7
+ data.tar.gz: 301999bb8145e49e39d28f77dc75a71d22ead3ad3c01ebe85e809a73b2cc8baf3a59e980361dd43319d2c7d5be90ff7f22f74370ad89f7a53ae85e77e09634d9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.3 — 2026-04-14
4
+
5
+ ### Fixed
6
+
7
+ - **`omq req` (and other interactive senders) no longer wedge on
8
+ blank input lines.** `Formatter#decode` used to return `[]` for
9
+ a blank line, which `BaseRunner#send_msg` then silently dropped
10
+ — so REQ never sent a request but still blocked in `recv_msg`
11
+ waiting for a reply. Blank lines now decode to a single empty
12
+ frame (`[""]`) via the new
13
+ `Formatter::EMPTY_MSG = [Protocol::ZMTP::Codec::EMPTY_BINARY]`
14
+ constant, so the request actually goes out. As a side effect,
15
+ ascii/quoted decoding now uses `split("\t", -1)` and preserves
16
+ trailing empty frames (`"a\t\n"` → `["a", ""]`).
17
+ - **Cleaner `:disconnected` log lines on plain peer close.**
18
+ `-vv` used to emit `omq: disconnected tcp://… (Stream finished
19
+ before reading enough data!)` — the raw io-stream message for
20
+ what is really just an `EOFError` at the ZMTP framing boundary.
21
+ `Term.format_event` now routes through a new
22
+ `Term.format_event_detail` helper that rewrites `EOFError` to
23
+ `(closed by peer)`, leaving other errors' messages untouched.
24
+ The underlying `event.detail[:error]` is unchanged.
25
+
26
+ ### Changed
27
+
28
+ - **Lowercased `Term.format_attach` verbs.** `omq: Bound to …` /
29
+ `omq: Connecting to …` now render as `omq: bound to …` /
30
+ `omq: connecting to …`, matching the lowercase style already
31
+ used by every other `omq:` log line (`disconnected`, `listening`,
32
+ `handshake_succeeded`, …).
33
+
34
+ ## 0.14.2 — 2026-04-13
35
+
36
+ ### Changed
37
+
38
+ - `kill_on_protocol_error` is now a single
39
+ `SocketSetup.kill_on_protocol_error(sock, event)` class method.
40
+ Previously `BaseRunner`, `ParallelWorker`, and `PipeRunner` each
41
+ carried an identical 4-line copy of the CLI policy that
42
+ protocol-level disconnects mark the socket dead.
43
+ - `ExpressionEvaluator.extract_block` is now a single class method
44
+ used by both the instance compile path and the
45
+ `compile_inside_ractor` path. The in-Ractor copy previously lived
46
+ as a local lambda that duplicated the instance
47
+ `extract_block` method.
48
+ - `Formatter#encode` drops one String allocation per message on
49
+ the ascii / quoted / jsonl / marshal paths by mutating the
50
+ fresh `.join` / `JSON.generate` / `.inspect` result with `<<`
51
+ instead of `+ "\n"`.
52
+ - `Formatter.marshal_preview` and `Formatter.frames_preview` (extracted
53
+ from `Formatter.preview` in the `-vvv` marshal trace work) are now
54
+ `private_class_method` — they were only ever meant to be called
55
+ through `Formatter.preview` but ended up on the public class surface.
56
+ - Dropped a redundant unary `+` before `Formatter.sanitize(...)` in
57
+ `marshal_preview`: `sanitize` already returns a fresh mutable String
58
+ via `.tr`, so the `+""` dup was dead weight.
59
+ - **`-vvv` marshal trace headers now show plaintext and wire byte
60
+ sizes.** Previously `<< (marshal) ...` carried no size info; it
61
+ now renders as `(135B marshal) ...` and, when ZMTP-Zstd
62
+ compression is negotiated, `(135B wire=50B marshal) ...` —
63
+ matching the frame-based preview format used by every other
64
+ `-vvv` output.
65
+ Other formats (ascii/quoted/jsonl/msgpack/raw) already showed
66
+ plaintext size via the frame preview; they now also pick up
67
+ `wire=NB` when compression is active, since `wire_size` is
68
+ side-channelled from `:message_sent` / `:message_received`
69
+ monitor events. Send-side `wire_size` is best-effort — the
70
+ engine's send pump emits the compressed byte count
71
+ asynchronously, so the value reflects the most recently
72
+ *completed* send; receive-side is exact.
73
+ - Hot-path optimized.
74
+
3
75
  ## 0.14.1 — 2026-04-13
4
76
 
5
77
  ### Changed
@@ -304,9 +304,19 @@ module OMQ
304
304
  # may write to stdout (e.g. `-e 'p it'`), and we want the
305
305
  # trace line to precede any such output so the sequence on the
306
306
  # terminal reads as: trace → eval side-effects → body.
307
+ #
308
+ # +@last_recv_wire_size+ is populated by the :message_received
309
+ # monitor event, which fires *before* the recv queue enqueue
310
+ # (recv_pump.rb) — so by the time @sock.receive returns here,
311
+ # the cache reflects this message. +@last_recv_uncompressed+
312
+ # is captured in #recv_msg from the raw marshal frame size.
307
313
  def trace_recv(parts)
308
314
  return unless config.verbose >= 3
309
- $stderr.write("#{Term.log_prefix(config.timestamps)}omq: << #{Formatter.preview(parts, format: config.format)}\n")
315
+ preview = Formatter.preview(parts,
316
+ format: config.format,
317
+ wire_size: @last_recv_wire_size,
318
+ uncompressed_size: @last_recv_uncompressed)
319
+ $stderr.write("#{Term.log_prefix(config.timestamps)}omq: << #{preview}\n")
310
320
  $stderr.flush
311
321
  end
312
322
 
@@ -331,8 +341,9 @@ module OMQ
331
341
  def send_msg(parts)
332
342
  case config.format
333
343
  when :marshal
334
- trace_send(parts)
335
- @sock.send([Marshal.dump(parts)])
344
+ dumped = Marshal.dump(parts)
345
+ trace_send(parts, uncompressed_size: dumped.bytesize)
346
+ @sock.send([dumped])
336
347
  else
337
348
  return if parts.empty?
338
349
  trace_send(parts)
@@ -342,12 +353,21 @@ module OMQ
342
353
  end
343
354
 
344
355
 
345
- # Symmetric to #trace_recv — log the outgoing message *before*
346
- # Marshal.dump runs, so -M traces show the app-level object
347
- # (`[nil, :foo, "bar"]`) instead of the wire-side dump bytes.
348
- def trace_send(parts)
356
+ # Symmetric to #trace_recv — log the outgoing message using the
357
+ # pre-Marshal.dump +parts+, so -M traces show the app-level
358
+ # object (`[nil, :foo, "bar"]`) instead of the wire-side dump
359
+ # bytes. +@last_send_wire_size+ is best-effort: it reflects the
360
+ # *previous* message (populated by the :message_sent monitor
361
+ # event, which fires on a separate fiber after the pump writes),
362
+ # so early sends may show no `wire=` at all. Receive-side tracing
363
+ # is the authoritative path for observing wire bytes.
364
+ def trace_send(parts, uncompressed_size: nil)
349
365
  return unless config.verbose >= 3
350
- $stderr.write("#{Term.log_prefix(config.timestamps)}omq: >> #{Formatter.preview(parts, format: config.format)}\n")
366
+ preview = Formatter.preview(parts,
367
+ format: config.format,
368
+ wire_size: @last_send_wire_size,
369
+ uncompressed_size: uncompressed_size)
370
+ $stderr.write("#{Term.log_prefix(config.timestamps)}omq: >> #{preview}\n")
351
371
  $stderr.flush
352
372
  end
353
373
 
@@ -355,7 +375,13 @@ module OMQ
355
375
  def recv_msg
356
376
  parts = @sock.receive
357
377
  return nil if parts.nil?
358
- parts = Marshal.load(parts.first) if config.format == :marshal
378
+
379
+ case config.format
380
+ when :marshal
381
+ @last_recv_uncompressed = parts.first.bytesize
382
+ parts = Marshal.load(parts.first)
383
+ end
384
+
359
385
  transient_ready!
360
386
  parts
361
387
  end
@@ -499,37 +525,32 @@ module OMQ
499
525
  # -vvv also log message sent/received traces
500
526
  # --timestamps[=s|ms|us]: prepend UTC timestamps to log lines
501
527
  #
502
- # :message_received and :message_sent are intentionally skipped
503
- # here and traced from BaseRunner#trace_recv / #trace_send
504
- # instead same fiber as the body write, so trace-then-body
528
+ # :message_received and :message_sent are not *logged* from the
529
+ # monitor fiber #trace_recv / #trace_send render them inline
530
+ # on the same fiber as the body write, so trace-then-body
505
531
  # ordering is strict on a shared tty. The monitor-fiber path
506
532
  # suffered from $stderr/$stdout buffer races and from dumping
507
533
  # wire-side bytes (pre-Marshal.load on recv, post-Marshal.dump
508
- # on send) instead of app-level parts.
509
- SKIP_MONITOR_EVENTS = %i[message_received message_sent].freeze
534
+ # on send) instead of app-level parts. We still *observe* these
535
+ # events here to side-channel the compressed wire_size — for
536
+ # :message_received the event fires before the recv queue
537
+ # enqueue (engine/recv_pump.rb), so by the time @sock.receive
538
+ # returns, @last_recv_wire_size reflects the current message.
510
539
  def start_event_monitor
511
540
  trace = config.verbose >= 3
512
541
  log_events = config.verbose >= 2
513
542
  @sock.monitor(verbose: trace) do |event|
514
- Term.write_event(event, config.timestamps) if log_events && !SKIP_MONITOR_EVENTS.include?(event.type)
515
- kill_on_protocol_error(event)
543
+ case event.type
544
+ when :message_received
545
+ @last_recv_wire_size = event.detail[:wire_size]
546
+ when :message_sent
547
+ @last_send_wire_size = event.detail[:wire_size]
548
+ else
549
+ Term.write_event(event, config.timestamps) if log_events
550
+ end
551
+ SocketSetup.kill_on_protocol_error(@sock, event)
516
552
  end
517
553
  end
518
-
519
-
520
- # omq-cli policy: a peer that commits a protocol-level violation
521
- # (Protocol::ZMTP::Error — oversized frame, decompression
522
- # bytebomb, bad framing, …) is almost certainly a
523
- # misconfiguration the user needs to see. Mark the socket dead
524
- # so the next receive raises SocketDeadError. The library
525
- # itself just drops the connection and keeps serving the
526
- # others; this stricter policy is CLI-only.
527
- def kill_on_protocol_error(event)
528
- return unless event.type == :disconnected
529
- error = event.detail && event.detail[:error]
530
- return unless error.is_a?(Protocol::ZMTP::Error)
531
- @sock.engine.signal_fatal_error(error)
532
- end
533
554
  end
534
555
  end
535
556
  end
@@ -49,7 +49,7 @@ module OMQ
49
49
  return result if @format == :marshal
50
50
 
51
51
  result = result.is_a?(Array) ? result : [result]
52
- result.map(&:to_s)
52
+ result.map { |part| part.to_s }
53
53
  rescue => e
54
54
  $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
55
55
  exit 3
@@ -67,7 +67,7 @@ module OMQ
67
67
  return nil if result.nil?
68
68
  return result if format == :marshal
69
69
  result = result.is_a?(Array) ? result : [result]
70
- result.map(&:to_s)
70
+ result.map { |part| part.to_s }
71
71
  end
72
72
 
73
73
 
@@ -79,22 +79,8 @@ module OMQ
79
79
  def self.compile_inside_ractor(src)
80
80
  return [nil, nil, nil] unless src
81
81
 
82
- extract = ->(expr, kw) {
83
- s = expr.index(/#{kw}\s*\{/)
84
- return [expr, nil] unless s
85
- ci = expr.index("{", s)
86
- depth = 1
87
- j = ci + 1
88
- while j < expr.length && depth > 0
89
- depth += 1 if expr[j] == "{"
90
- depth -= 1 if expr[j] == "}"
91
- j += 1
92
- end
93
- [expr[0...s] + expr[j..], expr[(ci + 1)..(j - 2)]]
94
- }
95
-
96
- expr, begin_body = extract.(src, "BEGIN")
97
- expr, end_body = extract.(expr, "END")
82
+ expr, begin_body = extract_block(src, "BEGIN")
83
+ expr, end_body = extract_block(expr, "END")
98
84
 
99
85
  begin_proc = eval("proc { #{begin_body} }") if begin_body
100
86
  end_proc = eval("proc { #{end_body} }") if end_body
@@ -107,17 +93,13 @@ module OMQ
107
93
  end
108
94
 
109
95
 
110
- private
111
-
112
-
113
- def extract_blocks(expr)
114
- expr, begin_body = extract_block(expr, "BEGIN")
115
- expr, end_body = extract_block(expr, "END")
116
- [expr, begin_body, end_body]
117
- end
118
-
119
-
120
- def extract_block(expr, keyword)
96
+ # Strips a +BEGIN {...}+ or +END {...}+ block from +expr+ and
97
+ # returns +[trimmed_expr, block_body_or_nil]+. Brace-matched scan,
98
+ # so nested `{}` inside the block body are handled. Shared by
99
+ # instance and Ractor compile paths, so must be a class method
100
+ # (Ractors cannot call back into instance state).
101
+ #
102
+ def self.extract_block(expr, keyword)
121
103
  start = expr.index(/#{keyword}\s*\{/)
122
104
  return [expr, nil] unless start
123
105
 
@@ -138,6 +120,16 @@ module OMQ
138
120
  trimmed = expr[0...start] + expr[j..]
139
121
  [trimmed, body]
140
122
  end
123
+
124
+
125
+ private
126
+
127
+
128
+ def extract_blocks(expr)
129
+ expr, begin_body = self.class.extract_block(expr, "BEGIN")
130
+ expr, end_body = self.class.extract_block(expr, "END")
131
+ [expr, begin_body, end_body]
132
+ end
141
133
  end
142
134
  end
143
135
  end
@@ -7,6 +7,10 @@ module OMQ
7
7
  # (omq-rfc-zstd) once enabled via +socket.compression=+; the
8
8
  # formatter sees plaintext frames in both directions.
9
9
  class Formatter
10
+ # Single empty frame — used as the decoded form of a blank input line.
11
+ EMPTY_MSG = [::Protocol::ZMTP::Codec::EMPTY_BINARY].freeze
12
+
13
+
10
14
  # @param format [Symbol] wire format (:ascii, :quoted, :raw, :jsonl, :msgpack, :marshal)
11
15
  def initialize(format)
12
16
  @format = format
@@ -20,20 +24,20 @@ module OMQ
20
24
  def encode(parts)
21
25
  case @format
22
26
  when :ascii
23
- parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") + "\n"
27
+ parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") << "\n"
24
28
  when :quoted
25
- parts.map { |p| p.b.dump[1..-2] }.join("\t") + "\n"
29
+ parts.map { |p| p.b.dump[1..-2] }.join("\t") << "\n"
26
30
  when :raw
27
31
  parts.each_with_index.map do |p, i|
28
32
  Protocol::ZMTP::Codec::Frame.new(p.to_s, more: i < parts.size - 1).to_wire
29
33
  end.join
30
34
  when :jsonl
31
- JSON.generate(parts) + "\n"
35
+ JSON.generate(parts) << "\n"
32
36
  when :msgpack
33
37
  MessagePack.pack(parts)
34
38
  when :marshal
35
39
  # Under -M, `parts` is a single Ruby object (not a frame array).
36
- parts.inspect + "\n"
40
+ parts.inspect << "\n"
37
41
  end
38
42
  end
39
43
 
@@ -45,9 +49,11 @@ module OMQ
45
49
  def decode(line)
46
50
  case @format
47
51
  when :ascii, :marshal
48
- line.chomp.split("\t")
52
+ parts = line.chomp.split("\t", -1)
53
+ parts.empty? ? EMPTY_MSG : parts
49
54
  when :quoted
50
- line.chomp.split("\t").map { |p| "\"#{p}\"".undump }
55
+ parts = line.chomp.split("\t", -1).map { |p| "\"#{p}\"".undump }
56
+ parts.empty? ? EMPTY_MSG : parts
51
57
  when :raw
52
58
  [line]
53
59
  when :jsonl
@@ -103,37 +109,65 @@ module OMQ
103
109
  # itself (not an Array of frames); the preview inspects it so
104
110
  # the reader sees the actual payload structure (e.g.
105
111
  # `[nil, :foo, "bar"]`) instead of a meaningless "1obj" header.
112
+ # For marshal, +uncompressed_size+ is the Marshal.dump bytesize
113
+ # (known to the caller, which already serialized for send or
114
+ # received the wire frame for recv) — passed through instead of
115
+ # redumping here.
106
116
  #
107
117
  # @param parts [Array<String, Object>, Object] message frames, or raw object when +format+ is :marshal
108
118
  # @param format [Symbol, nil] active CLI format (:marshal enables object-inspect mode)
109
119
  # @param wire_size [Integer, nil] compressed bytes on the wire
120
+ # @param uncompressed_size [Integer, nil] plaintext bytes (marshal only)
110
121
  # @return [String] truncated preview of each frame joined by |
111
- def self.preview(parts, format: nil, wire_size: nil)
112
- if format == :marshal
113
- inspected = parts.inspect
114
- truncated = inspected.bytesize > 60
115
- inspected = inspected.byteslice(0, 60) if truncated
116
- out = +"(marshal) #{sanitize(inspected)}"
117
- out << "…" if truncated
118
- return out
122
+ def self.preview(parts, format: nil, wire_size: nil, uncompressed_size: nil)
123
+ case format
124
+ when :marshal
125
+ marshal_preview(parts, uncompressed_size: uncompressed_size, wire_size: wire_size)
126
+ else
127
+ frames_preview(parts, format: format, wire_size: wire_size)
119
128
  end
129
+ end
130
+
131
+
132
+ def self.marshal_preview(parts, uncompressed_size:, wire_size:)
133
+ inspected = parts.inspect
134
+ truncated = inspected.bytesize > 60
135
+ inspected = inspected.byteslice(0, 60) if truncated
136
+ body = sanitize(inspected)
137
+
138
+ body << "…" if truncated
139
+
140
+ header = case
141
+ when uncompressed_size && wire_size
142
+ "(#{uncompressed_size}B wire=#{wire_size}B marshal)"
143
+ when uncompressed_size
144
+ "(#{uncompressed_size}B marshal)"
145
+ else
146
+ "(marshal)"
147
+ end
148
+
149
+ "#{header} #{body}"
150
+ end
151
+ private_class_method :marshal_preview
152
+
120
153
 
154
+ def self.frames_preview(parts, format:, wire_size:)
121
155
  nparts = parts.size
122
156
  shown = parts.first(3).map { |p| preview_frame(p) }
123
157
  tail = nparts > 3 ? "|…" : ""
124
- total = parts.all?(String) ? parts.sum(&:bytesize) : nil
125
- size =
126
- if wire_size && total
127
- "#{total}B wire=#{wire_size}B"
128
- elsif total
129
- "#{total}B"
130
- else
131
- "#{nparts}obj"
132
- end
158
+ total = parts.all?(String) ? parts.sum { |p| p.bytesize } : nil
159
+ size = if wire_size && total
160
+ "#{total}B wire=#{wire_size}B"
161
+ elsif total
162
+ "#{total}B"
163
+ else
164
+ "#{nparts}obj"
165
+ end
133
166
  header = nparts > 1 ? "(#{size} #{nparts}F)" : "(#{size})"
134
167
 
135
168
  "#{header} #{shown.join("|")}#{tail}"
136
169
  end
170
+ private_class_method :frames_preview
137
171
 
138
172
 
139
173
  # Renders one frame or decoded object for {Formatter.preview}.
@@ -70,22 +70,11 @@ module OMQ
70
70
  log_events = @config.verbose >= 2
71
71
  @sock.monitor(verbose: trace) do |event|
72
72
  @log_port.send(OMQ::CLI::Term.format_event(event, @config.timestamps)) if log_events
73
- kill_on_protocol_error(event)
73
+ OMQ::CLI::SocketSetup.kill_on_protocol_error(@sock, event)
74
74
  end
75
75
  end
76
76
 
77
77
 
78
- # Mirrors BaseRunner#kill_on_protocol_error: CLI-level policy
79
- # that protocol-level disconnects kill the socket so the
80
- # recv loop unblocks with SocketDeadError.
81
- def kill_on_protocol_error(event)
82
- return unless event.type == :disconnected
83
- error = event.detail && event.detail[:error]
84
- return unless error.is_a?(Protocol::ZMTP::Error)
85
- @sock.engine.signal_fatal_error(error)
86
- end
87
-
88
-
89
78
  def wait_for_peer
90
79
  if @config.timeout
91
80
  Fiber.scheduler.with_timeout(@config.timeout) do
data/lib/omq/cli/pipe.rb CHANGED
@@ -203,18 +203,10 @@ module OMQ
203
203
  [@pull, @push].each do |sock|
204
204
  sock.monitor(verbose: trace) do |event|
205
205
  Term.write_event(event, config.timestamps) if log_events
206
- kill_on_protocol_error(sock, event)
206
+ SocketSetup.kill_on_protocol_error(sock, event)
207
207
  end
208
208
  end
209
209
  end
210
-
211
-
212
- def kill_on_protocol_error(sock, event)
213
- return unless event.type == :disconnected
214
- error = event.detail && event.detail[:error]
215
- return unless error.is_a?(Protocol::ZMTP::Error)
216
- sock.engine.signal_fatal_error(error)
217
- end
218
210
  end
219
211
  end
220
212
  end
@@ -179,6 +179,24 @@ module OMQ
179
179
  $stderr.puts "OMQ_SERVER_KEY='#{Protocol::ZMTP::Z85.encode(server_pub)}'"
180
180
  end
181
181
  end
182
+
183
+
184
+ # CLI-level policy: a peer that commits a protocol-level violation
185
+ # (Protocol::ZMTP::Error — oversized frame, decompression bytebomb,
186
+ # bad framing, …) is almost certainly a misconfiguration the user
187
+ # needs to see. Mark +sock+ dead so the next receive raises
188
+ # SocketDeadError. The library itself just drops the connection and
189
+ # keeps serving the others; this stricter policy is CLI-only.
190
+ #
191
+ # @param sock [OMQ::Socket]
192
+ # @param event [OMQ::MonitorEvent]
193
+ #
194
+ def self.kill_on_protocol_error(sock, event)
195
+ return unless event.type == :disconnected
196
+ error = event.detail && event.detail[:error]
197
+ return unless error.is_a?(Protocol::ZMTP::Error)
198
+ sock.engine.signal_fatal_error(error)
199
+ end
182
200
  end
183
201
  end
184
202
  end
data/lib/omq/cli/term.rb CHANGED
@@ -49,27 +49,46 @@ module OMQ
49
49
  "#{prefix}omq: << ZDICT (#{event.detail[:size]}B)"
50
50
  else
51
51
  ep = event.endpoint ? " #{event.endpoint}" : ""
52
- detail =
53
- if event.detail.is_a?(Hash) && event.detail[:reason]
54
- " (#{event.detail[:reason]})"
55
- elsif event.detail
56
- " #{event.detail}"
57
- else
58
- ""
59
- end
52
+ detail = format_event_detail(event.detail)
60
53
  "#{prefix}omq: #{event.type}#{ep}#{detail}"
61
54
  end
62
55
  end
63
56
 
64
57
 
65
- # Formats an "attached endpoint" log line (Bound to / Connecting to).
58
+ # Renders +MonitorEvent#detail+ as a " (...)" suffix for log lines.
59
+ # Rewrites plain peer-close exceptions (EOFError) to "closed by
60
+ # peer" — the io-stream library reports these as "Stream finished
61
+ # before reading enough data!", which is confusing noise for what
62
+ # is just a normal disconnect.
63
+ #
64
+ # @param detail [Hash, Object, nil]
65
+ # @return [String]
66
+ def format_event_detail(detail)
67
+ return "" if detail.nil?
68
+ return " #{detail}" unless detail.is_a?(Hash)
69
+
70
+ error = detail[:error]
71
+ reason = detail[:reason]
72
+
73
+ case error
74
+ when nil
75
+ reason ? " (#{reason})" : ""
76
+ when EOFError
77
+ " (closed by peer)"
78
+ else
79
+ " (#{reason || error.message})"
80
+ end
81
+ end
82
+
83
+
84
+ # Formats an "attached endpoint" log line (bound to / connecting to).
66
85
  #
67
86
  # @param kind [:bind, :connect]
68
87
  # @param url [String]
69
88
  # @param timestamps [Symbol, nil]
70
89
  # @return [String]
71
90
  def format_attach(kind, url, timestamps)
72
- verb = kind == :bind ? "Bound to" : "Connecting to"
91
+ verb = kind == :bind ? "bound to" : "connecting to"
73
92
  "#{log_prefix(timestamps)}omq: #{verb} #{url}"
74
93
  end
75
94
 
@@ -85,7 +104,7 @@ module OMQ
85
104
  end
86
105
 
87
106
 
88
- # Writes one "Bound to / Connecting to" line to +io+
107
+ # Writes one "bound to / connecting to" line to +io+
89
108
  # (default $stderr).
90
109
  #
91
110
  # @param kind [:bind, :connect]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.14.1"
5
+ VERSION = "0.14.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.14.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: '0.19'
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 0.19.2
21
+ version: 0.19.3
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,7 +28,7 @@ dependencies:
28
28
  version: '0.19'
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 0.19.2
31
+ version: 0.19.3
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: omq-ffi
34
34
  requirement: !ruby/object:Gem::Requirement