v0.5.2: shell_bg / remote_shell_bg — background launch tools
server.lua gains a shell_bg tool that launches a detached command via setsid + nohup + stdio-redirect + &, returns immediately with PID and log path. Linux-only for MVP (Windows Start-Process equivalent TBD). hub.lua gains remote_shell_bg, forwarding to backend shell_bg. lmcp-only, no ssh fallback — fallback for fire-and-forget is semantically murky. Addresses the 'how do I launch a daemon over lmcp without the sentinel- file wrapper blocking forever' question. Existing remote_shell keeps its current synchronous-with-timeout behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -355,7 +355,7 @@ math.randomseed(os.time())
|
|||||||
|
|
||||||
local server = lmcp.new(os.getenv("LMCP_NAME") or "hub-tools", {
|
local server = lmcp.new(os.getenv("LMCP_NAME") or "hub-tools", {
|
||||||
port = tonumber(os.getenv("LMCP_PORT") or arg[1]) or 8090,
|
port = tonumber(os.getenv("LMCP_PORT") or arg[1]) or 8090,
|
||||||
version = "0.5.0",
|
version = "0.5.2",
|
||||||
conf = os.getenv("LMCP_HUB_CONF") or "/opt/herding/etc/lmcp-hub.conf",
|
conf = os.getenv("LMCP_HUB_CONF") or "/opt/herding/etc/lmcp-hub.conf",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -433,6 +433,17 @@ server:tool("remote_edit_file",
|
|||||||
function(a) return call_remote("edit_file", a, false, nil) end
|
function(a) return call_remote("edit_file", a, false, nil) end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
server:tool("remote_shell_bg",
|
||||||
|
"Launch a detached background command on a fleet host (Linux). Returns PID + log path immediately. Requires backend lmcp v0.5.2+.",
|
||||||
|
{ type = "object", properties = {
|
||||||
|
host = HOST_ARG,
|
||||||
|
command = { type = "string", description = "Shell command" },
|
||||||
|
cwd = { type = "string" },
|
||||||
|
log = { type = "string", description = "Log file path" },
|
||||||
|
}, required = { "host", "command" } },
|
||||||
|
function(a) return call_remote("shell_bg", a, false, nil) end
|
||||||
|
)
|
||||||
|
|
||||||
server:tool("remote_list_dir", "List directory entries on a fleet host.",
|
server:tool("remote_list_dir", "List directory entries on a fleet host.",
|
||||||
{ type = "object", properties = {
|
{ type = "object", properties = {
|
||||||
host = HOST_ARG,
|
host = HOST_ARG,
|
||||||
|
|||||||
+43
@@ -170,6 +170,49 @@ server:tool("shell", "Execute a shell command.", {
|
|||||||
return run(cmd, a.timeout or 120)
|
return run(cmd, a.timeout or 120)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
server:tool("shell_bg",
|
||||||
|
"Fire-and-forget shell command (Linux-only). Fully detaches via setsid+nohup+stdio-redirect and returns immediately with PID and log path. Use for daemons that must outlive the lmcp request.",
|
||||||
|
{
|
||||||
|
type = "object",
|
||||||
|
properties = {
|
||||||
|
command = { type = "string", description = "Shell command to launch" },
|
||||||
|
cwd = { type = "string", description = "Working directory" },
|
||||||
|
log = { type = "string", description = "Log file (stdout+stderr). Default: /tmp/lmcp-bg-<ts>-<rand>.log" },
|
||||||
|
},
|
||||||
|
required = { "command" },
|
||||||
|
},
|
||||||
|
function(a)
|
||||||
|
if WINDOWS then
|
||||||
|
return "Error: shell_bg is Linux-only (Windows Start-Process equivalent TBD)"
|
||||||
|
end
|
||||||
|
if type(a.command) ~= "string" or a.command == "" then
|
||||||
|
return "Error: command required"
|
||||||
|
end
|
||||||
|
local log = a.log
|
||||||
|
if not log or log == "" then
|
||||||
|
log = string.format("/tmp/lmcp-bg-%d-%d.log", os.time(), math.random(1000, 9999))
|
||||||
|
end
|
||||||
|
local pid_file = log .. ".pid"
|
||||||
|
local inner = a.command
|
||||||
|
if a.cwd and a.cwd ~= "" then
|
||||||
|
inner = "cd '" .. a.cwd:gsub("'", "'\\''") .. "' && " .. inner
|
||||||
|
end
|
||||||
|
local sq = function(s) return "'" .. s:gsub("'", "'\\''") .. "'" end
|
||||||
|
local full = string.format(
|
||||||
|
"setsid nohup sh -c %s </dev/null >%s 2>&1 & echo $! > %s",
|
||||||
|
sq(inner), sq(log), sq(pid_file)
|
||||||
|
)
|
||||||
|
os.execute(full)
|
||||||
|
local f = io.open(pid_file, 'r')
|
||||||
|
local pid = "?"
|
||||||
|
if f then
|
||||||
|
pid = (f:read('*a') or ""):match("(%d+)") or "?"
|
||||||
|
f:close()
|
||||||
|
os.remove(pid_file)
|
||||||
|
end
|
||||||
|
return string.format("launched pid=%s log=%s", pid, log)
|
||||||
|
end)
|
||||||
|
|
||||||
server:tool("read_file", "Read a file.", {
|
server:tool("read_file", "Read a file.", {
|
||||||
type = "object",
|
type = "object",
|
||||||
properties = { path = { type = "string" } },
|
properties = { path = { type = "string" } },
|
||||||
|
|||||||
Reference in New Issue
Block a user