omq-cli 0.14.0 → 0.14.2
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 +78 -0
- data/README.md +15 -1
- data/lib/omq/cli/base_runner.rb +81 -30
- data/lib/omq/cli/cli_parser.rb +17 -1
- data/lib/omq/cli/expression_evaluator.rb +28 -34
- data/lib/omq/cli/formatter.rb +100 -10
- data/lib/omq/cli/parallel_worker.rb +1 -12
- data/lib/omq/cli/pipe.rb +1 -9
- data/lib/omq/cli/radio_dish.rb +10 -4
- data/lib/omq/cli/socket_setup.rb +18 -0
- data/lib/omq/cli/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23eb8d602298e5db1259e6d7d0effe08e8e0dfacba55b8d9a5287800290f2eb9
|
|
4
|
+
data.tar.gz: b492d78fd4997c82434853bc284f7f98c662d905a450e3545c1df2ae822bf40c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3abd80d9856c52e2674c86a3e31ed4f02d6511cad2e3d9f35a2ab5b65faaf07acdc4c442f3746bc5ed0dd56e730df0fabc4930c0cdfe05ad8e289335fc0e8d4
|
|
7
|
+
data.tar.gz: 98e5cf25c2e267aace41bb958843459ead62da2a338b8a64aa3f6fc3af571dfefaee30b01a0de720b8880a96c6c0c3b55fe7a8439a6b5151a54085233ded3aaa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.2 — 2026-04-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `kill_on_protocol_error` is now a single
|
|
8
|
+
`SocketSetup.kill_on_protocol_error(sock, event)` class method.
|
|
9
|
+
Previously `BaseRunner`, `ParallelWorker`, and `PipeRunner` each
|
|
10
|
+
carried an identical 4-line copy of the CLI policy that
|
|
11
|
+
protocol-level disconnects mark the socket dead.
|
|
12
|
+
- `ExpressionEvaluator.extract_block` is now a single class method
|
|
13
|
+
used by both the instance compile path and the
|
|
14
|
+
`compile_inside_ractor` path. The in-Ractor copy previously lived
|
|
15
|
+
as a local lambda that duplicated the instance
|
|
16
|
+
`extract_block` method.
|
|
17
|
+
- `Formatter#encode` drops one String allocation per message on
|
|
18
|
+
the ascii / quoted / jsonl / marshal paths by mutating the
|
|
19
|
+
fresh `.join` / `JSON.generate` / `.inspect` result with `<<`
|
|
20
|
+
instead of `+ "\n"`.
|
|
21
|
+
- `Formatter.marshal_preview` and `Formatter.frames_preview` (extracted
|
|
22
|
+
from `Formatter.preview` in the `-vvv` marshal trace work) are now
|
|
23
|
+
`private_class_method` — they were only ever meant to be called
|
|
24
|
+
through `Formatter.preview` but ended up on the public class surface.
|
|
25
|
+
- Dropped a redundant unary `+` before `Formatter.sanitize(...)` in
|
|
26
|
+
`marshal_preview`: `sanitize` already returns a fresh mutable String
|
|
27
|
+
via `.tr`, so the `+""` dup was dead weight.
|
|
28
|
+
- **`-vvv` marshal trace headers now show plaintext and wire byte
|
|
29
|
+
sizes.** Previously `<< (marshal) ...` carried no size info; it
|
|
30
|
+
now renders as `(135B marshal) ...` and, when ZMTP-Zstd
|
|
31
|
+
compression is negotiated, `(135B wire=50B marshal) ...` —
|
|
32
|
+
matching the frame-based preview format used by every other
|
|
33
|
+
`-vvv` output.
|
|
34
|
+
Other formats (ascii/quoted/jsonl/msgpack/raw) already showed
|
|
35
|
+
plaintext size via the frame preview; they now also pick up
|
|
36
|
+
`wire=NB` when compression is active, since `wire_size` is
|
|
37
|
+
side-channelled from `:message_sent` / `:message_received`
|
|
38
|
+
monitor events. Send-side `wire_size` is best-effort — the
|
|
39
|
+
engine's send pump emits the compressed byte count
|
|
40
|
+
asynchronously, so the value reflects the most recently
|
|
41
|
+
*completed* send; receive-side is exact.
|
|
42
|
+
- Hot-path optimized.
|
|
43
|
+
|
|
44
|
+
## 0.14.1 — 2026-04-13
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **`-M` (Marshal) now carries raw Ruby objects, not array-wrapped
|
|
49
|
+
frames.** Under `-M`, each wire frame is one Marshal-dumped Ruby
|
|
50
|
+
object; inside `-e` / `-E`, `it` is that object directly (not
|
|
51
|
+
`[object]`). Enables natural scalar/hash/custom-class flows:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
omq push -b tcp://:5557 -ME '"foo"'
|
|
55
|
+
omq pull -c tcp://:5557 -M -e '{it => it.encoding}'
|
|
56
|
+
# => {"foo" => #<Encoding:UTF-8>}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The previous one-element-Array wrap was cosmetic — it always
|
|
60
|
+
produced exactly one wire frame anyway — so no multipart
|
|
61
|
+
semantics are lost.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **`-vvv` trace lines now precede stdout side-effects from
|
|
66
|
+
`-e` / `-E`.** A `<<` / `>>` line is emitted from the app fiber
|
|
67
|
+
*before* the eval expression runs, so sequences like
|
|
68
|
+
`-e 'p it'` read strictly as `trace → eval output → body` on a
|
|
69
|
+
shared tty. Previous design emitted traces from the monitor
|
|
70
|
+
fiber and raced with stdout.
|
|
71
|
+
- **`-vvv` under `-M` now shows the app-level object, not wire
|
|
72
|
+
bytes.** Preview header switches to `(marshal) <inspect>` with
|
|
73
|
+
sanitization and 60-byte truncation, e.g.
|
|
74
|
+
`<< (marshal) [nil, :foo, "bar"]`.
|
|
75
|
+
- **`-vvv` trace preview sanitizes control characters.** Tabs,
|
|
76
|
+
newlines, CR, and backslash render as `\t`, `\n`, `\r`, `\\`;
|
|
77
|
+
other non-printables collapse to `.`. Previously raw LF inside
|
|
78
|
+
a binary frame could leak and break the single-line guarantee.
|
|
79
|
+
- Test suite runs cleanly without protocol-error stderr noise.
|
|
80
|
+
|
|
3
81
|
## 0.14.0 — 2026-04-13
|
|
4
82
|
|
|
5
83
|
### 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,34 @@ 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
|
+
#
|
|
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.
|
|
313
|
+
def trace_recv(parts)
|
|
314
|
+
return unless config.verbose >= 3
|
|
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")
|
|
320
|
+
$stderr.flush
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
301
324
|
def wait_for_loops(receiver, sender)
|
|
302
325
|
if config.data || config.file || config.send_expr || config.recv_expr || config.target
|
|
303
326
|
sender.wait
|
|
@@ -316,17 +339,49 @@ module OMQ
|
|
|
316
339
|
|
|
317
340
|
|
|
318
341
|
def send_msg(parts)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
342
|
+
case config.format
|
|
343
|
+
when :marshal
|
|
344
|
+
dumped = Marshal.dump(parts)
|
|
345
|
+
trace_send(parts, uncompressed_size: dumped.bytesize)
|
|
346
|
+
@sock.send([dumped])
|
|
347
|
+
else
|
|
348
|
+
return if parts.empty?
|
|
349
|
+
trace_send(parts)
|
|
350
|
+
@sock.send(parts)
|
|
351
|
+
end
|
|
322
352
|
transient_ready!
|
|
323
353
|
end
|
|
324
354
|
|
|
325
355
|
|
|
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)
|
|
365
|
+
return unless config.verbose >= 3
|
|
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")
|
|
371
|
+
$stderr.flush
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
|
|
326
375
|
def recv_msg
|
|
327
376
|
parts = @sock.receive
|
|
328
377
|
return nil if parts.nil?
|
|
329
|
-
|
|
378
|
+
|
|
379
|
+
case config.format
|
|
380
|
+
when :marshal
|
|
381
|
+
@last_recv_uncompressed = parts.first.bytesize
|
|
382
|
+
parts = Marshal.load(parts.first)
|
|
383
|
+
end
|
|
384
|
+
|
|
330
385
|
transient_ready!
|
|
331
386
|
parts
|
|
332
387
|
end
|
|
@@ -392,10 +447,6 @@ module OMQ
|
|
|
392
447
|
|
|
393
448
|
def output(parts)
|
|
394
449
|
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
450
|
$stdout.write(@fmt.encode(parts))
|
|
400
451
|
$stdout.flush
|
|
401
452
|
end
|
|
@@ -473,33 +524,33 @@ module OMQ
|
|
|
473
524
|
# -vv log connect/disconnect/retry/timeout events
|
|
474
525
|
# -vvv also log message sent/received traces
|
|
475
526
|
# --timestamps[=s|ms|us]: prepend UTC timestamps to log lines
|
|
527
|
+
#
|
|
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
|
|
531
|
+
# ordering is strict on a shared tty. The monitor-fiber path
|
|
532
|
+
# suffered from $stderr/$stdout buffer races and from dumping
|
|
533
|
+
# wire-side bytes (pre-Marshal.load on recv, post-Marshal.dump
|
|
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.
|
|
476
539
|
def start_event_monitor
|
|
477
|
-
trace
|
|
478
|
-
log_events
|
|
540
|
+
trace = config.verbose >= 3
|
|
541
|
+
log_events = config.verbose >= 2
|
|
479
542
|
@sock.monitor(verbose: trace) do |event|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
484
550
|
end
|
|
485
|
-
kill_on_protocol_error(event)
|
|
551
|
+
SocketSetup.kill_on_protocol_error(@sock, event)
|
|
486
552
|
end
|
|
487
553
|
end
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
# omq-cli policy: a peer that commits a protocol-level violation
|
|
491
|
-
# (Protocol::ZMTP::Error — oversized frame, decompression
|
|
492
|
-
# bytebomb, bad framing, …) is almost certainly a
|
|
493
|
-
# misconfiguration the user needs to see. Mark the socket dead
|
|
494
|
-
# so the next receive raises SocketDeadError. The library
|
|
495
|
-
# itself just drops the connection and keeps serving the
|
|
496
|
-
# others; this stricter policy is CLI-only.
|
|
497
|
-
def kill_on_protocol_error(event)
|
|
498
|
-
return unless event.type == :disconnected
|
|
499
|
-
error = event.detail && event.detail[:error]
|
|
500
|
-
return unless error.is_a?(Protocol::ZMTP::Error)
|
|
501
|
-
@sock.engine.signal_fatal_error(error)
|
|
502
|
-
end
|
|
503
554
|
end
|
|
504
555
|
end
|
|
505
556
|
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,26 +46,28 @@ 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
|
-
result.map
|
|
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
|
|
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
|
-
result.map
|
|
70
|
+
result.map { |part| part.to_s }
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
|
|
@@ -77,22 +79,8 @@ module OMQ
|
|
|
77
79
|
def self.compile_inside_ractor(src)
|
|
78
80
|
return [nil, nil, nil] unless src
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return [expr, nil] unless s
|
|
83
|
-
ci = expr.index("{", s)
|
|
84
|
-
depth = 1
|
|
85
|
-
j = ci + 1
|
|
86
|
-
while j < expr.length && depth > 0
|
|
87
|
-
depth += 1 if expr[j] == "{"
|
|
88
|
-
depth -= 1 if expr[j] == "}"
|
|
89
|
-
j += 1
|
|
90
|
-
end
|
|
91
|
-
[expr[0...s] + expr[j..], expr[(ci + 1)..(j - 2)]]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
expr, begin_body = extract.(src, "BEGIN")
|
|
95
|
-
expr, end_body = extract.(expr, "END")
|
|
82
|
+
expr, begin_body = extract_block(src, "BEGIN")
|
|
83
|
+
expr, end_body = extract_block(expr, "END")
|
|
96
84
|
|
|
97
85
|
begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
98
86
|
end_proc = eval("proc { #{end_body} }") if end_body
|
|
@@ -105,17 +93,13 @@ module OMQ
|
|
|
105
93
|
end
|
|
106
94
|
|
|
107
95
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
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)
|
|
119
103
|
start = expr.index(/#{keyword}\s*\{/)
|
|
120
104
|
return [expr, nil] unless start
|
|
121
105
|
|
|
@@ -136,6 +120,16 @@ module OMQ
|
|
|
136
120
|
trimmed = expr[0...start] + expr[j..]
|
|
137
121
|
[trimmed, body]
|
|
138
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
|
|
139
133
|
end
|
|
140
134
|
end
|
|
141
135
|
end
|
data/lib/omq/cli/formatter.rb
CHANGED
|
@@ -20,19 +20,20 @@ module OMQ
|
|
|
20
20
|
def encode(parts)
|
|
21
21
|
case @format
|
|
22
22
|
when :ascii
|
|
23
|
-
parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t")
|
|
23
|
+
parts.map { |p| p.b.gsub(/[^[:print:]\t]/, ".") }.join("\t") << "\n"
|
|
24
24
|
when :quoted
|
|
25
|
-
parts.map { |p| p.b.dump[1..-2] }.join("\t")
|
|
25
|
+
parts.map { |p| p.b.dump[1..-2] }.join("\t") << "\n"
|
|
26
26
|
when :raw
|
|
27
27
|
parts.each_with_index.map do |p, i|
|
|
28
28
|
Protocol::ZMTP::Codec::Frame.new(p.to_s, more: i < parts.size - 1).to_wire
|
|
29
29
|
end.join
|
|
30
30
|
when :jsonl
|
|
31
|
-
JSON.generate(parts)
|
|
31
|
+
JSON.generate(parts) << "\n"
|
|
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,102 @@ 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.
|
|
106
|
+
# For marshal, +uncompressed_size+ is the Marshal.dump bytesize
|
|
107
|
+
# (known to the caller, which already serialized for send or
|
|
108
|
+
# received the wire frame for recv) — passed through instead of
|
|
109
|
+
# redumping here.
|
|
86
110
|
#
|
|
87
|
-
# @param parts [Array<String
|
|
111
|
+
# @param parts [Array<String, Object>, Object] message frames, or raw object when +format+ is :marshal
|
|
112
|
+
# @param format [Symbol, nil] active CLI format (:marshal enables object-inspect mode)
|
|
88
113
|
# @param wire_size [Integer, nil] compressed bytes on the wire
|
|
114
|
+
# @param uncompressed_size [Integer, nil] plaintext bytes (marshal only)
|
|
89
115
|
# @return [String] truncated preview of each frame joined by |
|
|
90
|
-
def self.preview(parts, wire_size: nil)
|
|
91
|
-
|
|
116
|
+
def self.preview(parts, format: nil, wire_size: nil, uncompressed_size: nil)
|
|
117
|
+
case format
|
|
118
|
+
when :marshal
|
|
119
|
+
marshal_preview(parts, uncompressed_size: uncompressed_size, wire_size: wire_size)
|
|
120
|
+
else
|
|
121
|
+
frames_preview(parts, format: format, wire_size: wire_size)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def self.marshal_preview(parts, uncompressed_size:, wire_size:)
|
|
127
|
+
inspected = parts.inspect
|
|
128
|
+
truncated = inspected.bytesize > 60
|
|
129
|
+
inspected = inspected.byteslice(0, 60) if truncated
|
|
130
|
+
body = sanitize(inspected)
|
|
131
|
+
|
|
132
|
+
body << "…" if truncated
|
|
133
|
+
|
|
134
|
+
header = case
|
|
135
|
+
when uncompressed_size && wire_size
|
|
136
|
+
"(#{uncompressed_size}B wire=#{wire_size}B marshal)"
|
|
137
|
+
when uncompressed_size
|
|
138
|
+
"(#{uncompressed_size}B marshal)"
|
|
139
|
+
else
|
|
140
|
+
"(marshal)"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
"#{header} #{body}"
|
|
144
|
+
end
|
|
145
|
+
private_class_method :marshal_preview
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def self.frames_preview(parts, format:, wire_size:)
|
|
92
149
|
nparts = parts.size
|
|
93
150
|
shown = parts.first(3).map { |p| preview_frame(p) }
|
|
94
151
|
tail = nparts > 3 ? "|…" : ""
|
|
95
|
-
|
|
152
|
+
total = parts.all?(String) ? parts.sum { |p| p.bytesize } : nil
|
|
153
|
+
size = if wire_size && total
|
|
154
|
+
"#{total}B wire=#{wire_size}B"
|
|
155
|
+
elsif total
|
|
156
|
+
"#{total}B"
|
|
157
|
+
else
|
|
158
|
+
"#{nparts}obj"
|
|
159
|
+
end
|
|
96
160
|
header = nparts > 1 ? "(#{size} #{nparts}F)" : "(#{size})"
|
|
97
161
|
|
|
98
162
|
"#{header} #{shown.join("|")}#{tail}"
|
|
99
163
|
end
|
|
164
|
+
private_class_method :frames_preview
|
|
100
165
|
|
|
101
166
|
|
|
167
|
+
# Renders one frame or decoded object for {Formatter.preview}.
|
|
168
|
+
# Strings are sanitized byte-wise (first 12 bytes); non-String
|
|
169
|
+
# objects fall back to #inspect (always single-line) truncated
|
|
170
|
+
# at 24 bytes.
|
|
171
|
+
#
|
|
172
|
+
# @param part [String, Object]
|
|
173
|
+
# @return [String]
|
|
102
174
|
def self.preview_frame(part)
|
|
175
|
+
unless part.is_a?(String)
|
|
176
|
+
s = part.inspect
|
|
177
|
+
return s.bytesize > 24 ? "#{s.byteslice(0, 24)}…" : s
|
|
178
|
+
end
|
|
179
|
+
|
|
103
180
|
bytes = part.b
|
|
104
181
|
# Empty frames must render as a visible marker, not as the empty
|
|
105
182
|
# string — otherwise joining with "|" would produce misleading
|
|
@@ -113,11 +190,24 @@ module OMQ
|
|
|
113
190
|
if printable < sample.bytesize / 2
|
|
114
191
|
"[#{bytes.bytesize}B]"
|
|
115
192
|
elsif bytes.bytesize > 12
|
|
116
|
-
"#{sample
|
|
193
|
+
"#{sanitize(sample)}…"
|
|
117
194
|
else
|
|
118
|
-
sample
|
|
195
|
+
sanitize(sample)
|
|
119
196
|
end
|
|
120
197
|
end
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Escapes bytes so a preview/body line is guaranteed single-line
|
|
201
|
+
# on a shared tty. Tab/newline/CR/backslash render as literal
|
|
202
|
+
# \t/\n/\r/\\; other non-printables collapse to '.'. Forced to
|
|
203
|
+
# binary encoding first to prevent UTF-8 quirks from rendering
|
|
204
|
+
# raw LF bytes.
|
|
205
|
+
#
|
|
206
|
+
# @param bytes [String]
|
|
207
|
+
# @return [String]
|
|
208
|
+
def self.sanitize(bytes)
|
|
209
|
+
bytes.b.gsub(/[\t\n\r\\]/, LINE_ESCAPES).tr("^ -~", ".")
|
|
210
|
+
end
|
|
121
211
|
end
|
|
122
212
|
end
|
|
123
213
|
end
|
|
@@ -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
|
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/socket_setup.rb
CHANGED
|
@@ -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/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.14.2
|
|
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.
|
|
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.
|
|
31
|
+
version: 0.19.3
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: omq-ffi
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|