From b6afac69b586dd9d68cccf686f3220581848c78b Mon Sep 17 00:00:00 2001 From: t Date: Mon, 1 Jun 2026 13:47:35 -0600 Subject: rework lua tool scheduler --- agent/tools/write.lua | 65 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 11 deletions(-) (limited to 'agent/tools/write.lua') diff --git a/agent/tools/write.lua b/agent/tools/write.lua index 01bf3b2..42cb85a 100644 --- a/agent/tools/write.lua +++ b/agent/tools/write.lua @@ -4,8 +4,46 @@ local uv = require("luv") +-- --------------------------------------------------------------------------- +-- Coroutine-synchronous libuv wrappers. panto runs each tool handler in +-- its own coroutine and drives a single `uv.run()` to completion; a +-- handler may only block by yielding on a pending libuv op whose +-- callback resumes it exactly once. `await` encapsulates that. +-- --------------------------------------------------------------------------- +local function await(arm) + local co = assert(coroutine.running(), + "write: await must run inside a tool handler coroutine") + local res, fired = nil, false + arm(function(...) + assert(not fired, "write: libuv callback fired twice") + fired = true + res = table.pack(...) + local ok, err = coroutine.resume(co) + if not ok then error(err, 0) end + end) + coroutine.yield() + return table.unpack(res, 1, res.n) +end + +-- Async `fs_mkdir`. luv's async callback is `(err, success)`, and on +-- EEXIST it reports the error via a string code in `err`; we detect +-- that textually since the async form doesn't surface the errno name +-- separately the way the sync form's third return does. +local function fs_mkdir(path, mode) + return await(function(resolve) uv.fs_mkdir(path, mode, resolve) end) +end +local function fs_open(path, flags, mode) + return await(function(resolve) uv.fs_open(path, flags, mode, resolve) end) +end +local function fs_write(fd, data, offset) + return await(function(resolve) uv.fs_write(fd, data, offset, resolve) end) +end +local function fs_close(fd) + return await(function(resolve) uv.fs_close(fd, resolve) end) +end + -- mkdir -p equivalent: walk the path components and create each. --- Ignores EEXIST so the call is idempotent. +-- Ignores "already exists" so the call is idempotent. local function mkdir_p(path) if path == "" or path == "." or path == "/" then return true end local parts = {} @@ -15,8 +53,8 @@ local function mkdir_p(path) local cur = (path:sub(1, 1) == "/") and "" or "." for _, piece in ipairs(parts) do cur = cur .. "/" .. piece - local ok, err, name = uv.fs_mkdir(cur, tonumber("755", 8)) - if not ok and name ~= "EEXIST" then + local err = fs_mkdir(cur, tonumber("755", 8)) + if err and not tostring(err):find("EEXIST") then return nil, err end end @@ -60,19 +98,24 @@ return { end end - local f, open_err = io.open(path, "wb") - if not f then + local open_err, fd = fs_open(path, "w", tonumber("644", 8)) + if not fd then return "Error: " .. (open_err or ("could not open " .. path .. " for writing")) end - local ok, write_err = f:write(content) - if not ok then - f:close() - return "Error: write failed: " .. tostring(write_err) + -- A single `fs_write` may short-write; loop until all bytes land. + local offset = 0 + while offset < #content do + local werr, n = fs_write(fd, content:sub(offset + 1), offset) + if not n then + fs_close(fd) + return "Error: write failed: " .. tostring(werr) + end + offset = offset + n end - local close_ok, close_err = f:close() - if not close_ok then + local close_err = fs_close(fd) + if close_err then return "Error: close failed: " .. tostring(close_err) end -- cgit v1.3