e2b 0.2.0 → 0.3.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 +4 -4
- data/README.md +6 -2
- data/lib/e2b/api/http_client.rb +30 -19
- data/lib/e2b/client.rb +79 -36
- data/lib/e2b/configuration.rb +12 -6
- data/lib/e2b/dockerfile_parser.rb +179 -0
- data/lib/e2b/errors.rb +24 -1
- data/lib/e2b/models/build_info.rb +29 -0
- data/lib/e2b/models/build_status_reason.rb +27 -0
- data/lib/e2b/models/sandbox_info.rb +19 -2
- data/lib/e2b/models/snapshot_info.rb +19 -0
- data/lib/e2b/models/template_build_status_response.rb +31 -0
- data/lib/e2b/models/template_log_entry.rb +54 -0
- data/lib/e2b/models/template_tag.rb +34 -0
- data/lib/e2b/models/template_tag_info.rb +21 -0
- data/lib/e2b/paginator.rb +97 -0
- data/lib/e2b/ready_cmd.rb +36 -0
- data/lib/e2b/sandbox.rb +217 -66
- data/lib/e2b/sandbox_helpers.rb +100 -0
- data/lib/e2b/services/base_service.rb +64 -15
- data/lib/e2b/services/command_handle.rb +189 -36
- data/lib/e2b/services/commands.rb +37 -50
- data/lib/e2b/services/filesystem.rb +70 -23
- data/lib/e2b/services/live_streamable.rb +94 -0
- data/lib/e2b/services/pty.rb +13 -64
- data/lib/e2b/services/watch_handle.rb +6 -3
- data/lib/e2b/template.rb +1089 -0
- data/lib/e2b/template_logger.rb +52 -0
- data/lib/e2b/version.rb +1 -1
- data/lib/e2b.rb +16 -0
- metadata +44 -2
|
@@ -4,6 +4,7 @@ require "base64"
|
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "openssl"
|
|
6
6
|
require "ostruct"
|
|
7
|
+
require "rubygems/version"
|
|
7
8
|
|
|
8
9
|
module E2B
|
|
9
10
|
module Services
|
|
@@ -15,17 +16,21 @@ module E2B
|
|
|
15
16
|
class BaseService
|
|
16
17
|
# Default envd port
|
|
17
18
|
ENVD_PORT = 49983
|
|
19
|
+
DEFAULT_USERNAME = "user"
|
|
20
|
+
ENVD_DEFAULT_USER_VERSION = Gem::Version.new("0.4.0")
|
|
21
|
+
ENVD_RECURSIVE_WATCH_VERSION = Gem::Version.new("0.1.4")
|
|
18
22
|
|
|
19
23
|
# @param sandbox_id [String] Sandbox ID
|
|
20
24
|
# @param sandbox_domain [String] Sandbox domain (e.g., "e2b.app")
|
|
21
25
|
# @param api_key [String] API key for authentication
|
|
22
26
|
# @param access_token [String, nil] Sandbox-specific access token
|
|
23
27
|
# @param logger [Logger, nil] Optional logger
|
|
24
|
-
def initialize(sandbox_id:, sandbox_domain:, api_key:, access_token: nil, logger: nil)
|
|
28
|
+
def initialize(sandbox_id:, sandbox_domain:, api_key:, access_token: nil, envd_version: nil, logger: nil)
|
|
25
29
|
@sandbox_id = sandbox_id
|
|
26
30
|
@sandbox_domain = sandbox_domain
|
|
27
31
|
@api_key = api_key
|
|
28
32
|
@access_token = access_token
|
|
33
|
+
@envd_version = envd_version
|
|
29
34
|
@logger = logger
|
|
30
35
|
@envd_client = nil
|
|
31
36
|
end
|
|
@@ -40,18 +45,18 @@ module E2B
|
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
# Perform GET request to envd
|
|
43
|
-
def envd_get(path, params: {}, timeout: 120)
|
|
44
|
-
envd_client.get(path, params: params, timeout: timeout)
|
|
48
|
+
def envd_get(path, params: {}, timeout: 120, headers: nil)
|
|
49
|
+
envd_client.get(path, params: params, timeout: timeout, headers: headers)
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
# Perform POST request to envd
|
|
48
|
-
def envd_post(path, body: nil, timeout: 120)
|
|
49
|
-
envd_client.post(path, body: body, timeout: timeout)
|
|
53
|
+
def envd_post(path, body: nil, timeout: 120, headers: nil)
|
|
54
|
+
envd_client.post(path, body: body, timeout: timeout, headers: headers)
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
# Perform DELETE request to envd
|
|
53
|
-
def envd_delete(path, timeout: 120)
|
|
54
|
-
envd_client.delete(path, timeout: timeout)
|
|
58
|
+
def envd_delete(path, timeout: 120, headers: nil)
|
|
59
|
+
envd_client.delete(path, timeout: timeout, headers: headers)
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
# Perform Connect RPC call to envd
|
|
@@ -62,8 +67,39 @@ module E2B
|
|
|
62
67
|
# @param timeout [Integer] Request timeout in seconds
|
|
63
68
|
# @param on_event [Proc, nil] Callback for streaming events
|
|
64
69
|
# @return [Hash] Response with :events, :stdout, :stderr, :exit_code
|
|
65
|
-
def envd_rpc(service, method, body: {}, timeout: 120, on_event: nil)
|
|
66
|
-
envd_client.rpc(service, method, body: body, timeout: timeout, on_event: on_event)
|
|
70
|
+
def envd_rpc(service, method, body: {}, timeout: 120, on_event: nil, headers: nil)
|
|
71
|
+
envd_client.rpc(service, method, body: body, timeout: timeout, on_event: on_event, headers: headers)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def user_auth_headers(user)
|
|
75
|
+
resolved_user = resolve_username(user)
|
|
76
|
+
return nil if resolved_user.nil? || resolved_user.to_s.empty?
|
|
77
|
+
|
|
78
|
+
encoded = Base64.strict_encode64("#{resolved_user}:")
|
|
79
|
+
{ "Authorization" => "Basic #{encoded}" }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resolve_username(user)
|
|
83
|
+
return user unless user.nil? || user.to_s.empty?
|
|
84
|
+
return DEFAULT_USERNAME if legacy_default_user?
|
|
85
|
+
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def legacy_default_user?
|
|
90
|
+
return false if @envd_version.nil? || @envd_version.to_s.empty?
|
|
91
|
+
|
|
92
|
+
Gem::Version.new(@envd_version) < ENVD_DEFAULT_USER_VERSION
|
|
93
|
+
rescue ArgumentError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def supports_recursive_watch?
|
|
98
|
+
return true if @envd_version.nil? || @envd_version.to_s.empty?
|
|
99
|
+
|
|
100
|
+
Gem::Version.new(@envd_version) >= ENVD_RECURSIVE_WATCH_VERSION
|
|
101
|
+
rescue ArgumentError
|
|
102
|
+
true
|
|
67
103
|
end
|
|
68
104
|
|
|
69
105
|
private
|
|
@@ -98,28 +134,31 @@ module E2B
|
|
|
98
134
|
@connection = build_connection
|
|
99
135
|
end
|
|
100
136
|
|
|
101
|
-
def get(path, params: {}, timeout: DEFAULT_TIMEOUT)
|
|
137
|
+
def get(path, params: {}, timeout: DEFAULT_TIMEOUT, headers: nil)
|
|
102
138
|
handle_response do
|
|
103
139
|
@connection.get(normalize_path(path)) do |req|
|
|
104
140
|
req.params = params
|
|
105
141
|
req.options.timeout = timeout
|
|
142
|
+
req.headers.update(headers) if headers
|
|
106
143
|
end
|
|
107
144
|
end
|
|
108
145
|
end
|
|
109
146
|
|
|
110
|
-
def post(path, body: nil, timeout: DEFAULT_TIMEOUT)
|
|
147
|
+
def post(path, body: nil, timeout: DEFAULT_TIMEOUT, headers: nil)
|
|
111
148
|
handle_response do
|
|
112
149
|
@connection.post(normalize_path(path)) do |req|
|
|
113
150
|
req.body = body.to_json if body
|
|
114
151
|
req.options.timeout = timeout
|
|
152
|
+
req.headers.update(headers) if headers
|
|
115
153
|
end
|
|
116
154
|
end
|
|
117
155
|
end
|
|
118
156
|
|
|
119
|
-
def delete(path, timeout: DEFAULT_TIMEOUT)
|
|
157
|
+
def delete(path, timeout: DEFAULT_TIMEOUT, headers: nil)
|
|
120
158
|
handle_response do
|
|
121
159
|
@connection.delete(normalize_path(path)) do |req|
|
|
122
160
|
req.options.timeout = timeout
|
|
161
|
+
req.headers.update(headers) if headers
|
|
123
162
|
end
|
|
124
163
|
end
|
|
125
164
|
end
|
|
@@ -132,7 +171,7 @@ module E2B
|
|
|
132
171
|
# @param timeout [Integer] Timeout in seconds
|
|
133
172
|
# @param on_event [Proc, nil] Callback for streaming events
|
|
134
173
|
# @return [Hash] Response
|
|
135
|
-
def rpc(service, method, body: {}, timeout: DEFAULT_TIMEOUT, on_event: nil)
|
|
174
|
+
def rpc(service, method, body: {}, timeout: DEFAULT_TIMEOUT, on_event: nil, headers: nil)
|
|
136
175
|
path = "/#{service}/#{method}"
|
|
137
176
|
json_body = body.to_json
|
|
138
177
|
envelope = create_connect_envelope(json_body)
|
|
@@ -140,7 +179,7 @@ module E2B
|
|
|
140
179
|
log_debug("RPC #{service}/#{method}")
|
|
141
180
|
|
|
142
181
|
if on_event
|
|
143
|
-
return handle_streaming_rpc(path, envelope, timeout, on_event)
|
|
182
|
+
return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
144
183
|
end
|
|
145
184
|
|
|
146
185
|
handle_rpc_response(service, method) do
|
|
@@ -152,6 +191,7 @@ module E2B
|
|
|
152
191
|
request["Content-Type"] = "application/connect+json"
|
|
153
192
|
request["X-Access-Token"] = @access_token if @access_token
|
|
154
193
|
request["Connection"] = "keep-alive"
|
|
194
|
+
apply_custom_headers(request, headers)
|
|
155
195
|
request.body = envelope
|
|
156
196
|
|
|
157
197
|
response = http.request(request)
|
|
@@ -167,7 +207,7 @@ module E2B
|
|
|
167
207
|
end
|
|
168
208
|
|
|
169
209
|
# Streaming RPC with chunked response processing
|
|
170
|
-
def handle_streaming_rpc(path, envelope, timeout, on_event)
|
|
210
|
+
def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
171
211
|
result = { events: [], stdout: "", stderr: "", exit_code: nil }
|
|
172
212
|
buffer = "".b
|
|
173
213
|
|
|
@@ -180,6 +220,7 @@ module E2B
|
|
|
180
220
|
request["Content-Type"] = "application/connect+json"
|
|
181
221
|
request["X-Access-Token"] = @access_token if @access_token
|
|
182
222
|
request["Connection"] = "keep-alive"
|
|
223
|
+
apply_custom_headers(request, headers)
|
|
183
224
|
request.body = envelope
|
|
184
225
|
|
|
185
226
|
http.start do |conn|
|
|
@@ -480,6 +521,14 @@ module E2B
|
|
|
480
521
|
def log_debug(message)
|
|
481
522
|
@logger&.debug("[E2B] #{message}")
|
|
482
523
|
end
|
|
524
|
+
|
|
525
|
+
def apply_custom_headers(request, headers)
|
|
526
|
+
return unless headers
|
|
527
|
+
|
|
528
|
+
headers.each do |key, value|
|
|
529
|
+
request[key] = value
|
|
530
|
+
end
|
|
531
|
+
end
|
|
483
532
|
end
|
|
484
533
|
end
|
|
485
534
|
end
|
|
@@ -4,6 +4,65 @@ require "base64"
|
|
|
4
4
|
|
|
5
5
|
module E2B
|
|
6
6
|
module Services
|
|
7
|
+
# Thread-safe buffer for live command events.
|
|
8
|
+
#
|
|
9
|
+
# Allows a producer thread to push envd events while the command handle
|
|
10
|
+
# drains them on demand from {CommandHandle#each} or {CommandHandle#wait}.
|
|
11
|
+
class LiveEventStream
|
|
12
|
+
def initialize
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@condition = ConditionVariable.new
|
|
15
|
+
@items = []
|
|
16
|
+
@closed = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def push(event)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
return if @closed
|
|
22
|
+
|
|
23
|
+
@items << [:event, event]
|
|
24
|
+
@condition.signal
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fail(error)
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
return if @closed
|
|
31
|
+
|
|
32
|
+
@items << [:error, error]
|
|
33
|
+
@condition.signal
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close(discard_pending: false)
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
return if @closed
|
|
40
|
+
|
|
41
|
+
@closed = true
|
|
42
|
+
@items.clear if discard_pending
|
|
43
|
+
@condition.broadcast
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def each
|
|
48
|
+
return enum_for(:each) unless block_given?
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
item = @mutex.synchronize do
|
|
52
|
+
@condition.wait(@mutex) while @items.empty? && !@closed
|
|
53
|
+
@items.shift
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
break unless item
|
|
57
|
+
|
|
58
|
+
type, payload = item
|
|
59
|
+
raise payload if type == :error
|
|
60
|
+
|
|
61
|
+
yield payload
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
7
66
|
# Result of a command execution in the sandbox.
|
|
8
67
|
#
|
|
9
68
|
# Returned by {CommandHandle#wait} when the command finishes successfully.
|
|
@@ -92,25 +151,29 @@ module E2B
|
|
|
92
151
|
# @param pid [Integer, nil] Process ID
|
|
93
152
|
# @param handle_kill [Proc] Proc that sends SIGKILL to the process
|
|
94
153
|
# @param handle_send_stdin [Proc] Proc that sends data to stdin/pty
|
|
154
|
+
# @param handle_disconnect [Proc, nil] Proc that disconnects the event stream
|
|
95
155
|
# @param events_proc [Proc, nil] Proc that accepts a block and yields
|
|
96
156
|
# parsed events as they arrive. Each event is a Hash with keys like
|
|
97
157
|
# "event" => { "Start" => ..., "Data" => ..., "End" => ... }.
|
|
98
158
|
# May be nil if the result is already materialized.
|
|
99
159
|
# @param result [Hash, nil] Pre-materialized result from a synchronous
|
|
100
160
|
# RPC call. Expected keys: :events, :stdout, :stderr, :exit_code.
|
|
101
|
-
def initialize(pid:, handle_kill:, handle_send_stdin:, events_proc: nil, result: nil)
|
|
161
|
+
def initialize(pid:, handle_kill:, handle_send_stdin:, handle_disconnect: nil, events_proc: nil, result: nil)
|
|
102
162
|
@pid = pid
|
|
103
163
|
@handle_kill = handle_kill
|
|
104
164
|
@handle_send_stdin = handle_send_stdin
|
|
165
|
+
@handle_disconnect = handle_disconnect
|
|
105
166
|
@events_proc = events_proc
|
|
106
167
|
@result = result
|
|
107
168
|
@disconnected = false
|
|
108
169
|
@finished = false
|
|
170
|
+
@completed = false
|
|
109
171
|
@stdout = ""
|
|
110
172
|
@stderr = ""
|
|
111
173
|
@exit_code = nil
|
|
112
174
|
@error = nil
|
|
113
175
|
@mutex = Mutex.new
|
|
176
|
+
@materialized_event_index = 0
|
|
114
177
|
end
|
|
115
178
|
|
|
116
179
|
# Wait for the command to finish and return the result.
|
|
@@ -126,6 +189,10 @@ module E2B
|
|
|
126
189
|
# @raise [CommandExitError] if exit code is non-zero
|
|
127
190
|
def wait(on_stdout: nil, on_stderr: nil, on_pty: nil)
|
|
128
191
|
consume_events(on_stdout: on_stdout, on_stderr: on_stderr, on_pty: on_pty)
|
|
192
|
+
unless @disconnected || completed?
|
|
193
|
+
raise E2BError, "Command ended without an end event"
|
|
194
|
+
end
|
|
195
|
+
|
|
129
196
|
build_result.tap do |cmd_result|
|
|
130
197
|
unless cmd_result.success?
|
|
131
198
|
raise CommandExitError.new(
|
|
@@ -162,7 +229,10 @@ module E2B
|
|
|
162
229
|
#
|
|
163
230
|
# @return [void]
|
|
164
231
|
def disconnect
|
|
232
|
+
return if @disconnected
|
|
233
|
+
|
|
165
234
|
@disconnected = true
|
|
235
|
+
@handle_disconnect&.call
|
|
166
236
|
end
|
|
167
237
|
|
|
168
238
|
# Iterate over command output as it arrives.
|
|
@@ -201,11 +271,9 @@ module E2B
|
|
|
201
271
|
each do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
202
272
|
if stdout_chunk
|
|
203
273
|
on_stdout&.call(stdout_chunk)
|
|
204
|
-
@mutex.synchronize { @stdout += stdout_chunk }
|
|
205
274
|
end
|
|
206
275
|
if stderr_chunk
|
|
207
276
|
on_stderr&.call(stderr_chunk)
|
|
208
|
-
@mutex.synchronize { @stderr += stderr_chunk }
|
|
209
277
|
end
|
|
210
278
|
if pty_chunk
|
|
211
279
|
on_pty&.call(pty_chunk)
|
|
@@ -222,22 +290,12 @@ module E2B
|
|
|
222
290
|
#
|
|
223
291
|
# @return [CommandResult]
|
|
224
292
|
def build_result
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
293
|
+
CommandResult.new(
|
|
294
|
+
stdout: resolved_stdout,
|
|
295
|
+
stderr: resolved_stderr,
|
|
296
|
+
exit_code: resolved_exit_code || 0,
|
|
297
|
+
error: resolved_error
|
|
298
|
+
)
|
|
241
299
|
end
|
|
242
300
|
|
|
243
301
|
# Iterate over events from a pre-materialized result hash.
|
|
@@ -249,14 +307,14 @@ module E2B
|
|
|
249
307
|
# @yield [stdout, stderr, pty]
|
|
250
308
|
# @return [void]
|
|
251
309
|
def iterate_materialized_events
|
|
252
|
-
events =
|
|
253
|
-
events.
|
|
310
|
+
events = result_value(:events) || []
|
|
311
|
+
while @materialized_event_index < events.length
|
|
254
312
|
break if @disconnected
|
|
255
313
|
|
|
256
|
-
|
|
314
|
+
event_hash = events[@materialized_event_index]
|
|
315
|
+
@materialized_event_index += 1
|
|
257
316
|
|
|
258
|
-
|
|
259
|
-
process_event(event) do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
317
|
+
process_message(event_hash) do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
260
318
|
yield stdout_chunk, stderr_chunk, pty_chunk
|
|
261
319
|
end
|
|
262
320
|
end
|
|
@@ -270,25 +328,58 @@ module E2B
|
|
|
270
328
|
# @yield [stdout, stderr, pty]
|
|
271
329
|
# @return [void]
|
|
272
330
|
def iterate_streaming_events
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
next unless event_hash.is_a?(Hash) && event_hash["event"]
|
|
331
|
+
catch(:stop_iteration) do
|
|
332
|
+
@events_proc.call do |event_hash|
|
|
333
|
+
throw :stop_iteration if @disconnected
|
|
277
334
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
335
|
+
process_message(event_hash) do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
336
|
+
yield stdout_chunk, stderr_chunk, pty_chunk
|
|
337
|
+
end
|
|
281
338
|
end
|
|
282
339
|
end
|
|
283
340
|
end
|
|
284
341
|
|
|
285
|
-
# Process a single
|
|
342
|
+
# Process a single stream message, extracting output data.
|
|
343
|
+
#
|
|
344
|
+
# Messages usually arrive with an "event" wrapper from the envd process
|
|
345
|
+
# service, but some paths also surface top-level stdout/stderr/exitCode
|
|
346
|
+
# fields. This method handles both shapes.
|
|
286
347
|
#
|
|
287
348
|
# Event shapes from the envd process service:
|
|
288
349
|
# - Start: { "Start" => { "pid" => 123 } }
|
|
289
350
|
# - Data: { "Data" => { "stdout" => "base64", "stderr" => "base64", "pty" => "base64" } }
|
|
290
351
|
# - End: { "End" => { "exitCode" => 0, "error" => "...", "status" => "..." } }
|
|
291
352
|
#
|
|
353
|
+
# @param message [Hash] A raw stream message
|
|
354
|
+
# @yield [stdout, stderr, pty]
|
|
355
|
+
# @return [void]
|
|
356
|
+
def process_message(message)
|
|
357
|
+
return unless message.is_a?(Hash)
|
|
358
|
+
|
|
359
|
+
event = message["event"]
|
|
360
|
+
process_event(event) { |stdout_chunk, stderr_chunk, pty_chunk| yield stdout_chunk, stderr_chunk, pty_chunk } if event.is_a?(Hash)
|
|
361
|
+
|
|
362
|
+
if event.nil?
|
|
363
|
+
stdout_chunk = decode_base64(message["stdout"])
|
|
364
|
+
stderr_chunk = decode_base64(message["stderr"])
|
|
365
|
+
|
|
366
|
+
if stdout_chunk && !stdout_chunk.empty?
|
|
367
|
+
append_stdout(stdout_chunk)
|
|
368
|
+
yield(stdout_chunk, nil, nil)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
if stderr_chunk && !stderr_chunk.empty?
|
|
372
|
+
append_stderr(stderr_chunk)
|
|
373
|
+
yield(nil, stderr_chunk, nil)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
exit_value = message["exitCode"] || message["exit_code"]
|
|
378
|
+
complete!(exit_value, message["error"]) if exit_value
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Process a single event from the stream, extracting output data.
|
|
382
|
+
#
|
|
292
383
|
# @param event [Hash] The event sub-hash (value of "event" key)
|
|
293
384
|
# @yield [stdout, stderr, pty]
|
|
294
385
|
# @return [void]
|
|
@@ -300,8 +391,16 @@ module E2B
|
|
|
300
391
|
stderr_chunk = decode_base64(data_event["stderr"])
|
|
301
392
|
pty_chunk = decode_base64(data_event["pty"])
|
|
302
393
|
|
|
303
|
-
|
|
304
|
-
|
|
394
|
+
if stdout_chunk && !stdout_chunk.empty?
|
|
395
|
+
append_stdout(stdout_chunk)
|
|
396
|
+
yield(stdout_chunk, nil, nil)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
if stderr_chunk && !stderr_chunk.empty?
|
|
400
|
+
append_stderr(stderr_chunk)
|
|
401
|
+
yield(nil, stderr_chunk, nil)
|
|
402
|
+
end
|
|
403
|
+
|
|
305
404
|
yield(nil, nil, pty_chunk) if pty_chunk && !pty_chunk.empty?
|
|
306
405
|
end
|
|
307
406
|
|
|
@@ -310,8 +409,62 @@ module E2B
|
|
|
310
409
|
return unless end_event
|
|
311
410
|
|
|
312
411
|
exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
|
|
313
|
-
|
|
314
|
-
|
|
412
|
+
complete!(exit_value, end_event["error"])
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def append_stdout(chunk)
|
|
416
|
+
@mutex.synchronize { @stdout += chunk }
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def append_stderr(chunk)
|
|
420
|
+
@mutex.synchronize { @stderr += chunk }
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def complete!(exit_value, error_value)
|
|
424
|
+
@mutex.synchronize do
|
|
425
|
+
@completed = true
|
|
426
|
+
@exit_code = parse_exit_code(exit_value)
|
|
427
|
+
@error = error_value if error_value && !error_value.empty?
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def completed?
|
|
432
|
+
return true if @completed
|
|
433
|
+
|
|
434
|
+
!result_value(:exit_code, "exit_code", "exitCode").nil?
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def resolved_stdout
|
|
438
|
+
fallback = result_value(:stdout, "stdout")
|
|
439
|
+
@stdout.empty? && fallback ? fallback : @stdout
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def resolved_stderr
|
|
443
|
+
fallback = result_value(:stderr, "stderr")
|
|
444
|
+
@stderr.empty? && fallback ? fallback : @stderr
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def resolved_exit_code
|
|
448
|
+
return @exit_code unless @exit_code.nil?
|
|
449
|
+
|
|
450
|
+
raw_exit_code = result_value(:exit_code, "exit_code", "exitCode")
|
|
451
|
+
return nil if raw_exit_code.nil?
|
|
452
|
+
|
|
453
|
+
parse_exit_code(raw_exit_code)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def resolved_error
|
|
457
|
+
@error || result_value(:error, "error")
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def result_value(*keys)
|
|
461
|
+
return nil unless @result.is_a?(Hash)
|
|
462
|
+
|
|
463
|
+
keys.each do |key|
|
|
464
|
+
return @result[key] if @result.key?(key)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
nil
|
|
315
468
|
end
|
|
316
469
|
|
|
317
470
|
# Decode a base64-encoded string.
|
|
@@ -24,6 +24,7 @@ module E2B
|
|
|
24
24
|
# handle = sandbox.commands.run("sleep 10", background: true)
|
|
25
25
|
# handle.kill
|
|
26
26
|
class Commands < BaseService
|
|
27
|
+
include LiveStreamable
|
|
27
28
|
# Run a command in the sandbox
|
|
28
29
|
#
|
|
29
30
|
# @param cmd [String] Command to execute (run via /bin/bash -l -c)
|
|
@@ -55,8 +56,10 @@ module E2B
|
|
|
55
56
|
process_spec[:cwd] = cwd if cwd
|
|
56
57
|
|
|
57
58
|
body = { process: process_spec }
|
|
59
|
+
headers = user_auth_headers(user)
|
|
58
60
|
|
|
59
61
|
# Set up streaming callback
|
|
62
|
+
stream_block = block if block_given?
|
|
60
63
|
streaming_callback = nil
|
|
61
64
|
if on_stdout || on_stderr || block_given?
|
|
62
65
|
streaming_callback = lambda { |event_data|
|
|
@@ -66,46 +69,45 @@ module E2B
|
|
|
66
69
|
on_stdout&.call(stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
|
|
67
70
|
on_stderr&.call(stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
|
|
68
71
|
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
yield(:stderr, stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
|
|
72
|
-
end
|
|
72
|
+
stream_block&.call(:stdout, stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
|
|
73
|
+
stream_block&.call(:stderr, stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
|
|
73
74
|
}
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
effective_timeout = request_timeout || (timeout + 30)
|
|
77
78
|
|
|
79
|
+
if background
|
|
80
|
+
return build_live_handle(
|
|
81
|
+
rpc_method: "Start",
|
|
82
|
+
body: body,
|
|
83
|
+
timeout: effective_timeout,
|
|
84
|
+
headers: headers,
|
|
85
|
+
on_stdout: on_stdout,
|
|
86
|
+
on_stderr: on_stderr,
|
|
87
|
+
&block
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
78
91
|
response = envd_rpc("process.Process", "Start",
|
|
79
92
|
body: body,
|
|
80
93
|
timeout: effective_timeout,
|
|
94
|
+
headers: headers,
|
|
81
95
|
on_event: streaming_callback)
|
|
82
96
|
|
|
83
|
-
|
|
97
|
+
# Return CommandResult for foreground processes
|
|
98
|
+
result = build_result(response)
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
# Raise on non-zero exit code (matching official SDK behavior)
|
|
101
|
+
if result.exit_code != 0
|
|
102
|
+
raise CommandExitError.new(
|
|
103
|
+
stdout: result.stdout,
|
|
104
|
+
stderr: result.stderr,
|
|
105
|
+
exit_code: result.exit_code,
|
|
106
|
+
error: result.error
|
|
92
107
|
)
|
|
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
108
|
end
|
|
109
|
+
|
|
110
|
+
result
|
|
109
111
|
end
|
|
110
112
|
|
|
111
113
|
# List running processes
|
|
@@ -133,12 +135,13 @@ module E2B
|
|
|
133
135
|
# @param pid [Integer] Process ID to kill
|
|
134
136
|
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
135
137
|
# @return [Boolean] true if killed, false if not found
|
|
136
|
-
def kill(pid, request_timeout: nil)
|
|
138
|
+
def kill(pid, request_timeout: nil, headers: nil)
|
|
137
139
|
envd_rpc("process.Process", "SendSignal",
|
|
138
140
|
body: {
|
|
139
141
|
process: { pid: pid },
|
|
140
142
|
signal: 9 # SIGKILL
|
|
141
143
|
},
|
|
144
|
+
headers: headers,
|
|
142
145
|
timeout: request_timeout || 30)
|
|
143
146
|
true
|
|
144
147
|
rescue E2B::NotFoundError
|
|
@@ -152,13 +155,14 @@ module E2B
|
|
|
152
155
|
# @param pid [Integer] Process ID
|
|
153
156
|
# @param data [String] Data to send to stdin
|
|
154
157
|
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
155
|
-
def send_stdin(pid, data, request_timeout: nil)
|
|
158
|
+
def send_stdin(pid, data, request_timeout: nil, headers: nil)
|
|
156
159
|
encoded = Base64.strict_encode64(data.to_s)
|
|
157
160
|
envd_rpc("process.Process", "SendInput",
|
|
158
161
|
body: {
|
|
159
162
|
process: { pid: pid },
|
|
160
163
|
input: { stdin: encoded }
|
|
161
164
|
},
|
|
165
|
+
headers: headers,
|
|
162
166
|
timeout: request_timeout || 30)
|
|
163
167
|
end
|
|
164
168
|
|
|
@@ -169,33 +173,16 @@ module E2B
|
|
|
169
173
|
# @param request_timeout [Integer, nil] Request timeout in seconds
|
|
170
174
|
# @return [CommandHandle] Handle for the connected process
|
|
171
175
|
def connect(pid, timeout: 60, request_timeout: nil)
|
|
172
|
-
|
|
176
|
+
build_live_handle(
|
|
177
|
+
rpc_method: "Connect",
|
|
173
178
|
body: { process: { pid: pid } },
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
CommandHandle.new(
|
|
177
|
-
pid: pid,
|
|
178
|
-
handle_kill: -> { kill(pid) },
|
|
179
|
-
handle_send_stdin: ->(data) { send_stdin(pid, data) },
|
|
180
|
-
result: response
|
|
179
|
+
headers: user_auth_headers(nil),
|
|
180
|
+
timeout: request_timeout || (timeout + 30)
|
|
181
181
|
)
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
private
|
|
185
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
186
|
# Build CommandResult from response
|
|
200
187
|
def build_result(response)
|
|
201
188
|
stdout = response[:stdout] || ""
|