Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- pushfs_client.lua
- -- Pocket computer client for "pushfs-v1" turtles/computers.
- -- Subcommands:
- -- ping [--all | --id N | --label L]
- -- put <src> <dst> [--all|--id N|--label L]
- -- exec "<cmd line>" [--capture] [--timeout S] [--cwd /] [--all|--id N|--label L]
- -- reboot [--all|--id N|--label L]
- --
- -- Reporting: this client aggregates and prints ALL replies it receives
- -- within a short window for each request (fan-in reporting).
- --------------------------
- -- Config
- --------------------------
- local PROTOCOL = "pushfs-v1"
- local SHARED_SECRET = nil -- set if servers require a token
- local ACK_WINDOW = 2.5 -- seconds to collect multiple ACKs
- local STEP_WINDOW = 2.5 -- per-step window during chunked PUT
- local CHUNK = 4096
- local MAX_INLINE = 48*1024
- -- Lua 5.1/5.2 compat
- local unpack = table.unpack or unpack
- --------------------------
- -- Utilities
- --------------------------
- local function die(msg) print(msg) error(msg, 0) end
- local function openModems()
- local ok = false
- for _, s in ipairs(rs.getSides()) do
- if peripheral.getType(s) == "modem" then
- if not rednet.isOpen(s) then rednet.open(s) end
- ok = true
- end
- end
- if not ok then die("No modem. Attach a wireless modem.") end
- end
- local function readAll(path)
- if not fs.exists(path) or fs.isDir(path) then return nil, "missing or dir" end
- local h = fs.open(path, "rb"); if not h then return nil, "open failed" end
- local d = h.readAll() or ""; h.close(); return d
- end
- local function adler32(str)
- local MOD = 65521
- local a,b = 1,0
- for i=1,#str do
- a = (a + string.byte(str,i)) % MOD
- b = (b + a) % MOD
- end
- return string.format("%08x", bit32.bor(bit32.lshift(b,16), a))
- end
- local function gen_txid()
- return tostring(os.epoch("utc")) .. "-" .. tostring(math.random(1,1e9))
- end
- local function parse_args(argv)
- local cmd = argv[1]; if not cmd then die("Usage: pushfs_client <ping|put|exec|reboot> ...") end
- local out = { cmd = cmd, args = {}, flags = {} }
- local i = 2
- while i <= #argv do
- local a = argv[i]
- if a:sub(1,2) == "--" then
- local k = a:sub(3); local v = true
- if i < #argv and argv[i+1]:sub(1,2) ~= "--" then v = argv[i+1]; i = i + 1 end
- out.flags[k] = v
- else table.insert(out.args, a) end
- i = i + 1
- end
- return out
- end
- local function build_target(flags)
- if flags["all"] == true or flags["all"] == "true" then return "all" end
- if flags["id"] then return { id = tonumber(flags["id"]) } end
- if flags["label"] then return { label = tostring(flags["label"]) } end
- die("Target required: use --all or --id N or --label L")
- end
- -- Collect ALL matching replies for a txid/op during window_s seconds.
- local function collect_acks(txid, want_op, window_s)
- local end_at = os.epoch("utc") + math.floor((window_s or ACK_WINDOW) * 1000)
- local seen = {} -- sender_id -> msg
- while true do
- local now = os.epoch("utc"); if now >= end_at then break end
- local timeout = (end_at - now) / 1000
- local sender, msg, proto = rednet.receive(PROTOCOL, timeout)
- if sender and type(msg)=="table" and msg.txid==txid and msg.op==want_op then
- msg.__sender = sender
- seen[sender] = msg
- end
- end
- -- flatten
- local list = {}
- for _, m in pairs(seen) do table.insert(list, m) end
- table.sort(list, function(a,b) return (a.__sender or 0) < (b.__sender or 0) end)
- return list
- end
- -- Send, then collect all acks in a window.
- local function send_and_collect(msg, want_op, window_s)
- rednet.broadcast(msg, PROTOCOL)
- return collect_acks(msg.txid, want_op, window_s)
- end
- local function print_acks(title, acks)
- print(title .. (": %d node(s)"):format(#acks))
- for _, m in ipairs(acks) do
- local who = ("id=%s"):format(tostring(m.__sender))
- if m.id or m.label then who = ("%s label=%s id=%s"):format(tostring(m.label or ""), tostring(m.id or "?"), tostring(m.__sender)) end
- local note = m.note or ""
- print(("- %s: %s%s"):format(who, m.ok and "ok" or "err", note ~= "" and (" ("..note..")") or ""))
- end
- end
- --------------------------
- -- Ops
- --------------------------
- local function op_ping(flags)
- local target = build_target(flags)
- local txid = gen_txid()
- local req = { op="ping", txid=txid, target=target, token=SHARED_SECRET }
- local acks = send_and_collect(req, "ping", ACK_WINDOW)
- print_acks("ping", acks)
- end
- local function op_put(args, flags)
- if #args < 2 then die("put <src> <dst> [--all|--id N|--label L]") end
- local src, dst = args[1], args[2]
- local data, e = readAll(src); if not data then die("read: "..e) end
- local target = build_target(flags)
- local sum = adler32(data)
- if #data <= MAX_INLINE then
- local txid = gen_txid()
- local req = { op="put", txid=txid, target=target, fname=dst, data=data, checksum=sum, token=SHARED_SECRET }
- local acks = send_and_collect(req, "put", ACK_WINDOW)
- print_acks(("put %s -> %s"):format(src, dst), acks)
- return
- end
- -- chunked: begin
- local txid = gen_txid()
- local begin = { op="begin", txid=txid, target=target, fname=dst, size=#data, checksum=sum, token=SHARED_SECRET }
- local a_begin = send_and_collect(begin, "begin", STEP_WINDOW)
- print_acks(("begin %s -> %s"):format(src, dst), a_begin)
- -- chunk loop
- local sent = 0
- while sent < #data do
- local chunk = data:sub(sent+1, math.min(sent + CHUNK, #data))
- rednet.broadcast({ op="chunk", txid=txid, target=target, data=chunk }, PROTOCOL)
- local a_chunk = collect_acks(txid, "chunk", STEP_WINDOW)
- print(("[chunk %d..%d] acks=%d"):format(sent+1, sent+#chunk, #a_chunk))
- sent = sent + #chunk
- end
- -- end
- local endm = { op="end", txid=txid, target=target }
- local a_end = send_and_collect(endm, "end", STEP_WINDOW)
- print_acks("end", a_end)
- end
- local function op_exec(args, flags)
- if #args < 1 then
- die([[exec "<cmd line>" [--capture] [--timeout S] [--cwd /] [--all|--id N|--label L]]])
- end
- local line = args[1]
- local target = build_target(flags)
- local capture = flags["capture"] == true or flags["capture"] == "true"
- local timeout = tonumber(flags["timeout"] or "0")
- local cwd = flags["cwd"]
- local txid = gen_txid()
- local req = {
- op="exec", txid=txid, target=target, line=line,
- capture=capture, timeout=timeout, cwd=cwd, token=SHARED_SECRET
- }
- rednet.broadcast(req, PROTOCOL)
- local acks = collect_acks(txid, "exec", ACK_WINDOW)
- if capture then
- -- print each node's output
- for _, m in ipairs(acks) do
- local who = ("id=%s"):format(tostring(m.__sender))
- if m.id or m.label then who = ("%s label=%s id=%s"):format(tostring(m.label or ""), tostring(m.id or "?"), tostring(m.__sender)) end
- print(("--- %s ---"):format(who))
- if m.ok then print(m.output or "") else print("[error] "..tostring(m.note or "")) end
- end
- else
- print_acks(("exec %q"):format(line), acks)
- end
- end
- local function op_reboot(flags)
- -- Use exec to request reboot on targets. No capture.
- local target = build_target(flags)
- local txid = gen_txid()
- local req = { op="exec", txid=txid, target=target, line="reboot", capture=false, token=SHARED_SECRET }
- local acks = send_and_collect(req, "exec", 1.0) -- quick window; they will drop off after
- print_acks("reboot", acks)
- print("Note: nodes may vanish from the network immediately after ACK.")
- end
- --------------------------
- -- Main
- --------------------------
- local function main(...)
- openModems()
- math.randomseed(os.epoch("utc") % 2147483647)
- local argv = { ... }
- local parsed = parse_args(argv)
- if parsed.cmd == "ping" then
- op_ping(parsed.flags)
- elseif parsed.cmd == "put" then
- op_put(parsed.args, parsed.flags)
- elseif parsed.cmd == "exec" then
- op_exec(parsed.args, parsed.flags)
- elseif parsed.cmd == "reboot" then
- op_reboot(parsed.flags)
- else
- die("Unknown subcommand: "..tostring(parsed.cmd))
- end
- end
- local ok, err = pcall(main, ...)
- if not ok then print("Error: "..tostring(err)) end
Advertisement
Add Comment
Please, Sign In to add comment