omq 0.8.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +9 -49
- data/lib/omq/version.rb +1 -1
- metadata +7 -26
- data/exe/omq +0 -6
- data/lib/omq/cli/base_runner.rb +0 -459
- data/lib/omq/cli/channel.rb +0 -8
- data/lib/omq/cli/client_server.rb +0 -111
- data/lib/omq/cli/config.rb +0 -54
- data/lib/omq/cli/formatter.rb +0 -75
- data/lib/omq/cli/pair.rb +0 -31
- data/lib/omq/cli/peer.rb +0 -8
- data/lib/omq/cli/pipe.rb +0 -265
- data/lib/omq/cli/pub_sub.rb +0 -14
- data/lib/omq/cli/push_pull.rb +0 -14
- data/lib/omq/cli/radio_dish.rb +0 -27
- data/lib/omq/cli/req_rep.rb +0 -83
- data/lib/omq/cli/router_dealer.rb +0 -76
- data/lib/omq/cli/scatter_gather.rb +0 -14
- data/lib/omq/cli.rb +0 -540
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a14af693117ccfb193f54e9bc2a4faae9c2bf6396e7769abee97f279fd62fdba
|
|
4
|
+
data.tar.gz: 1f5e99ae3b61402d2b9e506c84fdb07d2003695515d3f736159cf2c373cc39d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76539e2a9fbdcbe59590e1fa499e300b8f7ad3d527255af6f94665139c3c0a60151f0b82202df372df1cdbc16e90f46369aa7b159342f951cf52babc897b837c
|
|
7
|
+
data.tar.gz: dba241d3283a0a0330ba0d12081c08f3ae074acaa8be7b1e9292dee9c19a483d822f896a4442ec585005dbeb36efa704b87f51c53e07c1de0ede54a85150f55c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-03-31
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **CLI extracted into omq-cli gem** — the `omq` executable, all CLI
|
|
8
|
+
code (`lib/omq/cli/`), tests, and `CLI.md` have moved to the
|
|
9
|
+
[omq-cli](https://github.com/paddor/omq-cli) gem. `gem install omq`
|
|
10
|
+
no longer provides the `omq` command — use `gem install omq-cli`.
|
|
11
|
+
- **`OMQ.outgoing` / `OMQ.incoming`** registration API moved to omq-cli.
|
|
12
|
+
Library-only users are unaffected (these were CLI-specific).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Gemspec is library-only** — no `exe/`, no `bindir`, no `executables`.
|
|
17
|
+
- **README** — restored title, replaced inline CLI section with a
|
|
18
|
+
pointer to omq-cli, fixed ZMTP attribution for protocol-zmtp.
|
|
19
|
+
- **DESIGN.md** — acknowledged protocol-zmtp, clarified transient
|
|
20
|
+
task / linger interaction, removed ZMTP wire protocol section (now in
|
|
21
|
+
protocol-zmtp), simplified inproc description, removed CLI section.
|
|
22
|
+
|
|
3
23
|
## 0.8.0 — 2026-03-31
|
|
4
24
|
|
|
5
25
|
### Breaking
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OMQ —
|
|
1
|
+
# OMQ — Where did the C dependency go!?
|
|
2
2
|
|
|
3
3
|
[](https://github.com/zeromq/omq/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/omq)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
`gem install omq` — that's it. No libzmq, no compiler, no system packages. Just Ruby.
|
|
9
9
|
|
|
10
|
-
OMQ
|
|
10
|
+
OMQ builds ZeroMQ socket patterns on top of [protocol-zmtp](https://github.com/paddor/protocol-zmtp) (a pure Ruby [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) codec) using [Async](https://github.com/socketry/async) fibers. It speaks native ZeroMQ on the wire and interoperates with libzmq, pyzmq, CZMQ, and everything else in the ZMQ ecosystem.
|
|
11
11
|
|
|
12
12
|
> **234k msg/s** inproc | **49k msg/s** ipc | **36k msg/s** tcp
|
|
13
13
|
>
|
|
@@ -31,12 +31,12 @@ See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all ma
|
|
|
31
31
|
|
|
32
32
|
- **Zero dependencies on C** — no extensions, no FFI, no libzmq. `gem install` just works everywhere
|
|
33
33
|
- **Fast** — YJIT-optimized hot paths, batched sends, 234k msg/s inproc with 12 µs latency
|
|
34
|
-
-
|
|
34
|
+
- **[`omq` CLI](https://github.com/paddor/omq-cli)** — `gem install omq-cli` for a command-line tool with Ruby eval, Ractor parallelism, and script handlers
|
|
35
35
|
- **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
|
|
36
36
|
- **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
|
|
37
37
|
- **Async-native** — built on fibers, non-blocking from the ground up. A shared IO thread handles sockets outside of Async — no reactor needed for simple scripts
|
|
38
38
|
- **Wire-compatible** — interoperates with libzmq, pyzmq, CZMQ over tcp and ipc
|
|
39
|
-
- **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects and
|
|
39
|
+
- **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects automatically and queued messages drain when peers arrive
|
|
40
40
|
|
|
41
41
|
For architecture internals, see [DESIGN.md](DESIGN.md).
|
|
42
42
|
|
|
@@ -150,58 +150,18 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes li
|
|
|
150
150
|
| **PEER** | Routing-ID | Fair-queue | Block |
|
|
151
151
|
| **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
|
|
152
152
|
|
|
153
|
-
##
|
|
153
|
+
## CLI
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
Install [omq-cli](https://github.com/paddor/omq-cli) for a command-line tool that sends, receives, pipes, and transforms ZeroMQ messages from the terminal:
|
|
156
156
|
|
|
157
157
|
```sh
|
|
158
|
-
|
|
159
|
-
omq rep -b tcp://:5555 --echo
|
|
160
|
-
|
|
161
|
-
# Upcase server — -e evals Ruby on each incoming message
|
|
162
|
-
omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
|
|
158
|
+
gem install omq-cli
|
|
163
159
|
|
|
164
|
-
|
|
160
|
+
omq rep -b tcp://:5555 --echo
|
|
165
161
|
echo "hello" | omq req -c tcp://localhost:5555
|
|
166
|
-
# => HELLO
|
|
167
|
-
|
|
168
|
-
# PUB/SUB
|
|
169
|
-
omq sub -b tcp://:5556 -s "weather." &
|
|
170
|
-
echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
|
|
171
|
-
|
|
172
|
-
# Pipeline with filtering
|
|
173
|
-
tail -f /var/log/syslog | omq push -c tcp://collector:5557
|
|
174
|
-
omq pull -b tcp://:5557 -e 'next unless /error/; $F'
|
|
175
|
-
|
|
176
|
-
# Transform outgoing messages with -E
|
|
177
|
-
echo hello | omq push -c tcp://localhost:5557 -E '$F.map(&:upcase)'
|
|
178
|
-
|
|
179
|
-
# REQ: transform request and reply independently
|
|
180
|
-
echo hello | omq req -c tcp://localhost:5555 \
|
|
181
|
-
-E '$F.map(&:upcase)' -e '$F.map(&:reverse)'
|
|
182
|
-
|
|
183
|
-
# Pipe: PULL → eval → PUSH in one process
|
|
184
|
-
omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
|
|
185
|
-
|
|
186
|
-
# Pipe with Ractor workers for CPU parallelism (-P = all CPUs)
|
|
187
|
-
omq pipe -c ipc://@work -c ipc://@sink -P -r./fib -e 'fib(Integer($_)).to_s'
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
`-e` (recv-eval) transforms incoming messages, `-E` (send-eval) transforms outgoing messages. `$F` is the message parts array, `$_` is the first part. Use `-r` to require gems or load scripts that register handlers via `OMQ.incoming` / `OMQ.outgoing`:
|
|
191
|
-
|
|
192
|
-
```ruby
|
|
193
|
-
# my_handler.rb
|
|
194
|
-
db = DB.connect("postgres://localhost/app")
|
|
195
|
-
|
|
196
|
-
OMQ.incoming { db.query($F.first) }
|
|
197
|
-
at_exit { db.close }
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
```sh
|
|
201
|
-
omq pull -b tcp://:5557 -r./my_handler.rb
|
|
202
162
|
```
|
|
203
163
|
|
|
204
|
-
See [
|
|
164
|
+
See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentation.
|
|
205
165
|
|
|
206
166
|
## Development
|
|
207
167
|
|
data/lib/omq/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -51,40 +51,21 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.11'
|
|
54
|
-
description: Pure Ruby implementation of
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
— pipe, filter, and transform across processes with Ruby eval, multiple formats
|
|
58
|
-
(JSON, msgpack, marshal), Ractor parallelism, and compression. No native libraries
|
|
54
|
+
description: Pure Ruby implementation of ZeroMQ with all socket types (REQ/REP, PUB/SUB,
|
|
55
|
+
PUSH/PULL, DEALER/ROUTER, and draft types) and TCP/IPC/inproc transports. Built
|
|
56
|
+
on protocol-zmtp (ZMTP 3.1 wire protocol) and Async fibers. No native libraries
|
|
59
57
|
required.
|
|
60
58
|
email:
|
|
61
59
|
- paddor@gmail.com
|
|
62
|
-
executables:
|
|
63
|
-
- omq
|
|
60
|
+
executables: []
|
|
64
61
|
extensions: []
|
|
65
62
|
extra_rdoc_files: []
|
|
66
63
|
files:
|
|
67
64
|
- CHANGELOG.md
|
|
68
65
|
- LICENSE
|
|
69
66
|
- README.md
|
|
70
|
-
- exe/omq
|
|
71
67
|
- lib/omq.rb
|
|
72
68
|
- lib/omq/channel.rb
|
|
73
|
-
- lib/omq/cli.rb
|
|
74
|
-
- lib/omq/cli/base_runner.rb
|
|
75
|
-
- lib/omq/cli/channel.rb
|
|
76
|
-
- lib/omq/cli/client_server.rb
|
|
77
|
-
- lib/omq/cli/config.rb
|
|
78
|
-
- lib/omq/cli/formatter.rb
|
|
79
|
-
- lib/omq/cli/pair.rb
|
|
80
|
-
- lib/omq/cli/peer.rb
|
|
81
|
-
- lib/omq/cli/pipe.rb
|
|
82
|
-
- lib/omq/cli/pub_sub.rb
|
|
83
|
-
- lib/omq/cli/push_pull.rb
|
|
84
|
-
- lib/omq/cli/radio_dish.rb
|
|
85
|
-
- lib/omq/cli/req_rep.rb
|
|
86
|
-
- lib/omq/cli/router_dealer.rb
|
|
87
|
-
- lib/omq/cli/scatter_gather.rb
|
|
88
69
|
- lib/omq/client_server.rb
|
|
89
70
|
- lib/omq/pair.rb
|
|
90
71
|
- lib/omq/peer.rb
|
|
@@ -148,5 +129,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
148
129
|
requirements: []
|
|
149
130
|
rubygems_version: 4.0.6
|
|
150
131
|
specification_version: 4
|
|
151
|
-
summary: Pure Ruby ZMQ library
|
|
132
|
+
summary: Pure Ruby ZMQ library
|
|
152
133
|
test_files: []
|
data/exe/omq
DELETED
data/lib/omq/cli/base_runner.rb
DELETED
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module CLI
|
|
5
|
-
class BaseRunner
|
|
6
|
-
attr_reader :config, :sock
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def initialize(config, socket_class)
|
|
10
|
-
@config = config
|
|
11
|
-
@klass = socket_class
|
|
12
|
-
@fmt = Formatter.new(config.format, compress: config.compress)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def call(task)
|
|
17
|
-
@sock = create_socket
|
|
18
|
-
attach_endpoints
|
|
19
|
-
setup_curve
|
|
20
|
-
setup_subscriptions
|
|
21
|
-
compile_expr
|
|
22
|
-
|
|
23
|
-
if config.transient
|
|
24
|
-
start_disconnect_monitor(task)
|
|
25
|
-
Async::Task.current.yield # let monitor start waiting
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
sleep(config.delay) if config.delay && config.recv_only?
|
|
29
|
-
wait_for_peer if needs_peer_wait?
|
|
30
|
-
|
|
31
|
-
@sock.instance_exec(&@send_begin_proc) if @send_begin_proc
|
|
32
|
-
@sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
|
|
33
|
-
run_loop(task)
|
|
34
|
-
@sock.instance_exec(&@send_end_proc) if @send_end_proc
|
|
35
|
-
@sock.instance_exec(&@recv_end_proc) if @recv_end_proc
|
|
36
|
-
ensure
|
|
37
|
-
@sock&.close
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Subclasses override this.
|
|
45
|
-
def run_loop(task)
|
|
46
|
-
raise NotImplementedError
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# ── Socket creation ─────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def create_socket
|
|
53
|
-
sock_opts = { linger: config.linger }
|
|
54
|
-
sock_opts[:conflate] = true if config.conflate && %w[pub radio].include?(config.type_name)
|
|
55
|
-
sock = @klass.new(**sock_opts)
|
|
56
|
-
sock.recv_timeout = config.timeout if config.timeout
|
|
57
|
-
sock.send_timeout = config.timeout if config.timeout
|
|
58
|
-
sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
59
|
-
sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
|
|
60
|
-
sock.identity = config.identity if config.identity
|
|
61
|
-
sock.router_mandatory = true if config.type_name == "router"
|
|
62
|
-
sock
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def attach_endpoints
|
|
67
|
-
config.binds.each do |url|
|
|
68
|
-
@sock.bind(url)
|
|
69
|
-
log "Bound to #{@sock.last_endpoint}"
|
|
70
|
-
end
|
|
71
|
-
config.connects.each do |url|
|
|
72
|
-
@sock.connect(url)
|
|
73
|
-
log "Connecting to #{url}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# ── Peer wait with grace period ─────────────────────────────────
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def needs_peer_wait?
|
|
81
|
-
!config.recv_only? && (config.connects.any? || config.type_name == "router")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def wait_for_peer
|
|
86
|
-
with_timeout(config.timeout) do
|
|
87
|
-
@sock.peer_connected.wait
|
|
88
|
-
log "Peer connected"
|
|
89
|
-
if %w[pub xpub].include?(config.type_name)
|
|
90
|
-
@sock.subscriber_joined.wait
|
|
91
|
-
log "Subscriber joined"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Grace period: when multiple peers may be connecting (bind or
|
|
95
|
-
# multiple connect URLs), wait one reconnect interval so
|
|
96
|
-
# latecomers finish their handshake before we start sending.
|
|
97
|
-
if config.binds.any? || config.connects.size > 1
|
|
98
|
-
ri = @sock.options.reconnect_interval
|
|
99
|
-
sleep(ri.is_a?(Range) ? ri.begin : ri)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# ── Transient disconnect monitor ────────────────────────────────
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def start_disconnect_monitor(task)
|
|
108
|
-
@transient_barrier = Async::Promise.new
|
|
109
|
-
task.async do
|
|
110
|
-
@transient_barrier.wait
|
|
111
|
-
@sock.all_peers_gone.wait unless @sock.connection_count == 0
|
|
112
|
-
log "All peers disconnected, exiting"
|
|
113
|
-
@sock.reconnect_enabled = false
|
|
114
|
-
if config.send_only?
|
|
115
|
-
task.stop
|
|
116
|
-
else
|
|
117
|
-
@sock.close_read
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def transient_ready!
|
|
124
|
-
if config.transient && !@transient_barrier.resolved?
|
|
125
|
-
@transient_barrier.resolve(true)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# ── Timeout helper ──────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def with_timeout(seconds)
|
|
133
|
-
if seconds
|
|
134
|
-
Async::Task.current.with_timeout(seconds) { yield }
|
|
135
|
-
else
|
|
136
|
-
yield
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# ── Socket setup ────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def setup_subscriptions
|
|
144
|
-
case config.type_name
|
|
145
|
-
when "sub"
|
|
146
|
-
prefixes = config.subscribes.empty? ? [""] : config.subscribes
|
|
147
|
-
prefixes.each { |p| @sock.subscribe(p) }
|
|
148
|
-
when "dish"
|
|
149
|
-
config.joins.each { |g| @sock.join(g) }
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def setup_curve
|
|
155
|
-
server_key_z85 = config.curve_server_key || ENV["OMQ_SERVER_KEY"]
|
|
156
|
-
server_mode = config.curve_server || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
|
|
157
|
-
|
|
158
|
-
if server_key_z85
|
|
159
|
-
if ENV["OMQ_DEV"]
|
|
160
|
-
require_relative "../../../../omq-curve/lib/omq/curve"
|
|
161
|
-
else
|
|
162
|
-
require "omq/curve"
|
|
163
|
-
end
|
|
164
|
-
server_key = OMQ::Z85.decode(server_key_z85)
|
|
165
|
-
client_key = RbNaCl::PrivateKey.generate
|
|
166
|
-
@sock.mechanism = OMQ::Curve.client(
|
|
167
|
-
client_key.public_key.to_s, client_key.to_s, server_key: server_key
|
|
168
|
-
)
|
|
169
|
-
elsif server_mode
|
|
170
|
-
if ENV["OMQ_DEV"]
|
|
171
|
-
require_relative "../../../../omq-curve/lib/omq/curve"
|
|
172
|
-
else
|
|
173
|
-
require "omq/curve"
|
|
174
|
-
end
|
|
175
|
-
if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
|
|
176
|
-
server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
|
|
177
|
-
server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
|
|
178
|
-
else
|
|
179
|
-
key = RbNaCl::PrivateKey.generate
|
|
180
|
-
server_pub = key.public_key.to_s
|
|
181
|
-
server_sec = key.to_s
|
|
182
|
-
end
|
|
183
|
-
@sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
|
|
184
|
-
$stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
|
|
185
|
-
end
|
|
186
|
-
rescue LoadError
|
|
187
|
-
abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# ── Shared loop bodies ──────────────────────────────────────────
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def run_send_logic
|
|
194
|
-
n = config.count
|
|
195
|
-
i = 0
|
|
196
|
-
sleep(config.delay) if config.delay
|
|
197
|
-
if config.interval
|
|
198
|
-
i += send_tick
|
|
199
|
-
unless @send_tick_eof || (n && n > 0 && i >= n)
|
|
200
|
-
Async::Loop.quantized(interval: config.interval) do
|
|
201
|
-
i += send_tick
|
|
202
|
-
break if @send_tick_eof || (n && n > 0 && i >= n)
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
elsif config.data || config.file
|
|
206
|
-
parts = eval_send_expr(read_next)
|
|
207
|
-
send_msg(parts) if parts
|
|
208
|
-
elsif stdin_ready?
|
|
209
|
-
loop do
|
|
210
|
-
parts = read_next
|
|
211
|
-
break unless parts
|
|
212
|
-
parts = eval_send_expr(parts)
|
|
213
|
-
send_msg(parts) if parts
|
|
214
|
-
i += 1
|
|
215
|
-
break if n && n > 0 && i >= n
|
|
216
|
-
end
|
|
217
|
-
elsif @send_eval_proc
|
|
218
|
-
parts = eval_send_expr(nil)
|
|
219
|
-
send_msg(parts) if parts
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def send_tick
|
|
225
|
-
raw = read_next_or_nil
|
|
226
|
-
if raw.nil? && !@send_eval_proc
|
|
227
|
-
@send_tick_eof = true
|
|
228
|
-
return 0
|
|
229
|
-
end
|
|
230
|
-
parts = eval_send_expr(raw)
|
|
231
|
-
send_msg(parts) if parts
|
|
232
|
-
1
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def run_recv_logic
|
|
237
|
-
n = config.count
|
|
238
|
-
i = 0
|
|
239
|
-
loop do
|
|
240
|
-
parts = recv_msg
|
|
241
|
-
break if parts.nil?
|
|
242
|
-
parts = eval_recv_expr(parts)
|
|
243
|
-
output(parts)
|
|
244
|
-
i += 1
|
|
245
|
-
break if n && n > 0 && i >= n
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def wait_for_loops(receiver, sender)
|
|
251
|
-
if config.data || config.file || config.send_expr || config.recv_expr || config.target
|
|
252
|
-
sender.wait
|
|
253
|
-
receiver.stop
|
|
254
|
-
elsif config.count && config.count > 0
|
|
255
|
-
receiver.wait
|
|
256
|
-
sender.stop
|
|
257
|
-
else
|
|
258
|
-
sender.wait
|
|
259
|
-
receiver.stop
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# ── Message I/O ─────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def send_msg(parts)
|
|
267
|
-
return if parts.empty?
|
|
268
|
-
parts = [Marshal.dump(parts)] if config.format == :marshal
|
|
269
|
-
parts = @fmt.compress(parts)
|
|
270
|
-
@sock.send(parts)
|
|
271
|
-
transient_ready!
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def recv_msg
|
|
276
|
-
raw = @sock.receive
|
|
277
|
-
return nil if raw.nil?
|
|
278
|
-
parts = @fmt.decompress(raw)
|
|
279
|
-
parts = Marshal.load(parts.first) if config.format == :marshal
|
|
280
|
-
transient_ready!
|
|
281
|
-
parts
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def recv_msg_raw
|
|
286
|
-
msg = @sock.receive
|
|
287
|
-
msg&.dup
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def read_next
|
|
292
|
-
if config.data
|
|
293
|
-
@fmt.decode(config.data + "\n")
|
|
294
|
-
elsif config.file
|
|
295
|
-
@file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
|
|
296
|
-
@fmt.decode(@file_data + "\n")
|
|
297
|
-
elsif config.format == :msgpack
|
|
298
|
-
@fmt.decode_msgpack($stdin)
|
|
299
|
-
elsif config.format == :marshal
|
|
300
|
-
@fmt.decode_marshal($stdin)
|
|
301
|
-
elsif config.format == :raw
|
|
302
|
-
data = $stdin.read
|
|
303
|
-
return nil if data.nil? || data.empty?
|
|
304
|
-
[data]
|
|
305
|
-
else
|
|
306
|
-
line = $stdin.gets
|
|
307
|
-
return nil if line.nil?
|
|
308
|
-
@fmt.decode(line)
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def stdin_ready?
|
|
314
|
-
return @stdin_ready unless @stdin_ready.nil?
|
|
315
|
-
|
|
316
|
-
@stdin_ready = !$stdin.closed? &&
|
|
317
|
-
!config.stdin_is_tty &&
|
|
318
|
-
IO.select([$stdin], nil, nil, 0.01) &&
|
|
319
|
-
!$stdin.eof?
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def read_next_or_nil
|
|
324
|
-
if config.data || config.file
|
|
325
|
-
read_next
|
|
326
|
-
elsif @send_eval_proc
|
|
327
|
-
nil
|
|
328
|
-
else
|
|
329
|
-
read_next
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def output(parts)
|
|
335
|
-
return if config.quiet || parts.nil?
|
|
336
|
-
$stdout.write(@fmt.encode(parts))
|
|
337
|
-
$stdout.flush
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# ── Routing helpers ─────────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def display_routing_id(id)
|
|
344
|
-
if id.bytes.all? { |b| b >= 0x20 && b <= 0x7E }
|
|
345
|
-
id
|
|
346
|
-
else
|
|
347
|
-
"0x#{id.unpack1("H*")}"
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def resolve_target(target)
|
|
353
|
-
if target.start_with?("0x")
|
|
354
|
-
[target[2..].delete(" ")].pack("H*")
|
|
355
|
-
else
|
|
356
|
-
target
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
# ── Eval ────────────────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def compile_expr
|
|
364
|
-
compile_one_expr(:send, config.send_expr)
|
|
365
|
-
compile_one_expr(:recv, config.recv_expr)
|
|
366
|
-
@send_eval_proc ||= wrap_registered_proc(OMQ.outgoing_proc)
|
|
367
|
-
@recv_eval_proc ||= wrap_registered_proc(OMQ.incoming_proc)
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def wrap_registered_proc(block)
|
|
372
|
-
return unless block
|
|
373
|
-
proc do |msg|
|
|
374
|
-
$_ = msg&.first
|
|
375
|
-
block.call(msg)
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def compile_one_expr(direction, src)
|
|
381
|
-
return unless src
|
|
382
|
-
expr, begin_body, end_body = extract_blocks(src)
|
|
383
|
-
instance_variable_set(:"@#{direction}_begin_proc", eval("proc { #{begin_body} }")) if begin_body
|
|
384
|
-
instance_variable_set(:"@#{direction}_end_proc", eval("proc { #{end_body} }")) if end_body
|
|
385
|
-
if expr && !expr.strip.empty?
|
|
386
|
-
instance_variable_set(:"@#{direction}_eval_proc", eval("proc { $_ = $F&.first; #{expr} }"))
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def extract_blocks(expr)
|
|
392
|
-
begin_body = end_body = nil
|
|
393
|
-
expr, begin_body = extract_block(expr, "BEGIN")
|
|
394
|
-
expr, end_body = extract_block(expr, "END")
|
|
395
|
-
[expr, begin_body, end_body]
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
def extract_block(expr, keyword)
|
|
400
|
-
start = expr.index(/#{keyword}\s*\{/)
|
|
401
|
-
return [expr, nil] unless start
|
|
402
|
-
|
|
403
|
-
# Find the opening brace
|
|
404
|
-
i = expr.index("{", start)
|
|
405
|
-
depth = 1
|
|
406
|
-
j = i + 1
|
|
407
|
-
while j < expr.length && depth > 0
|
|
408
|
-
case expr[j]
|
|
409
|
-
when "{" then depth += 1
|
|
410
|
-
when "}" then depth -= 1
|
|
411
|
-
end
|
|
412
|
-
j += 1
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
body = expr[(i + 1)..(j - 2)]
|
|
416
|
-
trimmed = expr[0...start] + expr[j..]
|
|
417
|
-
[trimmed, body]
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
SENT = Object.new.freeze # sentinel: eval already sent the reply
|
|
422
|
-
|
|
423
|
-
def eval_send_expr(parts)
|
|
424
|
-
return parts unless @send_eval_proc
|
|
425
|
-
run_eval(@send_eval_proc, parts)
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def eval_recv_expr(parts)
|
|
430
|
-
return parts unless @recv_eval_proc
|
|
431
|
-
run_eval(@recv_eval_proc, parts)
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def run_eval(eval_proc, parts)
|
|
436
|
-
$F = parts
|
|
437
|
-
result = @sock.instance_exec(parts, &eval_proc)
|
|
438
|
-
return nil if result.nil?
|
|
439
|
-
return SENT if result.equal?(@sock)
|
|
440
|
-
return [result] if config.format == :marshal
|
|
441
|
-
case result
|
|
442
|
-
when Array then result
|
|
443
|
-
when String then [result]
|
|
444
|
-
else [result.to_str]
|
|
445
|
-
end
|
|
446
|
-
rescue => e
|
|
447
|
-
$stderr.puts "omq: eval error: #{e.message} (#{e.class})"
|
|
448
|
-
exit 3
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
# ── Logging ─────────────────────────────────────────────────────
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def log(msg)
|
|
455
|
-
$stderr.puts(msg) if config.verbose
|
|
456
|
-
end
|
|
457
|
-
end
|
|
458
|
-
end
|
|
459
|
-
end
|