14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
# File 'lib/kairos_mcp/daemon/restricted_shell/runner.rb', line 14
def self.run_with_timeout(wrapped_cmd:, env:, cwd:, timeout:,
stdin_data: nil, max_output_bytes:,
cmd_for_hash: nil, sandbox_driver: :sandbox_exec)
r_out, w_out = IO.pipe
r_err, w_err = IO.pipe
r_in, w_in = stdin_data ? IO.pipe : [nil, nil]
pid = Process.spawn(
env, *wrapped_cmd,
chdir: cwd,
in: r_in || :close,
out: w_out, err: w_err,
pgroup: true,
unsetenv_others: true
)
w_out.close; w_err.close
r_in&.close
if w_in && stdin_data
w_in.write(stdin_data)
w_in.close
w_in = nil
end
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
deadline = start + timeout
stdout_buf = ''.dup
stderr_buf = ''.dup
stdout_trunc = false
stderr_trunc = false
status = nil
begin
readers = [r_out, r_err].compact
loop do
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
if remaining <= 0
kill_tree!(pid)
elapsed = (timeout * 1000).to_i
raise TimeoutError.new("timeout after #{timeout}s", elapsed_ms: elapsed, pid: pid)
end
break if readers.empty?
ready = IO.select(readers, nil, nil, [remaining, 0.1].min)
if ready
ready[0].each do |io|
chunk = io.read_nonblock(16_384, exception: false)
case chunk
when :wait_readable
next
when nil
io.close rescue nil
readers.delete(io)
else
if io == r_out
stdout_buf << chunk
if stdout_buf.bytesize > max_output_bytes
stdout_trunc = true
kill_tree!(pid)
raise OutputTruncated.new(:stdout, max_output_bytes)
end
else
stderr_buf << chunk
if stderr_buf.bytesize > max_output_bytes
stderr_trunc = true
kill_tree!(pid)
raise OutputTruncated.new(:stderr, max_output_bytes)
end
end
end
end
end
if readers.empty?
_, status = Process.waitpid2(pid, 0)
break
end
_, s = Process.waitpid2(pid, Process::WNOHANG)
if s
readers.each do |io|
loop do
chunk = io.read_nonblock(16_384, exception: false)
break if chunk.nil? || chunk == :wait_readable
if io == r_out
stdout_buf << chunk
else
stderr_buf << chunk
end
end
io.close rescue nil
end
status = s
break
end
end
rescue OutputTruncated, TimeoutError
Process.waitpid2(pid, Process::WNOHANG) rescue nil
raise
ensure
[r_out, r_err, w_in].compact.each { |io| io.close rescue nil }
end
_, status = Process.waitpid2(pid, 0) unless status
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
cmd_hash = cmd_for_hash ? compute_hash(cmd_for_hash, cwd) : nil
Result.new(
status: status&.exitstatus,
signal: status&.signaled? ? Signal.signame(status.termsig) : nil,
stdout: stdout_buf, stderr: stderr_buf,
duration_ms: elapsed,
stdout_truncated: stdout_trunc, stderr_truncated: stderr_trunc,
sandbox_driver: sandbox_driver,
cmd_hash: cmd_hash
)
end
|