omq-cli 0.14.0 → 0.14.1
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 +37 -0
- data/README.md +15 -1
- data/lib/omq/cli/base_runner.rb +44 -14
- data/lib/omq/cli/cli_parser.rb +17 -1
- data/lib/omq/cli/expression_evaluator.rb +7 -5
- data/lib/omq/cli/formatter.rb +69 -7
- data/lib/omq/cli/radio_dish.rb +10 -4
- data/lib/omq/cli/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5352d1d8bdbab1b64aa1e7a36afcc1c70619eb0ae8373d78e95c77669d6a4fec
|
|
4
|
+
data.tar.gz: 38172d3bbff810e3be322e3b8aa4e6d9e09520976d76b34e6721606dd8a15c95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 158eb775b0ccea84275a69dc39e8279f41fab5f9bfeb94e8ca0c9e5dd7342b6472980e925850b7d82eceb509981942c42cd80abcb52a9e871b335075898a05b6
|
|
7
|
+
data.tar.gz: 8885731e2d99e87038bb6cffe9f4dc7edd480d285d4b2ac5b5f2244772433f70123e1ed49e9da7a9aa55944aab938bbbf10d7acc98975a04df6b45fd0a1d6b7c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.1 — 2026-04-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`-M` (Marshal) now carries raw Ruby objects, not array-wrapped
|
|
8
|
+
frames.** Under `-M`, each wire frame is one Marshal-dumped Ruby
|
|
9
|
+
object; inside `-e` / `-E`, `it` is that object directly (not
|
|
10
|
+
`[object]`). Enables natural scalar/hash/custom-class flows:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
omq push -b tcp://:5557 -ME '"foo"'
|
|
14
|
+
omq pull -c tcp://:5557 -M -e '{it => it.encoding}'
|
|
15
|
+
# => {"foo" => #<Encoding:UTF-8>}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The previous one-element-Array wrap was cosmetic — it always
|
|
19
|
+
produced exactly one wire frame anyway — so no multipart
|
|
20
|
+
semantics are lost.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`-vvv` trace lines now precede stdout side-effects from
|
|
25
|
+
`-e` / `-E`.** A `<<` / `>>` line is emitted from the app fiber
|
|
26
|
+
*before* the eval expression runs, so sequences like
|
|
27
|
+
`-e 'p it'` read strictly as `trace → eval output → body` on a
|
|
28
|
+
shared tty. Previous design emitted traces from the monitor
|
|
29
|
+
fiber and raced with stdout.
|
|
30
|
+
- **`-vvv` under `-M` now shows the app-level object, not wire
|
|
31
|
+
bytes.** Preview header switches to `(marshal) <inspect>` with
|
|
32
|
+
sanitization and 60-byte truncation, e.g.
|
|
33
|
+
`<< (marshal) [nil, :foo, "bar"]`.
|
|
34
|
+
- **`-vvv` trace preview sanitizes control characters.** Tabs,
|
|
35
|
+
newlines, CR, and backslash render as `\t`, `\n`, `\r`, `\\`;
|
|
36
|
+
other non-printables collapse to `.`. Previously raw LF inside
|
|
37
|
+
a binary frame could leak and break the single-line guarantee.
|
|
38
|
+
- Test suite runs cleanly without protocol-error stderr noise.
|
|
39
|
+
|
|
3
40
|
## 0.14.0 — 2026-04-13
|
|
4
41
|
|
|
5
42
|
### Added
|
data/README.md
CHANGED
|
@@ -312,7 +312,7 @@ OMQ.outgoing { |msg| [*msg, Time.now.iso8601] }
|
|
|
312
312
|
| `--raw` | Raw ZMTP binary (pipe to `hexdump -C` for debugging) |
|
|
313
313
|
| `-J` / `--jsonl` | JSON Lines — `["frame1","frame2"]` per line |
|
|
314
314
|
| `--msgpack` | MessagePack arrays (binary stream) |
|
|
315
|
-
| `-M` / `--marshal` | Ruby Marshal
|
|
315
|
+
| `-M` / `--marshal` | Ruby Marshal — one arbitrary Ruby object per message |
|
|
316
316
|
|
|
317
317
|
Multipart messages: in ASCII/quoted mode, frames are tab-separated. In JSONL mode,
|
|
318
318
|
each message is a JSON array.
|
|
@@ -326,6 +326,20 @@ echo '["key","value"]' | omq push -c tcp://localhost:5557 -J
|
|
|
326
326
|
omq pull -b tcp://:5557 -J
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
+
Under `-M`, each wire frame is one Marshal-dumped Ruby object. Inside `-e` / `-E`,
|
|
330
|
+
`it` is that raw object — not an Array of frames — so scalars, hashes, custom
|
|
331
|
+
classes, or any Marshal-safe value flow through transparently:
|
|
332
|
+
|
|
333
|
+
```sh
|
|
334
|
+
# send a bare String, receive a { string => encoding } Hash
|
|
335
|
+
omq push -b tcp://:5557 -ME '"foo"'
|
|
336
|
+
omq pull -c tcp://:5557 -M -e '{it => it.encoding}'
|
|
337
|
+
# => {"foo" => #<Encoding:UTF-8>}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
At `-vvv`, trace lines for `-M` render the app-level object instead of wire
|
|
341
|
+
bytes: `omq: >> (marshal) [nil, :foo, "bar"]`.
|
|
342
|
+
|
|
329
343
|
## Timing
|
|
330
344
|
|
|
331
345
|
| Flag | Effect |
|
data/lib/omq/cli/base_runner.rb
CHANGED
|
@@ -266,6 +266,7 @@ module OMQ
|
|
|
266
266
|
loop do
|
|
267
267
|
parts = recv_msg
|
|
268
268
|
break if parts.nil?
|
|
269
|
+
trace_recv(parts)
|
|
269
270
|
parts = eval_recv_expr(parts)
|
|
270
271
|
output(parts)
|
|
271
272
|
i += 1
|
|
@@ -292,12 +293,24 @@ module OMQ
|
|
|
292
293
|
@recv_tick_eof = true
|
|
293
294
|
return 0
|
|
294
295
|
end
|
|
296
|
+
trace_recv(parts)
|
|
295
297
|
parts = eval_recv_expr(parts)
|
|
296
298
|
output(parts)
|
|
297
299
|
1
|
|
298
300
|
end
|
|
299
301
|
|
|
300
302
|
|
|
303
|
+
# At -vvv, log the received message *before* eval runs. Eval
|
|
304
|
+
# may write to stdout (e.g. `-e 'p it'`), and we want the
|
|
305
|
+
# trace line to precede any such output so the sequence on the
|
|
306
|
+
# terminal reads as: trace → eval side-effects → body.
|
|
307
|
+
def trace_recv(parts)
|
|
308
|
+
return unless config.verbose >= 3
|
|
309
|
+
$stderr.write("#{Term.log_prefix(config.timestamps)}omq: << #{Formatter.preview(parts, format: config.format)}\n")
|
|
310
|
+
$stderr.flush
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
|
|
301
314
|
def wait_for_loops(receiver, sender)
|
|
302
315
|
if config.data || config.file || config.send_expr || config.recv_expr || config.target
|
|
303
316
|
sender.wait
|
|
@@ -316,13 +329,29 @@ module OMQ
|
|
|
316
329
|
|
|
317
330
|
|
|
318
331
|
def send_msg(parts)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
332
|
+
case config.format
|
|
333
|
+
when :marshal
|
|
334
|
+
trace_send(parts)
|
|
335
|
+
@sock.send([Marshal.dump(parts)])
|
|
336
|
+
else
|
|
337
|
+
return if parts.empty?
|
|
338
|
+
trace_send(parts)
|
|
339
|
+
@sock.send(parts)
|
|
340
|
+
end
|
|
322
341
|
transient_ready!
|
|
323
342
|
end
|
|
324
343
|
|
|
325
344
|
|
|
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)
|
|
349
|
+
return unless config.verbose >= 3
|
|
350
|
+
$stderr.write("#{Term.log_prefix(config.timestamps)}omq: >> #{Formatter.preview(parts, format: config.format)}\n")
|
|
351
|
+
$stderr.flush
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
|
|
326
355
|
def recv_msg
|
|
327
356
|
parts = @sock.receive
|
|
328
357
|
return nil if parts.nil?
|
|
@@ -392,10 +421,6 @@ module OMQ
|
|
|
392
421
|
|
|
393
422
|
def output(parts)
|
|
394
423
|
return if config.quiet || parts.nil?
|
|
395
|
-
# At -vvv, the monitor fiber owns both the trace and the body
|
|
396
|
-
# output (see start_event_monitor). Skipping the app-side write
|
|
397
|
-
# avoids interleaving between the two fibers on a shared tty.
|
|
398
|
-
return if config.verbose >= 3
|
|
399
424
|
$stdout.write(@fmt.encode(parts))
|
|
400
425
|
$stdout.flush
|
|
401
426
|
end
|
|
@@ -473,15 +498,20 @@ module OMQ
|
|
|
473
498
|
# -vv log connect/disconnect/retry/timeout events
|
|
474
499
|
# -vvv also log message sent/received traces
|
|
475
500
|
# --timestamps[=s|ms|us]: prepend UTC timestamps to log lines
|
|
501
|
+
#
|
|
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
|
|
505
|
+
# ordering is strict on a shared tty. The monitor-fiber path
|
|
506
|
+
# suffered from $stderr/$stdout buffer races and from dumping
|
|
507
|
+
# 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
|
|
476
510
|
def start_event_monitor
|
|
477
|
-
trace
|
|
478
|
-
log_events
|
|
511
|
+
trace = config.verbose >= 3
|
|
512
|
+
log_events = config.verbose >= 2
|
|
479
513
|
@sock.monitor(verbose: trace) do |event|
|
|
480
|
-
Term.write_event(event, config.timestamps) if log_events
|
|
481
|
-
if trace && event.type == :message_received && !config.quiet
|
|
482
|
-
$stdout.write(@fmt.encode(event.detail[:parts]))
|
|
483
|
-
$stdout.flush
|
|
484
|
-
end
|
|
514
|
+
Term.write_event(event, config.timestamps) if log_events && !SKIP_MONITOR_EVENTS.include?(event.type)
|
|
485
515
|
kill_on_protocol_error(event)
|
|
486
516
|
end
|
|
487
517
|
end
|
data/lib/omq/cli/cli_parser.rb
CHANGED
|
@@ -123,6 +123,22 @@ module OMQ
|
|
|
123
123
|
# multipart via tabs
|
|
124
124
|
printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
|
|
125
125
|
|
|
126
|
+
-- Marshal (arbitrary Ruby objects) -------------------------
|
|
127
|
+
|
|
128
|
+
# With -M, each message on the wire is one Marshal-dumped
|
|
129
|
+
# Ruby object. Inside -e/-E, `it` is that raw object — not
|
|
130
|
+
# an Array of frames — so you can send/receive scalars,
|
|
131
|
+
# hashes, custom classes, whatever Marshal handles.
|
|
132
|
+
|
|
133
|
+
# send a bare string, receive a { string => encoding } hash
|
|
134
|
+
omq push -b tcp://:5557 -ME '"foo"'
|
|
135
|
+
omq pull -c tcp://:5557 -Mvvv -e '{it => it.encoding}'
|
|
136
|
+
# output: {"foo" => #<Encoding:UTF-8>}
|
|
137
|
+
|
|
138
|
+
# -vvv traces render the app object, not wire bytes
|
|
139
|
+
omq push -b tcp://:5557 -ME '{now: Time.now, pid: Process.pid}' -vvv
|
|
140
|
+
# >> (marshal) {now: 2026-04-13 ..., pid: 12345}
|
|
141
|
+
|
|
126
142
|
-- Compression ----------------------------------------------
|
|
127
143
|
|
|
128
144
|
# ZMTP-Zstd is negotiated transparently during the handshake.
|
|
@@ -344,7 +360,7 @@ module OMQ
|
|
|
344
360
|
o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
|
|
345
361
|
o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
|
|
346
362
|
o.on( "--msgpack", "MessagePack arrays (binary stream)") { require "msgpack"; opts[:format] = :msgpack }
|
|
347
|
-
o.on("-M", "--marshal", "Ruby Marshal stream (
|
|
363
|
+
o.on("-M", "--marshal", "Ruby Marshal stream (one arbitrary object per message)") { opts[:format] = :marshal }
|
|
348
364
|
|
|
349
365
|
o.separator "\nSubscription/groups:"
|
|
350
366
|
o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
|
|
@@ -46,7 +46,7 @@ module OMQ
|
|
|
46
46
|
result = context.instance_exec(parts, &@eval_proc)
|
|
47
47
|
return nil if result.nil?
|
|
48
48
|
return SENT if result.equal?(context)
|
|
49
|
-
return
|
|
49
|
+
return result if @format == :marshal
|
|
50
50
|
|
|
51
51
|
result = result.is_a?(Array) ? result : [result]
|
|
52
52
|
result.map(&:to_s)
|
|
@@ -56,14 +56,16 @@ module OMQ
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
# Normalises an eval result to nil (skip)
|
|
59
|
+
# Normalises an eval result to nil (skip), an Array (text formats),
|
|
60
|
+
# or an arbitrary Ruby object (+:marshal+).
|
|
61
|
+
#
|
|
60
62
|
# Used inside Ractor worker blocks where instance methods are unavailable.
|
|
61
|
-
# When +format+ is :marshal,
|
|
62
|
-
#
|
|
63
|
+
# When +format+ is :marshal, the raw result is passed through — the
|
|
64
|
+
# wire path will Marshal.dump it into a single frame.
|
|
63
65
|
#
|
|
64
66
|
def self.normalize_result(result, format: nil)
|
|
65
67
|
return nil if result.nil?
|
|
66
|
-
return
|
|
68
|
+
return result if format == :marshal
|
|
67
69
|
result = result.is_a?(Array) ? result : [result]
|
|
68
70
|
result.map(&:to_s)
|
|
69
71
|
end
|
data/lib/omq/cli/formatter.rb
CHANGED
|
@@ -32,7 +32,8 @@ module OMQ
|
|
|
32
32
|
when :msgpack
|
|
33
33
|
MessagePack.pack(parts)
|
|
34
34
|
when :marshal
|
|
35
|
-
parts
|
|
35
|
+
# Under -M, `parts` is a single Ruby object (not a frame array).
|
|
36
|
+
parts.inspect + "\n"
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
|
|
@@ -80,26 +81,74 @@ module OMQ
|
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
# Whitespace/backslash → visible escape sequence used by
|
|
85
|
+
# {Formatter.sanitize}. Everything else outside printable ASCII
|
|
86
|
+
# collapses to '.' via a single String#tr call.
|
|
87
|
+
LINE_ESCAPES = {
|
|
88
|
+
"\t" => '\\t',
|
|
89
|
+
"\n" => '\\n',
|
|
90
|
+
"\r" => '\\r',
|
|
91
|
+
"\\" => '\\\\',
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
|
|
83
95
|
# Formats message parts for human-readable preview (logging).
|
|
84
96
|
# When +wire_size+ is given (ZMTP-Zstd negotiated), the header
|
|
85
97
|
# also shows the compressed on-the-wire size: "(29B wire=12B)".
|
|
98
|
+
# Accepts either wire-side Array<String> (monitor events) or
|
|
99
|
+
# post-decode app parts that may contain non-String objects
|
|
100
|
+
# (e.g. -M Marshal.load output).
|
|
101
|
+
#
|
|
102
|
+
# When +format+ is +:marshal+, +parts+ is the raw Ruby object
|
|
103
|
+
# itself (not an Array of frames); the preview inspects it so
|
|
104
|
+
# the reader sees the actual payload structure (e.g.
|
|
105
|
+
# `[nil, :foo, "bar"]`) instead of a meaningless "1obj" header.
|
|
86
106
|
#
|
|
87
|
-
# @param parts [Array<String
|
|
107
|
+
# @param parts [Array<String, Object>, Object] message frames, or raw object when +format+ is :marshal
|
|
108
|
+
# @param format [Symbol, nil] active CLI format (:marshal enables object-inspect mode)
|
|
88
109
|
# @param wire_size [Integer, nil] compressed bytes on the wire
|
|
89
110
|
# @return [String] truncated preview of each frame joined by |
|
|
90
|
-
def self.preview(parts, wire_size: nil)
|
|
91
|
-
|
|
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
|
|
119
|
+
end
|
|
120
|
+
|
|
92
121
|
nparts = parts.size
|
|
93
122
|
shown = parts.first(3).map { |p| preview_frame(p) }
|
|
94
123
|
tail = nparts > 3 ? "|…" : ""
|
|
95
|
-
|
|
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
|
|
96
133
|
header = nparts > 1 ? "(#{size} #{nparts}F)" : "(#{size})"
|
|
97
134
|
|
|
98
135
|
"#{header} #{shown.join("|")}#{tail}"
|
|
99
136
|
end
|
|
100
137
|
|
|
101
138
|
|
|
139
|
+
# Renders one frame or decoded object for {Formatter.preview}.
|
|
140
|
+
# Strings are sanitized byte-wise (first 12 bytes); non-String
|
|
141
|
+
# objects fall back to #inspect (always single-line) truncated
|
|
142
|
+
# at 24 bytes.
|
|
143
|
+
#
|
|
144
|
+
# @param part [String, Object]
|
|
145
|
+
# @return [String]
|
|
102
146
|
def self.preview_frame(part)
|
|
147
|
+
unless part.is_a?(String)
|
|
148
|
+
s = part.inspect
|
|
149
|
+
return s.bytesize > 24 ? "#{s.byteslice(0, 24)}…" : s
|
|
150
|
+
end
|
|
151
|
+
|
|
103
152
|
bytes = part.b
|
|
104
153
|
# Empty frames must render as a visible marker, not as the empty
|
|
105
154
|
# string — otherwise joining with "|" would produce misleading
|
|
@@ -113,11 +162,24 @@ module OMQ
|
|
|
113
162
|
if printable < sample.bytesize / 2
|
|
114
163
|
"[#{bytes.bytesize}B]"
|
|
115
164
|
elsif bytes.bytesize > 12
|
|
116
|
-
"#{sample
|
|
165
|
+
"#{sanitize(sample)}…"
|
|
117
166
|
else
|
|
118
|
-
sample
|
|
167
|
+
sanitize(sample)
|
|
119
168
|
end
|
|
120
169
|
end
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Escapes bytes so a preview/body line is guaranteed single-line
|
|
173
|
+
# on a shared tty. Tab/newline/CR/backslash render as literal
|
|
174
|
+
# \t/\n/\r/\\; other non-printables collapse to '.'. Forced to
|
|
175
|
+
# binary encoding first to prevent UTF-8 quirks from rendering
|
|
176
|
+
# raw LF bytes.
|
|
177
|
+
#
|
|
178
|
+
# @param bytes [String]
|
|
179
|
+
# @return [String]
|
|
180
|
+
def self.sanitize(bytes)
|
|
181
|
+
bytes.b.gsub(/[\t\n\r\\]/, LINE_ESCAPES).tr("^ -~", ".")
|
|
182
|
+
end
|
|
121
183
|
end
|
|
122
184
|
end
|
|
123
185
|
end
|
data/lib/omq/cli/radio_dish.rb
CHANGED
|
@@ -11,10 +11,16 @@ module OMQ
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def send_msg(parts)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
case config.format
|
|
15
|
+
when :marshal
|
|
16
|
+
trace_send(parts)
|
|
17
|
+
@sock.publish(config.group || "", Marshal.dump(parts))
|
|
18
|
+
else
|
|
19
|
+
return if parts.empty?
|
|
20
|
+
trace_send(parts)
|
|
21
|
+
group = config.group || parts.shift
|
|
22
|
+
@sock.publish(group, parts.first || "")
|
|
23
|
+
end
|
|
18
24
|
transient_ready!
|
|
19
25
|
end
|
|
20
26
|
end
|
data/lib/omq/cli/version.rb
CHANGED