Add example of ZeroMQ
This commit is contained in:
parent
e02ab48e51
commit
3528a28127
4 changed files with 254 additions and 0 deletions
47
bin/pkg
47
bin/pkg
|
|
@ -1,3 +1,50 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
# Simple Ruby package manager mock
|
||||
# Supports:
|
||||
# pkg --user install nvim
|
||||
# pkg --system install nvim
|
||||
# pkg install nvim # defaults to --package
|
||||
# pkg --package install nvim
|
||||
|
||||
require "package"
|
||||
|
||||
puts "Welcome to package manager"
|
||||
|
||||
# Extract options and commands from arguments
|
||||
args = ARGV.dup
|
||||
|
||||
scope = :package # default
|
||||
if args.first&.start_with?("--")
|
||||
case args.shift
|
||||
when "--user"
|
||||
scope = :user
|
||||
when "--system"
|
||||
scope = :system
|
||||
when "--package"
|
||||
scope = :package
|
||||
else
|
||||
puts "Unknown option: #{args.first}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
command = args.shift
|
||||
package = args.shift
|
||||
|
||||
if command.nil?
|
||||
puts "Usage: pkg [--user|--system|--package] <install|make> <package>"
|
||||
exit 1
|
||||
end
|
||||
|
||||
case command
|
||||
when "install"
|
||||
Package.install(package, scope)
|
||||
when "make"
|
||||
Package.make(package, scope)
|
||||
when "daemon"
|
||||
Package.daemon
|
||||
else
|
||||
puts "Unknown command: #{command}"
|
||||
exit 1
|
||||
end
|
||||
35
lib/package.rb
Normal file
35
lib/package.rb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
|
||||
module Package
|
||||
require_relative "package/daemon"
|
||||
require_relative "package/client"
|
||||
|
||||
ENDPOINT = "ipc:///tmp/zmq-rpc-demo"
|
||||
|
||||
def self.make(name, scope)
|
||||
puts "making package named: #{name} at #{scope}"
|
||||
|
||||
c = RpcClient.new(ENDPOINT, timeout_ms: 2000)
|
||||
begin
|
||||
puts "echo => #{c.call("echo", {"hello"=>"world"})}"
|
||||
puts "add => #{c.call("add", [1, 2, 3.5])}"
|
||||
puts "time => #{c.call("time")}"
|
||||
rescue ::Timeout::Error
|
||||
warn "RPC timeout: is the daemon running and bound to #{ENDPOINT}?"
|
||||
warn "Start it with: ruby daemon.rb (or your systemd service)"
|
||||
rescue => e
|
||||
warn "RPC error: #{e.class}: #{e.message}"
|
||||
ensure
|
||||
c.close
|
||||
end
|
||||
end
|
||||
|
||||
def self.install(name, scope)
|
||||
puts "making package named: #{name} at #{scope}"
|
||||
end
|
||||
|
||||
# gem install ffi-rzmq
|
||||
def self.daemon
|
||||
start_daemon(ENDPOINT)
|
||||
end
|
||||
end
|
||||
51
lib/package/client.rb
Normal file
51
lib/package/client.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# rpc_client.rb
|
||||
require 'ffi-rzmq'
|
||||
require 'json'
|
||||
require 'securerandom'
|
||||
require 'timeout'
|
||||
|
||||
ENDPOINT = "ipc:///tmp/zmq-rpc-demo"
|
||||
|
||||
class RpcClient
|
||||
def initialize(endpoint, timeout_ms: 2000)
|
||||
@ctx = ZMQ::Context.new
|
||||
@req = @ctx.socket(ZMQ::REQ)
|
||||
|
||||
@req.setsockopt(ZMQ::LINGER, 0)
|
||||
|
||||
rc = @req.connect(endpoint)
|
||||
raise "connect failed: #{ZMQ::Util.error_string}" unless rc == 0
|
||||
@poller = ZMQ::Poller.new
|
||||
@poller.register_readable(@req)
|
||||
@timeout = timeout_ms
|
||||
end
|
||||
|
||||
def call(method, params=nil)
|
||||
id = SecureRandom.uuid
|
||||
payload = { jsonrpc: "2.0", id: id, method: method, params: params }.to_json
|
||||
@req.send_string(payload)
|
||||
|
||||
# Wait for reply with timeout
|
||||
rc = @poller.poll(@timeout)
|
||||
raise Timeout::Error, "RPC timeout after #{@timeout}ms" if rc == 0
|
||||
|
||||
raw = ''
|
||||
@req.recv_string(raw)
|
||||
res = JSON.parse(raw)
|
||||
|
||||
if res["error"]
|
||||
code = res["error"]["code"]
|
||||
msg = res["error"]["message"]
|
||||
data = res["error"]["data"]
|
||||
raise StandardError, "RPC error (#{code}): #{msg}#{data ? " | #{data}" : ""}"
|
||||
end
|
||||
|
||||
raise "Mismatched id" if res["id"] != id
|
||||
res["result"]
|
||||
end
|
||||
|
||||
def close
|
||||
@req.close
|
||||
@ctx.terminate
|
||||
end
|
||||
end
|
||||
121
lib/package/daemon.rb
Normal file
121
lib/package/daemon.rb
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
require 'ffi-rzmq'
|
||||
require 'json'
|
||||
require 'time' # for Time#iso8601
|
||||
|
||||
# $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) # <<< add this line
|
||||
# require "package"
|
||||
|
||||
def start_daemon(endpoint)
|
||||
# Clean up stale ipc file (bind will fail otherwise)
|
||||
begin
|
||||
path = ENDPOINT.sub("ipc://", "")
|
||||
File.unlink(path) if File.exist?(path)
|
||||
rescue => e
|
||||
warn "Cleanup failed: #{e.message}"
|
||||
end
|
||||
|
||||
ctx = ZMQ::Context.new
|
||||
rep = ctx.socket(ZMQ::REP)
|
||||
rc = rep.bind(endpoint)
|
||||
abort "bind failed: #{ZMQ::Util.error_string}" unless rc == 0
|
||||
puts "RPC server on #{endpoint}"
|
||||
|
||||
# Define RPC methods (use a local variable, not a constant)
|
||||
rpc_methods = {
|
||||
"echo" => ->(params) { params }, # returns whatever you send
|
||||
"add" => ->(params) { Array(params).map(&:to_f).sum }, # add numbers
|
||||
"time" => ->(_params) { Time.now.utc.iso8601 } # current time
|
||||
}
|
||||
|
||||
ok = ->(id, result) { { "jsonrpc"=>"2.0", "id"=>id, "result"=>result }.to_json }
|
||||
err = ->(id, code, msg, data=nil) do
|
||||
body = { "jsonrpc"=>"2.0", "id"=>id, "error"=>{ "code"=>code, "message"=>msg } }
|
||||
body["error"]["data"] = data unless data.nil?
|
||||
body.to_json
|
||||
end
|
||||
|
||||
shutdown = proc do
|
||||
begin
|
||||
rep.close
|
||||
ensure
|
||||
ctx.terminate
|
||||
end
|
||||
puts "\nbye"
|
||||
begin
|
||||
path = endpoint.sub("ipc://", "")
|
||||
File.unlink(path) if File.exist?(path)
|
||||
rescue; end
|
||||
puts "\nbye"
|
||||
exit
|
||||
end
|
||||
trap("INT") { shutdown.call }
|
||||
trap("TERM") { shutdown.call }
|
||||
|
||||
loop do
|
||||
# raw = ''
|
||||
# next unless rep.recv_string(raw) == 0
|
||||
# puts raw
|
||||
|
||||
raw = ''
|
||||
rc = rep.recv_string(raw)
|
||||
puts "[server] recv rc=#{rc} raw=#{raw.inspect}"
|
||||
#next unless rc == 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if rc == -1
|
||||
errno = ZMQ::Util.errno
|
||||
warn "[server] recv error #{errno}: #{ZMQ::Util.error_string}"
|
||||
case errno
|
||||
when ZMQ::EINTR
|
||||
next # interrupted by signal; retry
|
||||
when ZMQ::ETERM
|
||||
break # context shutting down
|
||||
when ZMQ::EFSM
|
||||
# Socket in wrong state (likely previous send failed).
|
||||
# Reset the REP socket.
|
||||
rep.close
|
||||
rep = ctx.socket(ZMQ::REP)
|
||||
rep.setsockopt(ZMQ::LINGER, 0)
|
||||
rep.bind(ENDPOINT)
|
||||
next
|
||||
else
|
||||
sleep 0.01 # avoid hot loop on unexpected errors
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
begin
|
||||
req = JSON.parse(raw)
|
||||
id = req["id"]
|
||||
meth = req["method"]
|
||||
params = req["params"]
|
||||
|
||||
if req["jsonrpc"] != "2.0"
|
||||
rep.send_string(err.call(id, -32600, "Invalid Request: jsonrpc must be '2.0'"))
|
||||
next
|
||||
end
|
||||
|
||||
handler = rpc_methods[meth]
|
||||
unless handler
|
||||
rep.send_string(err.call(id, -32601, "Method not found: #{meth}"))
|
||||
next
|
||||
end
|
||||
|
||||
result = handler.call(params)
|
||||
rep.send_string(ok.call(id, result))
|
||||
rescue JSON::ParserError => e
|
||||
rep.send_string(err.call(nil, -32700, "Parse error", e.message))
|
||||
rescue => e
|
||||
rep.send_string(err.call(nil, -32000, "Server error", e.message))
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue