summaryrefslogtreecommitdiff
path: root/agent/tools/write.lua
diff options
context:
space:
mode:
Diffstat (limited to 'agent/tools/write.lua')
-rw-r--r--agent/tools/write.lua65
1 files changed, 54 insertions, 11 deletions
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