robathome icon

MC controller pocket PC

robathome | PRO | 11/07/25 02:04:39 AM UTC (Edited) | 0 ⭐ | 284 👁️ | Never ⏰ | []
text |

7.98 KB

|

None

|

0 👍

/

0 👎

-- 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
 

Comments