e2b 0.3.3 → 0.3.5
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 +104 -20
- data/lib/e2b/api/http_client.rb +5 -1
- data/lib/e2b/client.rb +3 -5
- data/lib/e2b/configuration.rb +6 -6
- data/lib/e2b/models/process_result.rb +20 -22
- data/lib/e2b/models/template_log_entry.rb +1 -1
- data/lib/e2b/paginator.rb +1 -3
- data/lib/e2b/sandbox.rb +5 -10
- data/lib/e2b/sandbox_helpers.rb +5 -7
- data/lib/e2b/services/base_service.rb +80 -68
- data/lib/e2b/services/command_handle.rb +16 -40
- data/lib/e2b/services/commands.rb +37 -30
- data/lib/e2b/services/envd_base64.rb +22 -0
- data/lib/e2b/services/filesystem.rb +29 -32
- data/lib/e2b/services/git.rb +8 -8
- data/lib/e2b/services/live_streamable.rb +25 -25
- data/lib/e2b/services/pty.rb +17 -18
- data/lib/e2b/template.rb +32 -29
- data/lib/e2b/version.rb +1 -1
- metadata +40 -10
|
@@ -4,6 +4,7 @@ require "base64"
|
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "openssl"
|
|
6
6
|
require "rubygems/version"
|
|
7
|
+
require_relative "envd_base64"
|
|
7
8
|
|
|
8
9
|
module E2B
|
|
9
10
|
module Services
|
|
@@ -14,7 +15,7 @@ module E2B
|
|
|
14
15
|
# Connect RPC protocol (gRPC-over-HTTP with JSON encoding).
|
|
15
16
|
class BaseService
|
|
16
17
|
# Default envd port
|
|
17
|
-
ENVD_PORT =
|
|
18
|
+
ENVD_PORT = 49_983
|
|
18
19
|
DEFAULT_USERNAME = "user"
|
|
19
20
|
ENVD_DEFAULT_USER_VERSION = Gem::Version.new("0.4.0")
|
|
20
21
|
ENVD_RECURSIVE_WATCH_VERSION = Gem::Version.new("0.1.4")
|
|
@@ -149,7 +150,7 @@ module E2B
|
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
152
|
-
def initialize(base_url:, api_key:, access_token: nil,
|
|
153
|
+
def initialize(base_url:, api_key:, sandbox_id:, access_token: nil, logger: nil)
|
|
153
154
|
@base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
|
|
154
155
|
@api_key = api_key
|
|
155
156
|
@access_token = access_token
|
|
@@ -202,16 +203,14 @@ module E2B
|
|
|
202
203
|
|
|
203
204
|
log_debug("RPC #{service}/#{method}")
|
|
204
205
|
|
|
205
|
-
if on_event
|
|
206
|
-
return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
207
|
-
end
|
|
206
|
+
return handle_streaming_rpc(path, envelope, timeout, on_event, headers) if on_event
|
|
208
207
|
|
|
209
208
|
# Unary RPCs: try Connect protocol first, fall back to plain JSON.
|
|
210
209
|
# Some envd versions (e.g., 0.5.4 on self-hosted) reject
|
|
211
210
|
# application/connect+json for unary calls but accept application/json.
|
|
212
211
|
handle_rpc_response(service, method) do
|
|
213
212
|
with_retry("RPC #{service}/#{method}") do
|
|
214
|
-
url = URI.parse("#{@base_url.chomp(
|
|
213
|
+
url = URI.parse("#{@base_url.chomp("/")}#{path}")
|
|
215
214
|
http = build_http(url, timeout)
|
|
216
215
|
|
|
217
216
|
request = Net::HTTP::Post.new(url.request_uri)
|
|
@@ -250,17 +249,35 @@ module E2B
|
|
|
250
249
|
# inherit proxy configuration and SSL settings. The streaming is handled
|
|
251
250
|
# via Faraday's on_data callback for chunked response processing.
|
|
252
251
|
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
252
|
+
# Retry policy:
|
|
253
|
+
#
|
|
254
|
+
# * `process.Process/Start` is **never retried**. The POST may have
|
|
255
|
+
# already reached envd and triggered a process spawn before the
|
|
256
|
+
# transport error surfaced; retrying would race a second process
|
|
257
|
+
# against the first. We've seen `git clone` fail with "destination
|
|
258
|
+
# already exists" exactly this way — first attempt spawns a process
|
|
259
|
+
# that mkdir's the target, transport blips, retry spawns process #2
|
|
260
|
+
# that finds the target non-empty. Caller decides what to do.
|
|
261
|
+
#
|
|
262
|
+
# * Other streaming paths (`Connect` re-attach, etc.) retry while no
|
|
263
|
+
# events have been emitted yet. Once any byte has been delivered
|
|
264
|
+
# via on_event, a retry would replay output to the caller, so we
|
|
265
|
+
# abort.
|
|
257
266
|
def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
258
267
|
result = { events: [], stdout: "", stderr: "", exit_code: nil }
|
|
259
268
|
buffer = "".b
|
|
260
269
|
|
|
261
270
|
full_path = normalize_path(path)
|
|
271
|
+
non_idempotent = path.end_with?("/Start")
|
|
272
|
+
|
|
273
|
+
retry_opts =
|
|
274
|
+
if non_idempotent
|
|
275
|
+
{ max_retries: 0 }
|
|
276
|
+
else
|
|
277
|
+
{ abort_if: -> { result[:events].any? } }
|
|
278
|
+
end
|
|
262
279
|
|
|
263
|
-
with_retry("Streaming RPC #{path}",
|
|
280
|
+
with_retry("Streaming RPC #{path}", **retry_opts) do
|
|
264
281
|
ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
|
|
265
282
|
|
|
266
283
|
streaming_conn = Faraday.new(url: @base_url, ssl: { verify: ssl_verify }) do |conn|
|
|
@@ -286,10 +303,11 @@ module E2B
|
|
|
286
303
|
req.body = envelope
|
|
287
304
|
req.options.on_data = proc do |chunk, _overall_size, _env|
|
|
288
305
|
next if chunk.nil? || chunk.empty?
|
|
306
|
+
|
|
289
307
|
buffer << chunk
|
|
290
308
|
|
|
291
309
|
while buffer.bytesize >= 5
|
|
292
|
-
|
|
310
|
+
buffer.getbyte(0)
|
|
293
311
|
length = buffer.byteslice(1, 4).unpack1("N")
|
|
294
312
|
|
|
295
313
|
break if length.nil? || buffer.bytesize < 5 + length
|
|
@@ -315,29 +333,28 @@ module E2B
|
|
|
315
333
|
|
|
316
334
|
data_event = event["Data"] || event["data"]
|
|
317
335
|
if data_event
|
|
318
|
-
stdout_data =
|
|
319
|
-
stderr_data =
|
|
336
|
+
stdout_data = EnvdBase64.decode_process_output(data_event["stdout"]) if data_event["stdout"]
|
|
337
|
+
stderr_data = EnvdBase64.decode_process_output(data_event["stderr"]) if data_event["stderr"]
|
|
320
338
|
result[:stdout] += stdout_data if stdout_data
|
|
321
339
|
result[:stderr] += stderr_data if stderr_data
|
|
322
340
|
end
|
|
323
341
|
|
|
324
342
|
end_event = event["End"] || event["end"]
|
|
325
343
|
if end_event
|
|
326
|
-
result[:exit_code] =
|
|
344
|
+
result[:exit_code] =
|
|
345
|
+
parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
|
|
327
346
|
end
|
|
328
347
|
end
|
|
329
348
|
|
|
330
349
|
if msg["stdout"]
|
|
331
|
-
stdout_data =
|
|
350
|
+
stdout_data = EnvdBase64.decode_process_output(msg["stdout"])
|
|
332
351
|
result[:stdout] += stdout_data
|
|
333
352
|
end
|
|
334
353
|
if msg["stderr"]
|
|
335
|
-
stderr_data =
|
|
354
|
+
stderr_data = EnvdBase64.decode_process_output(msg["stderr"])
|
|
336
355
|
result[:stderr] += stderr_data
|
|
337
356
|
end
|
|
338
|
-
if msg["exitCode"] || msg["exit_code"]
|
|
339
|
-
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
|
|
340
|
-
end
|
|
357
|
+
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"]) if msg["exitCode"] || msg["exit_code"]
|
|
341
358
|
|
|
342
359
|
on_event.call(
|
|
343
360
|
stdout: stdout_data,
|
|
@@ -352,9 +369,7 @@ module E2B
|
|
|
352
369
|
end
|
|
353
370
|
end
|
|
354
371
|
|
|
355
|
-
unless response.status.between?(200, 299)
|
|
356
|
-
handle_error(response)
|
|
357
|
-
end
|
|
372
|
+
handle_error(response) unless response.status.between?(200, 299)
|
|
358
373
|
end
|
|
359
374
|
|
|
360
375
|
result
|
|
@@ -409,13 +424,20 @@ module E2B
|
|
|
409
424
|
end
|
|
410
425
|
|
|
411
426
|
def resolve_proxy(url)
|
|
412
|
-
no_proxy = ENV["no_proxy"] || ENV
|
|
427
|
+
no_proxy = ENV["no_proxy"] || ENV.fetch("NO_PROXY", nil)
|
|
413
428
|
if no_proxy
|
|
414
429
|
no_proxy_hosts = no_proxy.split(",").map(&:strip)
|
|
415
430
|
return nil if no_proxy_hosts.any? { |h| url.host.end_with?(h) || h == "*" }
|
|
416
431
|
end
|
|
417
432
|
|
|
418
|
-
proxy_env = url.scheme == "https"
|
|
433
|
+
proxy_env = if url.scheme == "https"
|
|
434
|
+
ENV["https_proxy"] || ENV.fetch("HTTPS_PROXY",
|
|
435
|
+
nil)
|
|
436
|
+
else
|
|
437
|
+
ENV["http_proxy"] || ENV.fetch(
|
|
438
|
+
"HTTP_PROXY", nil
|
|
439
|
+
)
|
|
440
|
+
end
|
|
419
441
|
return nil unless proxy_env
|
|
420
442
|
|
|
421
443
|
URI.parse(proxy_env)
|
|
@@ -436,14 +458,12 @@ module E2B
|
|
|
436
458
|
|
|
437
459
|
retry_count += 1
|
|
438
460
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}"
|
|
446
|
-
end
|
|
461
|
+
raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}" unless retry_count <= max_retries
|
|
462
|
+
|
|
463
|
+
sleep_time = 2**retry_count
|
|
464
|
+
log_debug("#{operation}: retry #{retry_count}/#{max_retries} after #{e.class}: #{e.message}")
|
|
465
|
+
sleep(sleep_time)
|
|
466
|
+
retry
|
|
447
467
|
end
|
|
448
468
|
end
|
|
449
469
|
|
|
@@ -454,7 +474,11 @@ module E2B
|
|
|
454
474
|
body = response.body
|
|
455
475
|
|
|
456
476
|
if body.is_a?(String) && !body.empty?
|
|
457
|
-
content_type =
|
|
477
|
+
content_type = begin
|
|
478
|
+
response.headers["content-type"]
|
|
479
|
+
rescue StandardError
|
|
480
|
+
"unknown"
|
|
481
|
+
end
|
|
458
482
|
if content_type&.include?("json") || body.start_with?("{", "[")
|
|
459
483
|
begin
|
|
460
484
|
return JSON.parse(body)
|
|
@@ -471,7 +495,7 @@ module E2B
|
|
|
471
495
|
raise E2B::E2BError, "Connection to sandbox failed: #{e.message}"
|
|
472
496
|
end
|
|
473
497
|
|
|
474
|
-
def handle_rpc_response(
|
|
498
|
+
def handle_rpc_response(_service, _method)
|
|
475
499
|
response = yield
|
|
476
500
|
|
|
477
501
|
handle_error(response) unless response.success?
|
|
@@ -484,36 +508,32 @@ module E2B
|
|
|
484
508
|
messages = parse_connect_stream(body)
|
|
485
509
|
|
|
486
510
|
messages.each do |msg_str|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
msg = msg["result"] if msg["result"]
|
|
511
|
+
msg = JSON.parse(msg_str)
|
|
512
|
+
msg = msg["result"] if msg["result"]
|
|
490
513
|
|
|
491
|
-
|
|
514
|
+
result[:events] << msg
|
|
492
515
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
data_event = event["Data"] || event["data"]
|
|
497
|
-
if data_event
|
|
498
|
-
result[:stdout] += decode_base64(data_event["stdout"]) if data_event["stdout"]
|
|
499
|
-
result[:stderr] += decode_base64(data_event["stderr"]) if data_event["stderr"]
|
|
500
|
-
end
|
|
516
|
+
if msg["event"]
|
|
517
|
+
event = msg["event"]
|
|
501
518
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
end
|
|
519
|
+
data_event = event["Data"] || event["data"]
|
|
520
|
+
if data_event
|
|
521
|
+
result[:stdout] += EnvdBase64.decode_process_output(data_event["stdout"]) if data_event["stdout"]
|
|
522
|
+
result[:stderr] += EnvdBase64.decode_process_output(data_event["stderr"]) if data_event["stderr"]
|
|
507
523
|
end
|
|
508
524
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
result[:exit_code] = parse_exit_code(
|
|
525
|
+
end_event = event["End"] || event["end"]
|
|
526
|
+
if end_event
|
|
527
|
+
exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
|
|
528
|
+
result[:exit_code] = parse_exit_code(exit_value)
|
|
513
529
|
end
|
|
514
|
-
rescue JSON::ParserError
|
|
515
|
-
# Skip unparseable messages
|
|
516
530
|
end
|
|
531
|
+
|
|
532
|
+
result[:stdout] += EnvdBase64.decode_process_output(msg["stdout"]) if msg["stdout"]
|
|
533
|
+
result[:stderr] += EnvdBase64.decode_process_output(msg["stderr"]) if msg["stderr"]
|
|
534
|
+
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"]) if msg["exitCode"] || msg["exit_code"]
|
|
535
|
+
rescue JSON::ParserError
|
|
536
|
+
# Skip unparseable messages
|
|
517
537
|
end
|
|
518
538
|
|
|
519
539
|
result
|
|
@@ -568,22 +588,14 @@ module E2B
|
|
|
568
588
|
|
|
569
589
|
str = value.to_s
|
|
570
590
|
if str =~ /exit status (\d+)/i
|
|
571
|
-
|
|
591
|
+
::Regexp.last_match(1).to_i
|
|
572
592
|
elsif str =~ /^(\d+)$/
|
|
573
|
-
|
|
593
|
+
::Regexp.last_match(1).to_i
|
|
574
594
|
else
|
|
575
595
|
1
|
|
576
596
|
end
|
|
577
597
|
end
|
|
578
598
|
|
|
579
|
-
def decode_base64(data)
|
|
580
|
-
return "" if data.nil? || data.empty?
|
|
581
|
-
|
|
582
|
-
Base64.decode64(data)
|
|
583
|
-
rescue StandardError
|
|
584
|
-
data.to_s
|
|
585
|
-
end
|
|
586
|
-
|
|
587
599
|
def handle_error(response)
|
|
588
600
|
message = extract_error_message(response)
|
|
589
601
|
status = response.status
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "envd_base64"
|
|
4
4
|
|
|
5
5
|
module E2B
|
|
6
6
|
module Services
|
|
@@ -189,9 +189,7 @@ module E2B
|
|
|
189
189
|
# @raise [CommandExitError] if exit code is non-zero
|
|
190
190
|
def wait(on_stdout: nil, on_stderr: nil, on_pty: nil)
|
|
191
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
|
|
192
|
+
raise E2BError, "Command ended without an end event" unless @disconnected || completed?
|
|
195
193
|
|
|
196
194
|
build_result.tap do |cmd_result|
|
|
197
195
|
unless cmd_result.success?
|
|
@@ -269,15 +267,9 @@ module E2B
|
|
|
269
267
|
return if @finished
|
|
270
268
|
|
|
271
269
|
each do |stdout_chunk, stderr_chunk, pty_chunk|
|
|
272
|
-
if stdout_chunk
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if stderr_chunk
|
|
276
|
-
on_stderr&.call(stderr_chunk)
|
|
277
|
-
end
|
|
278
|
-
if pty_chunk
|
|
279
|
-
on_pty&.call(pty_chunk)
|
|
280
|
-
end
|
|
270
|
+
on_stdout&.call(stdout_chunk) if stdout_chunk
|
|
271
|
+
on_stderr&.call(stderr_chunk) if stderr_chunk
|
|
272
|
+
on_pty&.call(pty_chunk) if pty_chunk
|
|
281
273
|
end
|
|
282
274
|
|
|
283
275
|
@finished = true
|
|
@@ -306,7 +298,7 @@ module E2B
|
|
|
306
298
|
#
|
|
307
299
|
# @yield [stdout, stderr, pty]
|
|
308
300
|
# @return [void]
|
|
309
|
-
def iterate_materialized_events
|
|
301
|
+
def iterate_materialized_events(&block)
|
|
310
302
|
events = result_value(:events) || []
|
|
311
303
|
while @materialized_event_index < events.length
|
|
312
304
|
break if @disconnected
|
|
@@ -314,9 +306,7 @@ module E2B
|
|
|
314
306
|
event_hash = events[@materialized_event_index]
|
|
315
307
|
@materialized_event_index += 1
|
|
316
308
|
|
|
317
|
-
process_message(event_hash
|
|
318
|
-
yield stdout_chunk, stderr_chunk, pty_chunk
|
|
319
|
-
end
|
|
309
|
+
process_message(event_hash, &block)
|
|
320
310
|
end
|
|
321
311
|
end
|
|
322
312
|
|
|
@@ -327,14 +317,12 @@ module E2B
|
|
|
327
317
|
#
|
|
328
318
|
# @yield [stdout, stderr, pty]
|
|
329
319
|
# @return [void]
|
|
330
|
-
def iterate_streaming_events
|
|
320
|
+
def iterate_streaming_events(&block)
|
|
331
321
|
catch(:stop_iteration) do
|
|
332
322
|
@events_proc.call do |event_hash|
|
|
333
323
|
throw :stop_iteration if @disconnected
|
|
334
324
|
|
|
335
|
-
process_message(event_hash
|
|
336
|
-
yield stdout_chunk, stderr_chunk, pty_chunk
|
|
337
|
-
end
|
|
325
|
+
process_message(event_hash, &block)
|
|
338
326
|
end
|
|
339
327
|
end
|
|
340
328
|
end
|
|
@@ -353,15 +341,15 @@ module E2B
|
|
|
353
341
|
# @param message [Hash] A raw stream message
|
|
354
342
|
# @yield [stdout, stderr, pty]
|
|
355
343
|
# @return [void]
|
|
356
|
-
def process_message(message)
|
|
344
|
+
def process_message(message, &block)
|
|
357
345
|
return unless message.is_a?(Hash)
|
|
358
346
|
|
|
359
347
|
event = message["event"]
|
|
360
|
-
process_event(event
|
|
348
|
+
process_event(event, &block) if event.is_a?(Hash)
|
|
361
349
|
|
|
362
350
|
if event.nil?
|
|
363
|
-
stdout_chunk =
|
|
364
|
-
stderr_chunk =
|
|
351
|
+
stdout_chunk = EnvdBase64.decode_process_output(message["stdout"])
|
|
352
|
+
stderr_chunk = EnvdBase64.decode_process_output(message["stderr"])
|
|
365
353
|
|
|
366
354
|
if stdout_chunk && !stdout_chunk.empty?
|
|
367
355
|
append_stdout(stdout_chunk)
|
|
@@ -387,9 +375,9 @@ module E2B
|
|
|
387
375
|
# Handle Data event
|
|
388
376
|
data_event = event["Data"] || event["data"]
|
|
389
377
|
if data_event
|
|
390
|
-
stdout_chunk =
|
|
391
|
-
stderr_chunk =
|
|
392
|
-
pty_chunk =
|
|
378
|
+
stdout_chunk = EnvdBase64.decode_process_output(data_event["stdout"])
|
|
379
|
+
stderr_chunk = EnvdBase64.decode_process_output(data_event["stderr"])
|
|
380
|
+
pty_chunk = EnvdBase64.decode_process_output(data_event["pty"])
|
|
393
381
|
|
|
394
382
|
if stdout_chunk && !stdout_chunk.empty?
|
|
395
383
|
append_stdout(stdout_chunk)
|
|
@@ -467,18 +455,6 @@ module E2B
|
|
|
467
455
|
nil
|
|
468
456
|
end
|
|
469
457
|
|
|
470
|
-
# Decode a base64-encoded string.
|
|
471
|
-
#
|
|
472
|
-
# @param data [String, nil] Base64-encoded data
|
|
473
|
-
# @return [String, nil] Decoded string, or nil if input is nil/empty
|
|
474
|
-
def decode_base64(data)
|
|
475
|
-
return nil if data.nil? || data.empty?
|
|
476
|
-
|
|
477
|
-
Base64.decode64(data).force_encoding("UTF-8")
|
|
478
|
-
rescue StandardError
|
|
479
|
-
data.to_s
|
|
480
|
-
end
|
|
481
|
-
|
|
482
458
|
# Parse an exit code from various envd response formats.
|
|
483
459
|
#
|
|
484
460
|
# Handles integer values, string integers, and "exit status N" strings.
|
|
@@ -25,6 +25,7 @@ module E2B
|
|
|
25
25
|
# handle.kill
|
|
26
26
|
class Commands < BaseService
|
|
27
27
|
include LiveStreamable
|
|
28
|
+
|
|
28
29
|
# Run a command in the sandbox
|
|
29
30
|
#
|
|
30
31
|
# @param cmd [String] Command to execute (run via /bin/bash -l -c)
|
|
@@ -36,11 +37,15 @@ module E2B
|
|
|
36
37
|
# @param on_stderr [Proc, nil] Callback for stderr data
|
|
37
38
|
# @param timeout [Integer] Command timeout in seconds (default: 60)
|
|
38
39
|
# @param request_timeout [Integer, nil] HTTP request timeout in seconds
|
|
40
|
+
# @param stdin [Boolean] Allocate a stdin pipe for the process. Required
|
|
41
|
+
# when the caller plans to use {#send_stdin} or {CommandHandle#send_stdin}
|
|
42
|
+
# on a background handle. Defaults to +false+ to mirror TS/Python SDKs.
|
|
39
43
|
# @return [CommandResult, CommandHandle] Result or handle for background commands
|
|
40
44
|
#
|
|
41
45
|
# @raise [CommandExitError] If exit code is non-zero (foreground only)
|
|
42
46
|
def run(cmd, background: false, envs: nil, user: nil, cwd: nil,
|
|
43
|
-
on_stdout: nil, on_stderr: nil, timeout: 60, request_timeout: nil,
|
|
47
|
+
on_stdout: nil, on_stderr: nil, timeout: 60, request_timeout: nil,
|
|
48
|
+
stdin: false, &block)
|
|
44
49
|
# Build the process spec - official SDK always uses /bin/bash -l -c
|
|
45
50
|
process_spec = {
|
|
46
51
|
cmd: "/bin/bash",
|
|
@@ -55,7 +60,7 @@ module E2B
|
|
|
55
60
|
|
|
56
61
|
process_spec[:cwd] = cwd if cwd
|
|
57
62
|
|
|
58
|
-
body = { process: process_spec }
|
|
63
|
+
body = { process: process_spec, stdin: stdin }
|
|
59
64
|
headers = user_auth_headers(user)
|
|
60
65
|
|
|
61
66
|
# Set up streaming callback
|
|
@@ -89,10 +94,10 @@ module E2B
|
|
|
89
94
|
end
|
|
90
95
|
|
|
91
96
|
response = envd_rpc("process.Process", "Start",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
body: body,
|
|
98
|
+
timeout: effective_timeout,
|
|
99
|
+
headers: headers,
|
|
100
|
+
on_event: streaming_callback)
|
|
96
101
|
|
|
97
102
|
# Return CommandResult for foreground processes
|
|
98
103
|
result = build_result(response)
|
|
@@ -116,16 +121,15 @@ module E2B
|
|
|
116
121
|
# @return [Array<Hash>] List of running processes with pid, config, tag
|
|
117
122
|
def list(request_timeout: nil)
|
|
118
123
|
response = envd_rpc("process.Process", "List",
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
body: {},
|
|
125
|
+
timeout: request_timeout || 30)
|
|
121
126
|
|
|
122
127
|
processes = []
|
|
123
128
|
events = response[:events] || []
|
|
124
129
|
events.each do |event|
|
|
125
130
|
next unless event.is_a?(Hash)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
end
|
|
131
|
+
|
|
132
|
+
processes.concat(Array(event["processes"])) if event["processes"]
|
|
129
133
|
end
|
|
130
134
|
processes
|
|
131
135
|
end
|
|
@@ -137,12 +141,12 @@ module E2B
|
|
|
137
141
|
# @return [Boolean] true if killed, false if not found
|
|
138
142
|
def kill(pid, request_timeout: nil, headers: nil)
|
|
139
143
|
envd_rpc("process.Process", "SendSignal",
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
body: {
|
|
145
|
+
process: { pid: pid },
|
|
146
|
+
signal: 9 # SIGKILL
|
|
147
|
+
},
|
|
148
|
+
headers: headers,
|
|
149
|
+
timeout: request_timeout || 30)
|
|
146
150
|
true
|
|
147
151
|
rescue E2B::NotFoundError
|
|
148
152
|
false
|
|
@@ -150,7 +154,11 @@ module E2B
|
|
|
150
154
|
false
|
|
151
155
|
end
|
|
152
156
|
|
|
153
|
-
# Send stdin data to a running process
|
|
157
|
+
# Send stdin data to a running process.
|
|
158
|
+
#
|
|
159
|
+
# The target process must have been started with +stdin: true+ (see {#run})
|
|
160
|
+
# — otherwise envd silently drops the input and the call is a no-op on the
|
|
161
|
+
# process side.
|
|
154
162
|
#
|
|
155
163
|
# @param pid [Integer] Process ID
|
|
156
164
|
# @param data [String] Data to send to stdin
|
|
@@ -158,12 +166,12 @@ module E2B
|
|
|
158
166
|
def send_stdin(pid, data, request_timeout: nil, headers: nil)
|
|
159
167
|
encoded = Base64.strict_encode64(data.to_s)
|
|
160
168
|
envd_rpc("process.Process", "SendInput",
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
body: {
|
|
170
|
+
process: { pid: pid },
|
|
171
|
+
input: { stdin: encoded }
|
|
172
|
+
},
|
|
173
|
+
headers: headers,
|
|
174
|
+
timeout: request_timeout || 30)
|
|
167
175
|
end
|
|
168
176
|
|
|
169
177
|
# Close the stdin of a running process.
|
|
@@ -177,9 +185,9 @@ module E2B
|
|
|
177
185
|
# @raise [E2B::E2BError] if the process is not found
|
|
178
186
|
def close_stdin(pid, request_timeout: nil, headers: nil)
|
|
179
187
|
envd_rpc("process.Process", "CloseStdin",
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
body: { process: { pid: pid } },
|
|
189
|
+
headers: headers,
|
|
190
|
+
timeout: request_timeout || 30)
|
|
183
191
|
end
|
|
184
192
|
|
|
185
193
|
# Connect to a running process
|
|
@@ -214,10 +222,9 @@ module E2B
|
|
|
214
222
|
events = response[:events] || []
|
|
215
223
|
events.each do |event|
|
|
216
224
|
next unless event.is_a?(Hash) && event["event"]
|
|
225
|
+
|
|
217
226
|
end_event = event["event"]["End"] || event["event"]["end"]
|
|
218
|
-
if end_event
|
|
219
|
-
error = end_event["error"] if end_event["error"] && !end_event["error"].empty?
|
|
220
|
-
end
|
|
227
|
+
error = end_event["error"] if end_event && end_event["error"] && !end_event["error"].empty?
|
|
221
228
|
end
|
|
222
229
|
|
|
223
230
|
CommandResult.new(
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module E2B
|
|
6
|
+
module Services
|
|
7
|
+
# Base64 payloads from envd wrap raw subprocess / PTY bytes.
|
|
8
|
+
module EnvdBase64
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# @param data [String, nil] base64-encoded chunk from envd
|
|
12
|
+
# @return [String] UTF-8 string with invalid byte sequences scrubbed; "" if +data+ is nil or empty
|
|
13
|
+
def decode_process_output(data)
|
|
14
|
+
return "" if data.nil? || data.empty?
|
|
15
|
+
|
|
16
|
+
Base64.decode64(data).force_encoding(Encoding::UTF_8).scrub
|
|
17
|
+
rescue StandardError
|
|
18
|
+
data.to_s
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|