e2b 0.3.4 → 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 +17 -17
- 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 +75 -56
- data/lib/e2b/services/command_handle.rb +10 -22
- data/lib/e2b/services/commands.rb +26 -27
- 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 +15 -17
- data/lib/e2b/template.rb +32 -29
- data/lib/e2b/version.rb +1 -1
- metadata +39 -10
|
@@ -15,7 +15,7 @@ module E2B
|
|
|
15
15
|
# Connect RPC protocol (gRPC-over-HTTP with JSON encoding).
|
|
16
16
|
class BaseService
|
|
17
17
|
# Default envd port
|
|
18
|
-
ENVD_PORT =
|
|
18
|
+
ENVD_PORT = 49_983
|
|
19
19
|
DEFAULT_USERNAME = "user"
|
|
20
20
|
ENVD_DEFAULT_USER_VERSION = Gem::Version.new("0.4.0")
|
|
21
21
|
ENVD_RECURSIVE_WATCH_VERSION = Gem::Version.new("0.1.4")
|
|
@@ -150,7 +150,7 @@ module E2B
|
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
def initialize(base_url:, api_key:, access_token: nil,
|
|
153
|
+
def initialize(base_url:, api_key:, sandbox_id:, access_token: nil, logger: nil)
|
|
154
154
|
@base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
|
|
155
155
|
@api_key = api_key
|
|
156
156
|
@access_token = access_token
|
|
@@ -203,16 +203,14 @@ module E2B
|
|
|
203
203
|
|
|
204
204
|
log_debug("RPC #{service}/#{method}")
|
|
205
205
|
|
|
206
|
-
if on_event
|
|
207
|
-
return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
208
|
-
end
|
|
206
|
+
return handle_streaming_rpc(path, envelope, timeout, on_event, headers) if on_event
|
|
209
207
|
|
|
210
208
|
# Unary RPCs: try Connect protocol first, fall back to plain JSON.
|
|
211
209
|
# Some envd versions (e.g., 0.5.4 on self-hosted) reject
|
|
212
210
|
# application/connect+json for unary calls but accept application/json.
|
|
213
211
|
handle_rpc_response(service, method) do
|
|
214
212
|
with_retry("RPC #{service}/#{method}") do
|
|
215
|
-
url = URI.parse("#{@base_url.chomp(
|
|
213
|
+
url = URI.parse("#{@base_url.chomp("/")}#{path}")
|
|
216
214
|
http = build_http(url, timeout)
|
|
217
215
|
|
|
218
216
|
request = Net::HTTP::Post.new(url.request_uri)
|
|
@@ -251,17 +249,35 @@ module E2B
|
|
|
251
249
|
# inherit proxy configuration and SSL settings. The streaming is handled
|
|
252
250
|
# via Faraday's on_data callback for chunked response processing.
|
|
253
251
|
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
#
|
|
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.
|
|
258
266
|
def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
259
267
|
result = { events: [], stdout: "", stderr: "", exit_code: nil }
|
|
260
268
|
buffer = "".b
|
|
261
269
|
|
|
262
270
|
full_path = normalize_path(path)
|
|
271
|
+
non_idempotent = path.end_with?("/Start")
|
|
263
272
|
|
|
264
|
-
|
|
273
|
+
retry_opts =
|
|
274
|
+
if non_idempotent
|
|
275
|
+
{ max_retries: 0 }
|
|
276
|
+
else
|
|
277
|
+
{ abort_if: -> { result[:events].any? } }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
with_retry("Streaming RPC #{path}", **retry_opts) do
|
|
265
281
|
ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
|
|
266
282
|
|
|
267
283
|
streaming_conn = Faraday.new(url: @base_url, ssl: { verify: ssl_verify }) do |conn|
|
|
@@ -287,10 +303,11 @@ module E2B
|
|
|
287
303
|
req.body = envelope
|
|
288
304
|
req.options.on_data = proc do |chunk, _overall_size, _env|
|
|
289
305
|
next if chunk.nil? || chunk.empty?
|
|
306
|
+
|
|
290
307
|
buffer << chunk
|
|
291
308
|
|
|
292
309
|
while buffer.bytesize >= 5
|
|
293
|
-
|
|
310
|
+
buffer.getbyte(0)
|
|
294
311
|
length = buffer.byteslice(1, 4).unpack1("N")
|
|
295
312
|
|
|
296
313
|
break if length.nil? || buffer.bytesize < 5 + length
|
|
@@ -324,7 +341,8 @@ module E2B
|
|
|
324
341
|
|
|
325
342
|
end_event = event["End"] || event["end"]
|
|
326
343
|
if end_event
|
|
327
|
-
result[:exit_code] =
|
|
344
|
+
result[:exit_code] =
|
|
345
|
+
parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
|
|
328
346
|
end
|
|
329
347
|
end
|
|
330
348
|
|
|
@@ -336,9 +354,7 @@ module E2B
|
|
|
336
354
|
stderr_data = EnvdBase64.decode_process_output(msg["stderr"])
|
|
337
355
|
result[:stderr] += stderr_data
|
|
338
356
|
end
|
|
339
|
-
if msg["exitCode"] || msg["exit_code"]
|
|
340
|
-
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
|
|
341
|
-
end
|
|
357
|
+
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"]) if msg["exitCode"] || msg["exit_code"]
|
|
342
358
|
|
|
343
359
|
on_event.call(
|
|
344
360
|
stdout: stdout_data,
|
|
@@ -353,9 +369,7 @@ module E2B
|
|
|
353
369
|
end
|
|
354
370
|
end
|
|
355
371
|
|
|
356
|
-
unless response.status.between?(200, 299)
|
|
357
|
-
handle_error(response)
|
|
358
|
-
end
|
|
372
|
+
handle_error(response) unless response.status.between?(200, 299)
|
|
359
373
|
end
|
|
360
374
|
|
|
361
375
|
result
|
|
@@ -410,13 +424,20 @@ module E2B
|
|
|
410
424
|
end
|
|
411
425
|
|
|
412
426
|
def resolve_proxy(url)
|
|
413
|
-
no_proxy = ENV["no_proxy"] || ENV
|
|
427
|
+
no_proxy = ENV["no_proxy"] || ENV.fetch("NO_PROXY", nil)
|
|
414
428
|
if no_proxy
|
|
415
429
|
no_proxy_hosts = no_proxy.split(",").map(&:strip)
|
|
416
430
|
return nil if no_proxy_hosts.any? { |h| url.host.end_with?(h) || h == "*" }
|
|
417
431
|
end
|
|
418
432
|
|
|
419
|
-
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
|
|
420
441
|
return nil unless proxy_env
|
|
421
442
|
|
|
422
443
|
URI.parse(proxy_env)
|
|
@@ -437,14 +458,12 @@ module E2B
|
|
|
437
458
|
|
|
438
459
|
retry_count += 1
|
|
439
460
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}"
|
|
447
|
-
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
|
|
448
467
|
end
|
|
449
468
|
end
|
|
450
469
|
|
|
@@ -455,7 +474,11 @@ module E2B
|
|
|
455
474
|
body = response.body
|
|
456
475
|
|
|
457
476
|
if body.is_a?(String) && !body.empty?
|
|
458
|
-
content_type =
|
|
477
|
+
content_type = begin
|
|
478
|
+
response.headers["content-type"]
|
|
479
|
+
rescue StandardError
|
|
480
|
+
"unknown"
|
|
481
|
+
end
|
|
459
482
|
if content_type&.include?("json") || body.start_with?("{", "[")
|
|
460
483
|
begin
|
|
461
484
|
return JSON.parse(body)
|
|
@@ -472,7 +495,7 @@ module E2B
|
|
|
472
495
|
raise E2B::E2BError, "Connection to sandbox failed: #{e.message}"
|
|
473
496
|
end
|
|
474
497
|
|
|
475
|
-
def handle_rpc_response(
|
|
498
|
+
def handle_rpc_response(_service, _method)
|
|
476
499
|
response = yield
|
|
477
500
|
|
|
478
501
|
handle_error(response) unless response.success?
|
|
@@ -485,36 +508,32 @@ module E2B
|
|
|
485
508
|
messages = parse_connect_stream(body)
|
|
486
509
|
|
|
487
510
|
messages.each do |msg_str|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
msg = msg["result"] if msg["result"]
|
|
511
|
+
msg = JSON.parse(msg_str)
|
|
512
|
+
msg = msg["result"] if msg["result"]
|
|
491
513
|
|
|
492
|
-
|
|
514
|
+
result[:events] << msg
|
|
493
515
|
|
|
494
|
-
|
|
495
|
-
|
|
516
|
+
if msg["event"]
|
|
517
|
+
event = msg["event"]
|
|
496
518
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
end
|
|
502
|
-
|
|
503
|
-
end_event = event["End"] || event["end"]
|
|
504
|
-
if end_event
|
|
505
|
-
exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
|
|
506
|
-
result[:exit_code] = parse_exit_code(exit_value)
|
|
507
|
-
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"]
|
|
508
523
|
end
|
|
509
524
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
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)
|
|
514
529
|
end
|
|
515
|
-
rescue JSON::ParserError
|
|
516
|
-
# Skip unparseable messages
|
|
517
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
|
|
518
537
|
end
|
|
519
538
|
|
|
520
539
|
result
|
|
@@ -569,9 +588,9 @@ module E2B
|
|
|
569
588
|
|
|
570
589
|
str = value.to_s
|
|
571
590
|
if str =~ /exit status (\d+)/i
|
|
572
|
-
|
|
591
|
+
::Regexp.last_match(1).to_i
|
|
573
592
|
elsif str =~ /^(\d+)$/
|
|
574
|
-
|
|
593
|
+
::Regexp.last_match(1).to_i
|
|
575
594
|
else
|
|
576
595
|
1
|
|
577
596
|
end
|
|
@@ -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,11 +341,11 @@ 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
351
|
stdout_chunk = EnvdBase64.decode_process_output(message["stdout"])
|
|
@@ -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)
|
|
@@ -93,10 +94,10 @@ module E2B
|
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
response = envd_rpc("process.Process", "Start",
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
body: body,
|
|
98
|
+
timeout: effective_timeout,
|
|
99
|
+
headers: headers,
|
|
100
|
+
on_event: streaming_callback)
|
|
100
101
|
|
|
101
102
|
# Return CommandResult for foreground processes
|
|
102
103
|
result = build_result(response)
|
|
@@ -120,16 +121,15 @@ module E2B
|
|
|
120
121
|
# @return [Array<Hash>] List of running processes with pid, config, tag
|
|
121
122
|
def list(request_timeout: nil)
|
|
122
123
|
response = envd_rpc("process.Process", "List",
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
body: {},
|
|
125
|
+
timeout: request_timeout || 30)
|
|
125
126
|
|
|
126
127
|
processes = []
|
|
127
128
|
events = response[:events] || []
|
|
128
129
|
events.each do |event|
|
|
129
130
|
next unless event.is_a?(Hash)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
end
|
|
131
|
+
|
|
132
|
+
processes.concat(Array(event["processes"])) if event["processes"]
|
|
133
133
|
end
|
|
134
134
|
processes
|
|
135
135
|
end
|
|
@@ -141,12 +141,12 @@ module E2B
|
|
|
141
141
|
# @return [Boolean] true if killed, false if not found
|
|
142
142
|
def kill(pid, request_timeout: nil, headers: nil)
|
|
143
143
|
envd_rpc("process.Process", "SendSignal",
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
body: {
|
|
145
|
+
process: { pid: pid },
|
|
146
|
+
signal: 9 # SIGKILL
|
|
147
|
+
},
|
|
148
|
+
headers: headers,
|
|
149
|
+
timeout: request_timeout || 30)
|
|
150
150
|
true
|
|
151
151
|
rescue E2B::NotFoundError
|
|
152
152
|
false
|
|
@@ -166,12 +166,12 @@ module E2B
|
|
|
166
166
|
def send_stdin(pid, data, request_timeout: nil, headers: nil)
|
|
167
167
|
encoded = Base64.strict_encode64(data.to_s)
|
|
168
168
|
envd_rpc("process.Process", "SendInput",
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
body: {
|
|
170
|
+
process: { pid: pid },
|
|
171
|
+
input: { stdin: encoded }
|
|
172
|
+
},
|
|
173
|
+
headers: headers,
|
|
174
|
+
timeout: request_timeout || 30)
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
# Close the stdin of a running process.
|
|
@@ -185,9 +185,9 @@ module E2B
|
|
|
185
185
|
# @raise [E2B::E2BError] if the process is not found
|
|
186
186
|
def close_stdin(pid, request_timeout: nil, headers: nil)
|
|
187
187
|
envd_rpc("process.Process", "CloseStdin",
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
body: { process: { pid: pid } },
|
|
189
|
+
headers: headers,
|
|
190
|
+
timeout: request_timeout || 30)
|
|
191
191
|
end
|
|
192
192
|
|
|
193
193
|
# Connect to a running process
|
|
@@ -222,10 +222,9 @@ module E2B
|
|
|
222
222
|
events = response[:events] || []
|
|
223
223
|
events.each do |event|
|
|
224
224
|
next unless event.is_a?(Hash) && event["event"]
|
|
225
|
+
|
|
225
226
|
end_event = event["event"]["End"] || event["event"]["end"]
|
|
226
|
-
if end_event
|
|
227
|
-
error = end_event["error"] if end_event["error"] && !end_event["error"].empty?
|
|
228
|
-
end
|
|
227
|
+
error = end_event["error"] if end_event && end_event["error"] && !end_event["error"].empty?
|
|
229
228
|
end
|
|
230
229
|
|
|
231
230
|
CommandResult.new(
|
|
@@ -99,9 +99,9 @@ module E2B
|
|
|
99
99
|
# entries.each { |e| puts "#{e.name} (#{e.type})" }
|
|
100
100
|
def list(path, depth: 1, user: nil, request_timeout: 60)
|
|
101
101
|
response = envd_rpc("filesystem.Filesystem", "ListDir",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
body: { path: path, depth: depth },
|
|
103
|
+
timeout: request_timeout,
|
|
104
|
+
headers: user_auth_headers(user))
|
|
105
105
|
|
|
106
106
|
entries = extract_entries(response)
|
|
107
107
|
entries.map { |e| Models::EntryInfo.from_hash(e) }
|
|
@@ -132,9 +132,9 @@ module E2B
|
|
|
132
132
|
# @return [Models::EntryInfo] File/directory info
|
|
133
133
|
def get_info(path, user: nil, request_timeout: 30)
|
|
134
134
|
response = envd_rpc("filesystem.Filesystem", "Stat",
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
body: { path: path },
|
|
136
|
+
timeout: request_timeout,
|
|
137
|
+
headers: user_auth_headers(user))
|
|
138
138
|
|
|
139
139
|
entry_data = extract_entry(response)
|
|
140
140
|
Models::EntryInfo.from_hash(entry_data)
|
|
@@ -147,9 +147,9 @@ module E2B
|
|
|
147
147
|
# @param request_timeout [Integer] Request timeout in seconds
|
|
148
148
|
def remove(path, user: nil, request_timeout: 30)
|
|
149
149
|
envd_rpc("filesystem.Filesystem", "Remove",
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
body: { path: path },
|
|
151
|
+
timeout: request_timeout,
|
|
152
|
+
headers: user_auth_headers(user))
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
# Rename/move a file or directory
|
|
@@ -161,9 +161,9 @@ module E2B
|
|
|
161
161
|
# @return [Models::EntryInfo] Info about the moved entry
|
|
162
162
|
def rename(old_path, new_path, user: nil, request_timeout: 30)
|
|
163
163
|
response = envd_rpc("filesystem.Filesystem", "Move",
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
body: { source: old_path, destination: new_path },
|
|
165
|
+
timeout: request_timeout,
|
|
166
|
+
headers: user_auth_headers(user))
|
|
167
167
|
|
|
168
168
|
entry_data = extract_entry(response)
|
|
169
169
|
Models::EntryInfo.from_hash(entry_data)
|
|
@@ -177,9 +177,9 @@ module E2B
|
|
|
177
177
|
# @return [Boolean] true if created successfully
|
|
178
178
|
def make_dir(path, user: nil, request_timeout: 30)
|
|
179
179
|
envd_rpc("filesystem.Filesystem", "MakeDir",
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
body: { path: path },
|
|
181
|
+
timeout: request_timeout,
|
|
182
|
+
headers: user_auth_headers(user))
|
|
183
183
|
true
|
|
184
184
|
end
|
|
185
185
|
|
|
@@ -202,13 +202,13 @@ module E2B
|
|
|
202
202
|
def watch_dir(path, recursive: false, user: nil, request_timeout: 30)
|
|
203
203
|
if recursive && !supports_recursive_watch?
|
|
204
204
|
raise E2B::TemplateError,
|
|
205
|
-
|
|
205
|
+
"You need to update the template to use recursive watching. You can do this by running `e2b template build` in the directory with the template."
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
response = envd_rpc("filesystem.Filesystem", "CreateWatcher",
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
body: { path: path, recursive: recursive },
|
|
210
|
+
timeout: request_timeout,
|
|
211
|
+
headers: user_auth_headers(user))
|
|
212
212
|
|
|
213
213
|
watcher_id = response[:events]&.first&.dig("watcherId") ||
|
|
214
214
|
response["watcherId"] ||
|
|
@@ -257,9 +257,8 @@ module E2B
|
|
|
257
257
|
|
|
258
258
|
response = execute_http_request(uri, request, timeout: timeout)
|
|
259
259
|
unless successful_response?(response)
|
|
260
|
-
if response.code.to_i == 404
|
|
261
|
-
|
|
262
|
-
end
|
|
260
|
+
raise E2B::NotFoundError.new("File not found", status_code: 404) if response.code.to_i == 404
|
|
261
|
+
|
|
263
262
|
raise E2B::E2BError, "File read failed: HTTP #{response.code}"
|
|
264
263
|
end
|
|
265
264
|
|
|
@@ -281,9 +280,7 @@ module E2B
|
|
|
281
280
|
apply_request_headers(request)
|
|
282
281
|
|
|
283
282
|
response = execute_http_request(uri, request, timeout: timeout)
|
|
284
|
-
unless successful_response?(response)
|
|
285
|
-
raise E2B::E2BError, "File upload failed: HTTP #{response.code}"
|
|
286
|
-
end
|
|
283
|
+
raise E2B::E2BError, "File upload failed: HTTP #{response.code}" unless successful_response?(response)
|
|
287
284
|
|
|
288
285
|
parse_upload_response(response.body)
|
|
289
286
|
end
|
|
@@ -344,13 +341,11 @@ module E2B
|
|
|
344
341
|
rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, EOFError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
345
342
|
retry_count += 1
|
|
346
343
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}"
|
|
353
|
-
end
|
|
344
|
+
raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}" unless retry_count <= max_retries
|
|
345
|
+
|
|
346
|
+
sleep_time = 2**retry_count
|
|
347
|
+
sleep(sleep_time)
|
|
348
|
+
retry
|
|
354
349
|
end
|
|
355
350
|
end
|
|
356
351
|
|
|
@@ -365,6 +360,7 @@ module E2B
|
|
|
365
360
|
|
|
366
361
|
events.each do |event|
|
|
367
362
|
next unless event.is_a?(Hash)
|
|
363
|
+
|
|
368
364
|
# Direct entries field
|
|
369
365
|
if event["entries"]
|
|
370
366
|
entries.concat(Array(event["entries"]))
|
|
@@ -400,6 +396,7 @@ module E2B
|
|
|
400
396
|
events.each do |event|
|
|
401
397
|
next unless event.is_a?(Hash)
|
|
402
398
|
return event["watcherId"] || event["watcher_id"] if event["watcherId"] || event["watcher_id"]
|
|
399
|
+
|
|
403
400
|
result = event["result"]
|
|
404
401
|
return result["watcherId"] || result["watcher_id"] if result.is_a?(Hash) && (result["watcherId"] || result["watcher_id"])
|
|
405
402
|
end
|
data/lib/e2b/services/git.rb
CHANGED
|
@@ -84,7 +84,7 @@ module E2B
|
|
|
84
84
|
#
|
|
85
85
|
# @return [Boolean]
|
|
86
86
|
def has_conflicts?
|
|
87
|
-
file_status.any? { |f|
|
|
87
|
+
file_status.any? { |f| %w[u U].include?(f.index_status) }
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# Number of staged files
|
|
@@ -105,7 +105,7 @@ module E2B
|
|
|
105
105
|
#
|
|
106
106
|
# @return [Integer]
|
|
107
107
|
def conflict_count
|
|
108
|
-
file_status.count { |f|
|
|
108
|
+
file_status.count { |f| %w[u U].include?(f.index_status) }
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
# Number of modified files (in the working tree)
|
|
@@ -571,7 +571,7 @@ module E2B
|
|
|
571
571
|
envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
572
572
|
# Configure credential helper to use the store
|
|
573
573
|
set_config("credential.helper", "store", scope: "global",
|
|
574
|
-
|
|
574
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
575
575
|
|
|
576
576
|
# Write credentials to the credential store via git credential approve
|
|
577
577
|
credential_input = [
|
|
@@ -583,7 +583,7 @@ module E2B
|
|
|
583
583
|
].join("\n")
|
|
584
584
|
|
|
585
585
|
escaped_input = Shellwords.escape(credential_input)
|
|
586
|
-
args = [
|
|
586
|
+
args = %w[credential approve]
|
|
587
587
|
cmd = build_git_command(args, nil)
|
|
588
588
|
full_cmd = "echo #{escaped_input} | #{cmd}"
|
|
589
589
|
|
|
@@ -604,9 +604,9 @@ module E2B
|
|
|
604
604
|
# @return [void]
|
|
605
605
|
def configure_user(name, email, scope: "global", path: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
|
|
606
606
|
set_config("user.name", name, scope: scope, path: path,
|
|
607
|
-
|
|
607
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
608
608
|
set_config("user.email", email, scope: scope, path: path,
|
|
609
|
-
|
|
609
|
+
envs: envs, user: user, cwd: cwd, timeout: timeout)
|
|
610
610
|
end
|
|
611
611
|
|
|
612
612
|
private
|
|
@@ -781,7 +781,7 @@ module E2B
|
|
|
781
781
|
def validate_scope!(scope)
|
|
782
782
|
return if VALID_SCOPES.include?(scope)
|
|
783
783
|
|
|
784
|
-
raise E2B::E2BError, "Invalid git config scope '#{scope}'. Must be one of: #{VALID_SCOPES.join(
|
|
784
|
+
raise E2B::E2BError, "Invalid git config scope '#{scope}'. Must be one of: #{VALID_SCOPES.join(", ")}"
|
|
785
785
|
end
|
|
786
786
|
|
|
787
787
|
# Convert a scope name to its git CLI flag
|
|
@@ -835,7 +835,7 @@ module E2B
|
|
|
835
835
|
file_status << GitFileStatus.new(path: filepath.split("\t").first, index_status: idx, work_tree_status: wt)
|
|
836
836
|
when /\Au (.)(.) .+ .+ .+ .+ .+ (.+)\z/
|
|
837
837
|
# Unmerged entry
|
|
838
|
-
|
|
838
|
+
Regexp.last_match(1)
|
|
839
839
|
wt = Regexp.last_match(2)
|
|
840
840
|
filepath = Regexp.last_match(3)
|
|
841
841
|
file_status << GitFileStatus.new(path: filepath, index_status: "u", work_tree_status: wt)
|