e2b 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/lib/e2b/api/http_client.rb +164 -0
- data/lib/e2b/client.rb +201 -0
- data/lib/e2b/configuration.rb +119 -0
- data/lib/e2b/errors.rb +88 -0
- data/lib/e2b/models/entry_info.rb +243 -0
- data/lib/e2b/models/process_result.rb +127 -0
- data/lib/e2b/models/sandbox_info.rb +94 -0
- data/lib/e2b/sandbox.rb +407 -0
- data/lib/e2b/services/base_service.rb +485 -0
- data/lib/e2b/services/command_handle.rb +350 -0
- data/lib/e2b/services/commands.rb +229 -0
- data/lib/e2b/services/filesystem.rb +373 -0
- data/lib/e2b/services/git.rb +893 -0
- data/lib/e2b/services/pty.rb +297 -0
- data/lib/e2b/services/watch_handle.rb +110 -0
- data/lib/e2b/version.rb +5 -0
- data/lib/e2b.rb +87 -0
- metadata +142 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module E2B
|
|
6
|
+
module Services
|
|
7
|
+
# Result of a command execution in the sandbox.
|
|
8
|
+
#
|
|
9
|
+
# Returned by {CommandHandle#wait} when the command finishes successfully.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = handle.wait
|
|
13
|
+
# puts result.stdout
|
|
14
|
+
# puts result.exit_code # => 0
|
|
15
|
+
class CommandResult
|
|
16
|
+
# @return [String] Standard output accumulated from the command
|
|
17
|
+
attr_reader :stdout
|
|
18
|
+
|
|
19
|
+
# @return [String] Standard error accumulated from the command
|
|
20
|
+
attr_reader :stderr
|
|
21
|
+
|
|
22
|
+
# @return [Integer] Exit code of the command (0 indicates success)
|
|
23
|
+
attr_reader :exit_code
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] Error message from command execution, if any
|
|
26
|
+
attr_reader :error
|
|
27
|
+
|
|
28
|
+
# @param stdout [String] Standard output
|
|
29
|
+
# @param stderr [String] Standard error
|
|
30
|
+
# @param exit_code [Integer] Exit code
|
|
31
|
+
# @param error [String, nil] Error message
|
|
32
|
+
def initialize(stdout: "", stderr: "", exit_code: 0, error: nil)
|
|
33
|
+
@stdout = stdout
|
|
34
|
+
@stderr = stderr
|
|
35
|
+
@exit_code = exit_code
|
|
36
|
+
@error = error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if the command finished successfully.
|
|
40
|
+
#
|
|
41
|
+
# @return [Boolean] true if exit code is 0 and no error is present
|
|
42
|
+
def success?
|
|
43
|
+
@exit_code == 0 && @error.nil?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Combined output (stdout + stderr).
|
|
47
|
+
#
|
|
48
|
+
# @return [String]
|
|
49
|
+
def output
|
|
50
|
+
"#{@stdout}#{@stderr}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String]
|
|
54
|
+
def to_s
|
|
55
|
+
"#<#{self.class.name} exit_code=#{@exit_code} stdout=#{@stdout.bytesize}B stderr=#{@stderr.bytesize}B>"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
alias inspect to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Handle for an executing command in the sandbox.
|
|
62
|
+
#
|
|
63
|
+
# Provides methods for waiting on the command, sending stdin input,
|
|
64
|
+
# killing the process, disconnecting from the event stream, and
|
|
65
|
+
# iterating over stdout/stderr/pty output as it arrives.
|
|
66
|
+
#
|
|
67
|
+
# Instances are returned by {Commands#run} (background mode),
|
|
68
|
+
# {Pty#create}, and {Pty#connect}.
|
|
69
|
+
#
|
|
70
|
+
# @example Wait for a command and get output
|
|
71
|
+
# handle = sandbox.pty.create
|
|
72
|
+
# result = handle.wait(on_pty: ->(data) { print data })
|
|
73
|
+
#
|
|
74
|
+
# @example Iterate over output
|
|
75
|
+
# handle.each do |stdout, stderr, pty|
|
|
76
|
+
# print stdout if stdout
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
# @example Kill a long-running process
|
|
80
|
+
# handle.kill
|
|
81
|
+
class CommandHandle
|
|
82
|
+
include Enumerable
|
|
83
|
+
|
|
84
|
+
# @return [Integer, nil] Process ID of the running command
|
|
85
|
+
attr_reader :pid
|
|
86
|
+
|
|
87
|
+
# Initialize a new CommandHandle.
|
|
88
|
+
#
|
|
89
|
+
# Callers should not construct this directly; it is created by service
|
|
90
|
+
# methods such as {Pty#create} or {Commands#run}.
|
|
91
|
+
#
|
|
92
|
+
# @param pid [Integer, nil] Process ID
|
|
93
|
+
# @param handle_kill [Proc] Proc that sends SIGKILL to the process
|
|
94
|
+
# @param handle_send_stdin [Proc] Proc that sends data to stdin/pty
|
|
95
|
+
# @param events_proc [Proc, nil] Proc that accepts a block and yields
|
|
96
|
+
# parsed events as they arrive. Each event is a Hash with keys like
|
|
97
|
+
# "event" => { "Start" => ..., "Data" => ..., "End" => ... }.
|
|
98
|
+
# May be nil if the result is already materialized.
|
|
99
|
+
# @param result [Hash, nil] Pre-materialized result from a synchronous
|
|
100
|
+
# RPC call. Expected keys: :events, :stdout, :stderr, :exit_code.
|
|
101
|
+
def initialize(pid:, handle_kill:, handle_send_stdin:, events_proc: nil, result: nil)
|
|
102
|
+
@pid = pid
|
|
103
|
+
@handle_kill = handle_kill
|
|
104
|
+
@handle_send_stdin = handle_send_stdin
|
|
105
|
+
@events_proc = events_proc
|
|
106
|
+
@result = result
|
|
107
|
+
@disconnected = false
|
|
108
|
+
@finished = false
|
|
109
|
+
@stdout = ""
|
|
110
|
+
@stderr = ""
|
|
111
|
+
@exit_code = nil
|
|
112
|
+
@error = nil
|
|
113
|
+
@mutex = Mutex.new
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Wait for the command to finish and return the result.
|
|
117
|
+
#
|
|
118
|
+
# If the command exits with a non-zero exit code, raises
|
|
119
|
+
# {CommandExitError}. Callbacks are invoked as output
|
|
120
|
+
# arrives (or immediately if the result is already materialized).
|
|
121
|
+
#
|
|
122
|
+
# @param on_stdout [Proc, nil] Called with each chunk of stdout (String)
|
|
123
|
+
# @param on_stderr [Proc, nil] Called with each chunk of stderr (String)
|
|
124
|
+
# @param on_pty [Proc, nil] Called with each chunk of PTY output (String)
|
|
125
|
+
# @return [CommandResult]
|
|
126
|
+
# @raise [CommandExitError] if exit code is non-zero
|
|
127
|
+
def wait(on_stdout: nil, on_stderr: nil, on_pty: nil)
|
|
128
|
+
consume_events(on_stdout: on_stdout, on_stderr: on_stderr, on_pty: on_pty)
|
|
129
|
+
build_result.tap do |cmd_result|
|
|
130
|
+
unless cmd_result.success?
|
|
131
|
+
raise CommandExitError.new(
|
|
132
|
+
stdout: cmd_result.stdout,
|
|
133
|
+
stderr: cmd_result.stderr,
|
|
134
|
+
exit_code: cmd_result.exit_code,
|
|
135
|
+
error: cmd_result.error
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Kill the running command with SIGKILL.
|
|
142
|
+
#
|
|
143
|
+
# @return [Boolean] true if the signal was sent successfully, false on error
|
|
144
|
+
def kill
|
|
145
|
+
@handle_kill.call
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Send data to the command's stdin (or PTY input).
|
|
149
|
+
#
|
|
150
|
+
# @param data [String] Data to write
|
|
151
|
+
# @return [void]
|
|
152
|
+
def send_stdin(data)
|
|
153
|
+
@handle_send_stdin.call(data)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Disconnect from the command event stream without killing the process.
|
|
157
|
+
#
|
|
158
|
+
# After disconnecting, {#wait} and {#each} will return immediately
|
|
159
|
+
# with whatever output has been accumulated so far. The underlying
|
|
160
|
+
# process continues running and can be reconnected to via
|
|
161
|
+
# {Commands#connect} or {Pty#connect}.
|
|
162
|
+
#
|
|
163
|
+
# @return [void]
|
|
164
|
+
def disconnect
|
|
165
|
+
@disconnected = true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Iterate over command output as it arrives.
|
|
169
|
+
#
|
|
170
|
+
# Yields a triple of [stdout, stderr, pty] for each chunk of output.
|
|
171
|
+
# Exactly one of the three will be non-nil per iteration.
|
|
172
|
+
#
|
|
173
|
+
# @yield [stdout, stderr, pty] Output chunks
|
|
174
|
+
# @yieldparam stdout [String, nil] Stdout data, or nil
|
|
175
|
+
# @yieldparam stderr [String, nil] Stderr data, or nil
|
|
176
|
+
# @yieldparam pty [String, nil] PTY data, or nil
|
|
177
|
+
# @return [void]
|
|
178
|
+
def each(&block)
|
|
179
|
+
return enum_for(:each) unless block_given?
|
|
180
|
+
|
|
181
|
+
if @result
|
|
182
|
+
# Iterate over pre-materialized events
|
|
183
|
+
iterate_materialized_events(&block)
|
|
184
|
+
elsif @events_proc
|
|
185
|
+
# Iterate over streaming events
|
|
186
|
+
iterate_streaming_events(&block)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Consume all remaining events, invoking callbacks along the way.
|
|
193
|
+
#
|
|
194
|
+
# @param on_stdout [Proc, nil]
|
|
195
|
+
# @param on_stderr [Proc, nil]
|
|
196
|
+
# @param on_pty [Proc, nil]
|
|
197
|
+
# @return [void]
|
|
198
|
+
def consume_events(on_stdout: nil, on_stderr: nil, on_pty: nil)
|
|
199
|
+
return if @finished
|
|
200
|
+
|
|
201
|
+
each do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
202
|
+
if stdout_chunk
|
|
203
|
+
on_stdout&.call(stdout_chunk)
|
|
204
|
+
@mutex.synchronize { @stdout += stdout_chunk }
|
|
205
|
+
end
|
|
206
|
+
if stderr_chunk
|
|
207
|
+
on_stderr&.call(stderr_chunk)
|
|
208
|
+
@mutex.synchronize { @stderr += stderr_chunk }
|
|
209
|
+
end
|
|
210
|
+
if pty_chunk
|
|
211
|
+
on_pty&.call(pty_chunk)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
@finished = true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Build a {CommandResult} from accumulated state.
|
|
219
|
+
#
|
|
220
|
+
# If we have a pre-materialized result and haven't iterated events,
|
|
221
|
+
# use its values directly.
|
|
222
|
+
#
|
|
223
|
+
# @return [CommandResult]
|
|
224
|
+
def build_result
|
|
225
|
+
if @result && !@finished
|
|
226
|
+
# Use the pre-materialized result from the synchronous RPC call
|
|
227
|
+
CommandResult.new(
|
|
228
|
+
stdout: @result[:stdout] || "",
|
|
229
|
+
stderr: @result[:stderr] || "",
|
|
230
|
+
exit_code: @result[:exit_code] || 0,
|
|
231
|
+
error: @result[:error]
|
|
232
|
+
)
|
|
233
|
+
else
|
|
234
|
+
CommandResult.new(
|
|
235
|
+
stdout: @stdout,
|
|
236
|
+
stderr: @stderr,
|
|
237
|
+
exit_code: @exit_code || 0,
|
|
238
|
+
error: @error
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Iterate over events from a pre-materialized result hash.
|
|
244
|
+
#
|
|
245
|
+
# The result hash is produced by {EnvdHttpClient#handle_streaming_rpc}
|
|
246
|
+
# or {EnvdHttpClient#handle_rpc_response} and contains an :events array
|
|
247
|
+
# with parsed JSON hashes.
|
|
248
|
+
#
|
|
249
|
+
# @yield [stdout, stderr, pty]
|
|
250
|
+
# @return [void]
|
|
251
|
+
def iterate_materialized_events
|
|
252
|
+
events = @result[:events] || []
|
|
253
|
+
events.each do |event_hash|
|
|
254
|
+
break if @disconnected
|
|
255
|
+
|
|
256
|
+
next unless event_hash.is_a?(Hash) && event_hash["event"]
|
|
257
|
+
|
|
258
|
+
event = event_hash["event"]
|
|
259
|
+
process_event(event) do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
260
|
+
yield stdout_chunk, stderr_chunk, pty_chunk
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Iterate over events from a streaming proc.
|
|
266
|
+
#
|
|
267
|
+
# The events_proc is called with a block; it yields parsed event
|
|
268
|
+
# hashes as they arrive from the RPC stream.
|
|
269
|
+
#
|
|
270
|
+
# @yield [stdout, stderr, pty]
|
|
271
|
+
# @return [void]
|
|
272
|
+
def iterate_streaming_events
|
|
273
|
+
@events_proc.call do |event_hash|
|
|
274
|
+
break if @disconnected
|
|
275
|
+
|
|
276
|
+
next unless event_hash.is_a?(Hash) && event_hash["event"]
|
|
277
|
+
|
|
278
|
+
event = event_hash["event"]
|
|
279
|
+
process_event(event) do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
280
|
+
yield stdout_chunk, stderr_chunk, pty_chunk
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Process a single event from the stream, extracting output data.
|
|
286
|
+
#
|
|
287
|
+
# Event shapes from the envd process service:
|
|
288
|
+
# - Start: { "Start" => { "pid" => 123 } }
|
|
289
|
+
# - Data: { "Data" => { "stdout" => "base64", "stderr" => "base64", "pty" => "base64" } }
|
|
290
|
+
# - End: { "End" => { "exitCode" => 0, "error" => "...", "status" => "..." } }
|
|
291
|
+
#
|
|
292
|
+
# @param event [Hash] The event sub-hash (value of "event" key)
|
|
293
|
+
# @yield [stdout, stderr, pty]
|
|
294
|
+
# @return [void]
|
|
295
|
+
def process_event(event)
|
|
296
|
+
# Handle Data event
|
|
297
|
+
data_event = event["Data"] || event["data"]
|
|
298
|
+
if data_event
|
|
299
|
+
stdout_chunk = decode_base64(data_event["stdout"])
|
|
300
|
+
stderr_chunk = decode_base64(data_event["stderr"])
|
|
301
|
+
pty_chunk = decode_base64(data_event["pty"])
|
|
302
|
+
|
|
303
|
+
yield(stdout_chunk, nil, nil) if stdout_chunk && !stdout_chunk.empty?
|
|
304
|
+
yield(nil, stderr_chunk, nil) if stderr_chunk && !stderr_chunk.empty?
|
|
305
|
+
yield(nil, nil, pty_chunk) if pty_chunk && !pty_chunk.empty?
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Handle End event
|
|
309
|
+
end_event = event["End"] || event["end"]
|
|
310
|
+
return unless end_event
|
|
311
|
+
|
|
312
|
+
exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
|
|
313
|
+
@exit_code = parse_exit_code(exit_value)
|
|
314
|
+
@error = end_event["error"] if end_event["error"] && !end_event["error"].empty?
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Decode a base64-encoded string.
|
|
318
|
+
#
|
|
319
|
+
# @param data [String, nil] Base64-encoded data
|
|
320
|
+
# @return [String, nil] Decoded string, or nil if input is nil/empty
|
|
321
|
+
def decode_base64(data)
|
|
322
|
+
return nil if data.nil? || data.empty?
|
|
323
|
+
|
|
324
|
+
Base64.decode64(data).force_encoding("UTF-8")
|
|
325
|
+
rescue StandardError
|
|
326
|
+
data.to_s
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Parse an exit code from various envd response formats.
|
|
330
|
+
#
|
|
331
|
+
# Handles integer values, string integers, and "exit status N" strings.
|
|
332
|
+
#
|
|
333
|
+
# @param value [Integer, String, nil] Raw exit code value
|
|
334
|
+
# @return [Integer]
|
|
335
|
+
def parse_exit_code(value)
|
|
336
|
+
return 0 if value.nil?
|
|
337
|
+
return value if value.is_a?(Integer)
|
|
338
|
+
|
|
339
|
+
str = value.to_s
|
|
340
|
+
if str =~ /exit status (\d+)/i
|
|
341
|
+
::Regexp.last_match(1).to_i
|
|
342
|
+
elsif str =~ /^(\d+)$/
|
|
343
|
+
::Regexp.last_match(1).to_i
|
|
344
|
+
else
|
|
345
|
+
1
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module E2B
|
|
7
|
+
module Services
|
|
8
|
+
# Command execution service for E2B sandbox
|
|
9
|
+
#
|
|
10
|
+
# Provides methods to run terminal commands inside the sandbox.
|
|
11
|
+
# Uses Connect RPC protocol to communicate with the envd process service.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# result = sandbox.commands.run("ls -la")
|
|
15
|
+
# puts result.stdout
|
|
16
|
+
#
|
|
17
|
+
# @example With streaming callbacks
|
|
18
|
+
# sandbox.commands.run("npm install",
|
|
19
|
+
# on_stdout: ->(data) { print data },
|
|
20
|
+
# on_stderr: ->(data) { warn data }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# @example Background command
|
|
24
|
+
# handle = sandbox.commands.run("sleep 10", background: true)
|
|
25
|
+
# handle.kill
|
|
26
|
+
class Commands < BaseService
|
|
27
|
+
# Run a command in the sandbox
|
|
28
|
+
#
|
|
29
|
+
# @param cmd [String] Command to execute (run via /bin/bash -l -c)
|
|
30
|
+
# @param background [Boolean] Run in background, returns CommandHandle
|
|
31
|
+
# @param envs [Hash{String => String}, nil] Environment variables
|
|
32
|
+
# @param user [String, nil] User to run the command as
|
|
33
|
+
# @param cwd [String, nil] Working directory
|
|
34
|
+
# @param on_stdout [Proc, nil] Callback for stdout data
|
|
35
|
+
# @param on_stderr [Proc, nil] Callback for stderr data
|
|
36
|
+
# @param timeout [Integer] Command timeout in seconds (default: 60)
|
|
37
|
+
# @param request_timeout [Integer, nil] HTTP request timeout in seconds
|
|
38
|
+
# @return [CommandResult, CommandHandle] Result or handle for background commands
|
|
39
|
+
#
|
|
40
|
+
# @raise [CommandExitError] If exit code is non-zero (foreground only)
|
|
41
|
+
def run(cmd, background: false, envs: nil, user: nil, cwd: nil,
|
|
42
|
+
on_stdout: nil, on_stderr: nil, timeout: 60, request_timeout: nil, &block)
|
|
43
|
+
# Build the process spec - official SDK always uses /bin/bash -l -c
|
|
44
|
+
process_spec = {
|
|
45
|
+
cmd: "/bin/bash",
|
|
46
|
+
args: ["-l", "-c", cmd.to_s]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if envs && !envs.empty?
|
|
50
|
+
env_map = {}
|
|
51
|
+
envs.each { |k, v| env_map[k.to_s] = v.to_s }
|
|
52
|
+
process_spec[:envs] = env_map
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
process_spec[:cwd] = cwd if cwd
|
|
56
|
+
|
|
57
|
+
body = { process: process_spec }
|
|
58
|
+
|
|
59
|
+
# Set up streaming callback
|
|
60
|
+
streaming_callback = nil
|
|
61
|
+
if on_stdout || on_stderr || block_given?
|
|
62
|
+
streaming_callback = lambda { |event_data|
|
|
63
|
+
stdout_chunk = event_data[:stdout]
|
|
64
|
+
stderr_chunk = event_data[:stderr]
|
|
65
|
+
|
|
66
|
+
on_stdout&.call(stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
|
|
67
|
+
on_stderr&.call(stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
|
|
68
|
+
|
|
69
|
+
if block_given?
|
|
70
|
+
yield(:stdout, stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
|
|
71
|
+
yield(:stderr, stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
|
|
72
|
+
end
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
effective_timeout = request_timeout || (timeout + 30)
|
|
77
|
+
|
|
78
|
+
response = envd_rpc("process.Process", "Start",
|
|
79
|
+
body: body,
|
|
80
|
+
timeout: effective_timeout,
|
|
81
|
+
on_event: streaming_callback)
|
|
82
|
+
|
|
83
|
+
pid = extract_pid(response)
|
|
84
|
+
|
|
85
|
+
if background
|
|
86
|
+
# Return a CommandHandle for background processes
|
|
87
|
+
CommandHandle.new(
|
|
88
|
+
pid: pid,
|
|
89
|
+
handle_kill: -> { kill(pid) },
|
|
90
|
+
handle_send_stdin: ->(data) { send_stdin(pid, data) },
|
|
91
|
+
result: response
|
|
92
|
+
)
|
|
93
|
+
else
|
|
94
|
+
# Return CommandResult for foreground processes
|
|
95
|
+
result = build_result(response)
|
|
96
|
+
|
|
97
|
+
# Raise on non-zero exit code (matching official SDK behavior)
|
|
98
|
+
if result.exit_code != 0
|
|
99
|
+
raise CommandExitError.new(
|
|
100
|
+
stdout: result.stdout,
|
|
101
|
+
stderr: result.stderr,
|
|
102
|
+
exit_code: result.exit_code,
|
|
103
|
+
error: result.error
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# List running processes
|
|
112
|
+
#
|
|
113
|
+
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
114
|
+
# @return [Array<Hash>] List of running processes with pid, config, tag
|
|
115
|
+
def list(request_timeout: nil)
|
|
116
|
+
response = envd_rpc("process.Process", "List",
|
|
117
|
+
body: {},
|
|
118
|
+
timeout: request_timeout || 30)
|
|
119
|
+
|
|
120
|
+
processes = []
|
|
121
|
+
events = response[:events] || []
|
|
122
|
+
events.each do |event|
|
|
123
|
+
next unless event.is_a?(Hash)
|
|
124
|
+
if event["processes"]
|
|
125
|
+
processes.concat(Array(event["processes"]))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
processes
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Kill a running process
|
|
132
|
+
#
|
|
133
|
+
# @param pid [Integer] Process ID to kill
|
|
134
|
+
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
135
|
+
# @return [Boolean] true if killed, false if not found
|
|
136
|
+
def kill(pid, request_timeout: nil)
|
|
137
|
+
envd_rpc("process.Process", "SendSignal",
|
|
138
|
+
body: {
|
|
139
|
+
process: { pid: pid },
|
|
140
|
+
signal: 9 # SIGKILL
|
|
141
|
+
},
|
|
142
|
+
timeout: request_timeout || 30)
|
|
143
|
+
true
|
|
144
|
+
rescue E2B::NotFoundError
|
|
145
|
+
false
|
|
146
|
+
rescue E2B::E2BError
|
|
147
|
+
false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Send stdin data to a running process
|
|
151
|
+
#
|
|
152
|
+
# @param pid [Integer] Process ID
|
|
153
|
+
# @param data [String] Data to send to stdin
|
|
154
|
+
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
155
|
+
def send_stdin(pid, data, request_timeout: nil)
|
|
156
|
+
encoded = Base64.strict_encode64(data.to_s)
|
|
157
|
+
envd_rpc("process.Process", "SendInput",
|
|
158
|
+
body: {
|
|
159
|
+
process: { pid: pid },
|
|
160
|
+
input: { stdin: encoded }
|
|
161
|
+
},
|
|
162
|
+
timeout: request_timeout || 30)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Connect to a running process
|
|
166
|
+
#
|
|
167
|
+
# @param pid [Integer] Process ID to connect to
|
|
168
|
+
# @param timeout [Integer] Connection timeout in seconds
|
|
169
|
+
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
170
|
+
# @return [CommandHandle] Handle for the connected process
|
|
171
|
+
def connect(pid, timeout: 60, request_timeout: nil)
|
|
172
|
+
response = envd_rpc("process.Process", "Connect",
|
|
173
|
+
body: { process: { pid: pid } },
|
|
174
|
+
timeout: request_timeout || (timeout + 30))
|
|
175
|
+
|
|
176
|
+
CommandHandle.new(
|
|
177
|
+
pid: pid,
|
|
178
|
+
handle_kill: -> { kill(pid) },
|
|
179
|
+
handle_send_stdin: ->(data) { send_stdin(pid, data) },
|
|
180
|
+
result: response
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
# Extract PID from streaming response events
|
|
187
|
+
def extract_pid(response)
|
|
188
|
+
events = response[:events] || []
|
|
189
|
+
events.each do |event|
|
|
190
|
+
next unless event.is_a?(Hash) && event["event"]
|
|
191
|
+
start_event = event["event"]["Start"] || event["event"]["start"]
|
|
192
|
+
if start_event && start_event["pid"]
|
|
193
|
+
return start_event["pid"].to_i
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Build CommandResult from response
|
|
200
|
+
def build_result(response)
|
|
201
|
+
stdout = response[:stdout] || ""
|
|
202
|
+
stderr = response[:stderr] || ""
|
|
203
|
+
exit_code = response[:exit_code]
|
|
204
|
+
error = nil
|
|
205
|
+
|
|
206
|
+
# Parse exit code
|
|
207
|
+
exit_code = exit_code.to_i if exit_code.is_a?(String) && exit_code.match?(/^\d+$/)
|
|
208
|
+
exit_code ||= 0
|
|
209
|
+
|
|
210
|
+
# Check events for error info
|
|
211
|
+
events = response[:events] || []
|
|
212
|
+
events.each do |event|
|
|
213
|
+
next unless event.is_a?(Hash) && event["event"]
|
|
214
|
+
end_event = event["event"]["End"] || event["event"]["end"]
|
|
215
|
+
if end_event
|
|
216
|
+
error = end_event["error"] if end_event["error"] && !end_event["error"].empty?
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
CommandResult.new(
|
|
221
|
+
stdout: stdout,
|
|
222
|
+
stderr: stderr,
|
|
223
|
+
exit_code: exit_code,
|
|
224
|
+
error: error
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|