webmcp

view framework/env/execute/command.lua @ 489:42ddff7319e0

Avoid blocking of execute.command{...} if child process closes file descriptors but doesn't terminate
author jbe
date Sun Jun 18 01:58:25 2017 +0200 (2017-06-18)
parents 5b1c4f76e44f
children
line source
1 --[[--
2 output, -- collected data from stdout if process exited successfully
3 errmsg = -- error message if execution failed or if process didn't exit successfully
4 execute.command{
5 command = { filename, arg1, arg2, ... }, -- command and arguments
6 stdin_data = stdin_data, -- optional data to be sent to process via stdin
7 stdout_result_handler = stdout_result_handler, -- callback receiving: stdout data, success boolean, optional error message
8 stderr_line_handler = stderr_line_handler, -- callback for processing stderr line-wise
9 exit_handler = exit_handler, -- callback when process exited
10 signal_handler = signal_handler, -- callback when process terminated due to signal
11 timeout_handler = timeout_handler, -- callback when process gets killed due to timeout
12 abort_handler = abort_handler, -- callback when process gets killed due to request by poll function
13 abortable = abortable, -- set to true if process shall be terminated if poll function requests termination
14 poll = poll, -- alternative poll command with moonbridge_io.poll(...) semantics
15 db = db, -- database handle for waiting for notifies
16 db_notify_handler = db_notify_handler -- handler for database notifications which may return true to kill process
17 }
19 --]]--
21 function execute.command(args)
23 local moonbridge_io = require("moonbridge_io")
24 local poll = args.poll or moonbridge_io.poll
26 local stdout_chunks, stderr_chunks = {}, {}
28 local function return_error(errmsg)
29 if args.stdout_result_handler then
30 args.stdout_result_handler(table.concat(stdout_chunks), false, errmsg)
31 end
32 return nil, errmsg
33 end
35 if args.abortable then
36 local pollready, pollmsg, pollterm = poll(nil, nil, 0, true)
37 if pollterm then
38 if args.abort_handler then args.abort_handler(pollmsg) end
39 return_error(pollmsg)
40 end
41 end
43 local start = moonbridge_io.timeref()
44 local process, errmsg = moonbridge_io.exec(table.unpack(args.command))
45 if not process then return nil, errmsg end
47 local read_fds = {
48 [process] = true,
49 [process.stdout] = true,
50 [process.stderr] = true
51 }
52 local write_fds = {
53 [process.stdin] = true
54 }
55 if args.db then
56 read_fds[args.db.fd] = true
57 end
59 local function write(...)
60 if write_fds[process.stdin] then
61 local buffered = process.stdin:flush_nb(...)
62 if not buffered or buffered == 0 then
63 process.stdin:close()
64 write_fds[process.stdin] = nil
65 end
66 end
67 end
68 write(args.stdin_data or "")
70 local status
72 while
73 not status or
74 read_fds[process.stdout] or read_fds[process.stderr] or
75 write_fds[process.stdin]
76 do
77 local timeout = args.timeout and args.timeout-moonbridge_io.timeref(start)
78 local pollready, pollmsg, pollterm =
79 poll(read_fds, write_fds, timeout, args.abortable)
80 if not pollready then
81 if not status then
82 process:kill():wait()
83 end
84 if pollterm then
85 if args.abort_handler then args.abort_handler(pollmsg) end
86 else
87 if args.timeout_handler then args.timeout_handler() end
88 end
89 return return_error(pollmsg)
90 end
91 if not status then
92 status = process:wait_nb()
93 if status then
94 read_fds[process] = nil
95 end
96 end
97 if args.db then
98 local channel, payload, pid = db:wait(0)
99 if channel then
100 if args.db_notify_handler(channel, payload, pid) then
101 process:kill():wait()
102 return return_error("Database event received")
103 end
104 end
105 end
106 if read_fds[process.stdout] then
107 local chunk, status = process.stdout:read_nb()
108 if not chunk or status == "eof" then
109 process.stdout:close()
110 read_fds[process.stdout] = nil
111 end
112 if chunk and chunk ~= "" then
113 stdout_chunks[#stdout_chunks+1] = chunk
114 end
115 end
116 if read_fds[process.stderr] then
117 local chunk, status = process.stderr:read_nb()
118 if not chunk or status == "eof" then
119 process.stderr:close()
120 read_fds[process.stderr] = nil
121 end
122 if chunk and args.stderr_line_handler then
123 while true do
124 local chunk1, chunk2 = string.match(chunk, "(.-)\n(.*)")
125 if not chunk1 then break end
126 stderr_chunks[#stderr_chunks+1] = chunk1
127 args.stderr_line_handler(table.concat(stderr_chunks))
128 stderr_chunks = {}
129 chunk = chunk2
130 end
131 if chunk ~= "" then
132 stderr_chunks[#stderr_chunks+1] = chunk
133 end
134 if status == "eof" then
135 local line = table.concat(stderr_chunks)
136 if #line > 0 then args.stderr_line_handler(line) end
137 end
138 end
139 end
140 write()
141 end
143 if status < 0 then
144 if args.signal_handler then
145 args.signal_handler(-status)
146 end
147 return return_error("Command terminated by signal " .. -status)
148 elseif status > 0 then
149 if args.exit_handler then
150 args.exit_handler(status)
151 end
152 return return_error("Command returned exit code " .. status)
153 elseif args.stdout_result_handler then
154 args.stdout_result_handler(table.concat(stdout_chunks), true)
155 return true
156 else
157 return table.concat(stdout_chunks)
158 end
160 end

Impressum / About Us