e2b 0.3.1 → 0.3.3
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/lib/e2b/models/entry_info.rb +1 -1
- data/lib/e2b/models/sandbox_info.rb +2 -2
- data/lib/e2b/models/template_log_entry.rb +2 -2
- data/lib/e2b/sandbox.rb +39 -13
- data/lib/e2b/services/base_service.rb +168 -76
- data/lib/e2b/services/filesystem.rb +5 -1
- data/lib/e2b/template.rb +9 -4
- data/lib/e2b/version.rb +1 -1
- metadata +2 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fa9c1779eddd80d88939182c59ed84697bbcd75af38bacc61ea2272dee24b45
|
|
4
|
+
data.tar.gz: 547824e29330fb45199e9cd4b3ccb4ebc388edb92a39ebabd32a229a4db49300
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f05cba4db6cf6273d4ae2caf5e9f47755adea8059af6617cdb87b316d7b0a29fef87c8cfc32db9ed04d657f16d3876e6c4da274a26f05026fcb4187d5cd4632c
|
|
7
|
+
data.tar.gz: 2f010ab757dec6053bf033e472f817b3fddfa5e22f2fc6e8180e2bca6eb70241f2a22dc520662e947c941d3634c76b3dc893f20caa6f58d9f36a6e8aa54fccf0
|
data/lib/e2b/sandbox.rb
CHANGED
|
@@ -177,11 +177,15 @@ module E2B
|
|
|
177
177
|
|
|
178
178
|
# List running sandboxes
|
|
179
179
|
#
|
|
180
|
+
# Returns a paginator that yields sandbox info hashes one page at a time.
|
|
181
|
+
# Use {SandboxPaginator#has_next?} and {SandboxPaginator#next_items} to
|
|
182
|
+
# iterate through pages.
|
|
183
|
+
#
|
|
180
184
|
# @param query [Hash, nil] Filter parameters (metadata, state)
|
|
181
185
|
# @param limit [Integer] Maximum results per page
|
|
182
186
|
# @param next_token [String, nil] Pagination token
|
|
183
187
|
# @param api_key [String, nil] API key
|
|
184
|
-
# @return [
|
|
188
|
+
# @return [SandboxPaginator]
|
|
185
189
|
def list(query: nil, limit: 100, next_token: nil, api_key: nil, access_token: nil, domain: nil)
|
|
186
190
|
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
187
191
|
http_client = build_http_client(**credentials, domain: resolve_domain(domain))
|
|
@@ -225,10 +229,15 @@ module E2B
|
|
|
225
229
|
false
|
|
226
230
|
end
|
|
227
231
|
|
|
228
|
-
# Kill a sandbox by ID
|
|
232
|
+
# Kill a sandbox by ID (idempotent)
|
|
233
|
+
#
|
|
234
|
+
# Returns `true` if the sandbox was killed, *and* if it was already gone
|
|
235
|
+
# (NotFound). To distinguish "actually killed" from "already gone", use
|
|
236
|
+
# {.list} or {Sandbox#running?} before calling.
|
|
229
237
|
#
|
|
230
238
|
# @param sandbox_id [String] Sandbox ID to kill
|
|
231
239
|
# @param api_key [String, nil] API key
|
|
240
|
+
# @return [Boolean] always true
|
|
232
241
|
def kill(sandbox_id, api_key: nil, access_token: nil, domain: nil)
|
|
233
242
|
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
234
243
|
http_client = build_http_client(**credentials, domain: resolve_domain(domain))
|
|
@@ -294,9 +303,16 @@ module E2B
|
|
|
294
303
|
@end_at = Time.now + timeout
|
|
295
304
|
end
|
|
296
305
|
|
|
297
|
-
# Kill/terminate the sandbox
|
|
306
|
+
# Kill/terminate the sandbox (idempotent)
|
|
307
|
+
#
|
|
308
|
+
# Returns `true` whether the sandbox was running or already gone.
|
|
309
|
+
#
|
|
310
|
+
# @return [Boolean] always true
|
|
298
311
|
def kill
|
|
299
312
|
@http_client.delete("/sandboxes/#{@sandbox_id}")
|
|
313
|
+
true
|
|
314
|
+
rescue E2B::NotFoundError
|
|
315
|
+
true
|
|
300
316
|
end
|
|
301
317
|
|
|
302
318
|
# Pause the sandbox (saves state for later resume)
|
|
@@ -305,10 +321,16 @@ module E2B
|
|
|
305
321
|
@state = "paused"
|
|
306
322
|
end
|
|
307
323
|
|
|
308
|
-
#
|
|
324
|
+
# @deprecated Use {#resume} instead. The instance-level `#connect`
|
|
325
|
+
# collides in name with the class-level {Sandbox.connect}, which has
|
|
326
|
+
# different semantics (it builds a new Sandbox instance from a
|
|
327
|
+
# sandbox_id). The instance method only resumes the current sandbox.
|
|
309
328
|
#
|
|
310
329
|
# @param timeout [Integer, nil] New timeout in seconds
|
|
311
330
|
def connect(timeout: nil)
|
|
331
|
+
warn "[DEPRECATION] Sandbox#connect is deprecated; use Sandbox#resume " \
|
|
332
|
+
"instead. (Sandbox.connect class method is unchanged.) " \
|
|
333
|
+
"Called from #{caller(1, 1).first}"
|
|
312
334
|
resume(timeout: timeout)
|
|
313
335
|
self
|
|
314
336
|
end
|
|
@@ -456,11 +478,13 @@ module E2B
|
|
|
456
478
|
|
|
457
479
|
@sandbox_id = data["sandboxID"] || data["sandbox_id"] || data[:sandboxID] || @sandbox_id
|
|
458
480
|
@template_id = data["templateID"] || data["template_id"] || data[:templateID] || @template_id
|
|
459
|
-
@alias_name = data["alias"] || data[:alias]
|
|
460
|
-
@client_id = data["clientID"] || data["client_id"] || data[:clientID]
|
|
461
|
-
@cpu_count = data["cpuCount"] || data["cpu_count"] || data[:cpuCount]
|
|
462
|
-
@memory_mb = data["memoryMB"] || data["memory_mb"] || data[:memoryMB]
|
|
463
|
-
|
|
481
|
+
@alias_name = data["alias"] || data[:alias] || @alias_name
|
|
482
|
+
@client_id = data["clientID"] || data["client_id"] || data[:clientID] || @client_id
|
|
483
|
+
@cpu_count = data["cpuCount"] || data["cpu_count"] || data[:cpuCount] || @cpu_count
|
|
484
|
+
@memory_mb = data["memoryMB"] || data["memory_mb"] || data[:memoryMB] || @memory_mb
|
|
485
|
+
metadata = data["metadata"] || data[:metadata]
|
|
486
|
+
@metadata = metadata if metadata
|
|
487
|
+
@metadata ||= {}
|
|
464
488
|
@state = data["state"] || data[:state] || @state
|
|
465
489
|
@domain = data["domain"] || data[:domain] || @domain
|
|
466
490
|
|
|
@@ -468,8 +492,10 @@ module E2B
|
|
|
468
492
|
@envd_access_token = data["envdAccessToken"] || data["envd_access_token"] || data[:envdAccessToken] || @envd_access_token
|
|
469
493
|
@traffic_access_token = data["trafficAccessToken"] || data["traffic_access_token"] || data[:trafficAccessToken] || @traffic_access_token
|
|
470
494
|
|
|
471
|
-
|
|
472
|
-
@
|
|
495
|
+
started_at = parse_time(data["startedAt"] || data["started_at"] || data[:startedAt])
|
|
496
|
+
@started_at = started_at if started_at
|
|
497
|
+
end_at = parse_time(data["endAt"] || data["end_at"] || data[:endAt])
|
|
498
|
+
@end_at = end_at if end_at
|
|
473
499
|
end
|
|
474
500
|
|
|
475
501
|
def initialize_services
|
|
@@ -494,8 +520,8 @@ module E2B
|
|
|
494
520
|
return nil if value.nil?
|
|
495
521
|
return value if value.is_a?(Time)
|
|
496
522
|
|
|
497
|
-
Time.parse(value)
|
|
498
|
-
rescue ArgumentError
|
|
523
|
+
Time.parse(value.to_s)
|
|
524
|
+
rescue ArgumentError, TypeError
|
|
499
525
|
nil
|
|
500
526
|
end
|
|
501
527
|
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "openssl"
|
|
6
|
-
require "ostruct"
|
|
7
6
|
require "rubygems/version"
|
|
8
7
|
|
|
9
8
|
module E2B
|
|
@@ -105,7 +104,13 @@ module E2B
|
|
|
105
104
|
private
|
|
106
105
|
|
|
107
106
|
def build_envd_client
|
|
108
|
-
|
|
107
|
+
# Ensure envd traffic bypasses HTTP proxy — the proxy often can't
|
|
108
|
+
# CONNECT-tunnel to sandbox subdomains. Append the sandbox domain
|
|
109
|
+
# to no_proxy so Net::HTTP and Faraday connect directly.
|
|
110
|
+
ensure_no_proxy_for_domain!(@sandbox_domain)
|
|
111
|
+
|
|
112
|
+
scheme = ENV.fetch("E2B_ENVD_SCHEME", "https")
|
|
113
|
+
envd_url = "#{scheme}://#{ENVD_PORT}-#{@sandbox_id}.#{@sandbox_domain}"
|
|
109
114
|
|
|
110
115
|
EnvdHttpClient.new(
|
|
111
116
|
base_url: envd_url,
|
|
@@ -115,6 +120,19 @@ module E2B
|
|
|
115
120
|
logger: @logger
|
|
116
121
|
)
|
|
117
122
|
end
|
|
123
|
+
|
|
124
|
+
# Append domain to no_proxy/NO_PROXY env vars at runtime so that
|
|
125
|
+
# both Net::HTTP and Faraday bypass the HTTP proxy for envd traffic.
|
|
126
|
+
def ensure_no_proxy_for_domain!(domain)
|
|
127
|
+
return if domain.nil? || domain.empty?
|
|
128
|
+
|
|
129
|
+
%w[no_proxy NO_PROXY].each do |var|
|
|
130
|
+
current = ENV[var].to_s
|
|
131
|
+
next if current.split(",").any? { |h| h.strip == domain }
|
|
132
|
+
|
|
133
|
+
ENV[var] = current.empty? ? domain : "#{current},#{domain}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
118
136
|
end
|
|
119
137
|
|
|
120
138
|
# HTTP client for envd daemon communication
|
|
@@ -125,6 +143,12 @@ module E2B
|
|
|
125
143
|
class EnvdHttpClient
|
|
126
144
|
DEFAULT_TIMEOUT = 120
|
|
127
145
|
|
|
146
|
+
RpcResponse = Struct.new(:status, :body, :headers, keyword_init: true) do
|
|
147
|
+
def success?
|
|
148
|
+
status >= 200 && status < 300
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
128
152
|
def initialize(base_url:, api_key:, access_token: nil, sandbox_id:, logger: nil)
|
|
129
153
|
@base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
|
|
130
154
|
@api_key = api_key
|
|
@@ -182,6 +206,9 @@ module E2B
|
|
|
182
206
|
return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
183
207
|
end
|
|
184
208
|
|
|
209
|
+
# Unary RPCs: try Connect protocol first, fall back to plain JSON.
|
|
210
|
+
# Some envd versions (e.g., 0.5.4 on self-hosted) reject
|
|
211
|
+
# application/connect+json for unary calls but accept application/json.
|
|
185
212
|
handle_rpc_response(service, method) do
|
|
186
213
|
with_retry("RPC #{service}/#{method}") do
|
|
187
214
|
url = URI.parse("#{@base_url.chomp('/')}#{path}")
|
|
@@ -196,9 +223,21 @@ module E2B
|
|
|
196
223
|
|
|
197
224
|
response = http.request(request)
|
|
198
225
|
|
|
199
|
-
|
|
226
|
+
# Fall back to plain JSON if Connect protocol is unsupported (HTTP 415)
|
|
227
|
+
if response.code.to_i == 415
|
|
228
|
+
log_debug("Connect protocol unsupported for #{service}/#{method}, falling back to plain JSON")
|
|
229
|
+
request = Net::HTTP::Post.new(url.request_uri)
|
|
230
|
+
request["Content-Type"] = "application/json"
|
|
231
|
+
request["X-Access-Token"] = @access_token if @access_token
|
|
232
|
+
request["Connection"] = "keep-alive"
|
|
233
|
+
apply_custom_headers(request, headers)
|
|
234
|
+
request.body = json_body
|
|
235
|
+
|
|
236
|
+
response = http.request(request)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
RpcResponse.new(
|
|
200
240
|
status: response.code.to_i,
|
|
201
|
-
success?: response.code.to_i >= 200 && response.code.to_i < 300,
|
|
202
241
|
body: response.body,
|
|
203
242
|
headers: response.to_hash
|
|
204
243
|
)
|
|
@@ -206,98 +245,116 @@ module E2B
|
|
|
206
245
|
end
|
|
207
246
|
end
|
|
208
247
|
|
|
209
|
-
# Streaming RPC with chunked response processing
|
|
248
|
+
# Streaming RPC with chunked response processing.
|
|
249
|
+
# Uses Faraday for the HTTP connection (same as non-streaming RPCs) to
|
|
250
|
+
# inherit proxy configuration and SSL settings. The streaming is handled
|
|
251
|
+
# via Faraday's on_data callback for chunked response processing.
|
|
252
|
+
#
|
|
253
|
+
# Streaming RPCs are NOT idempotent (e.g. process.Process/Start spawns a
|
|
254
|
+
# process), so we only retry while no events have been emitted to the
|
|
255
|
+
# caller yet. Once any byte has been delivered via on_event, a retry
|
|
256
|
+
# would replay output AND start a second process server-side.
|
|
210
257
|
def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
|
|
211
258
|
result = { events: [], stdout: "", stderr: "", exit_code: nil }
|
|
212
259
|
buffer = "".b
|
|
213
260
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
with_retry("Streaming RPC #{path}") do
|
|
217
|
-
http = build_http(url, timeout)
|
|
261
|
+
full_path = normalize_path(path)
|
|
218
262
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
request["X-Access-Token"] = @access_token if @access_token
|
|
222
|
-
request["Connection"] = "keep-alive"
|
|
223
|
-
apply_custom_headers(request, headers)
|
|
224
|
-
request.body = envelope
|
|
263
|
+
with_retry("Streaming RPC #{path}", abort_if: -> { result[:events].any? }) do
|
|
264
|
+
ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
|
|
225
265
|
|
|
226
|
-
|
|
227
|
-
conn.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
end
|
|
266
|
+
streaming_conn = Faraday.new(url: @base_url, ssl: { verify: ssl_verify }) do |conn|
|
|
267
|
+
conn.options.timeout = timeout
|
|
268
|
+
conn.options.open_timeout = 30
|
|
269
|
+
conn.adapter Faraday.default_adapter
|
|
270
|
+
end
|
|
232
271
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
272
|
+
req_headers = {
|
|
273
|
+
"Content-Type" => "application/connect+json",
|
|
274
|
+
"Connection" => "keep-alive",
|
|
275
|
+
"E2b-Sandbox-Id" => @sandbox_id,
|
|
276
|
+
"E2b-Sandbox-Port" => "#{BaseService::ENVD_PORT}",
|
|
277
|
+
"X-API-Key" => @api_key
|
|
278
|
+
}
|
|
279
|
+
req_headers["X-Access-Token"] = @access_token if @access_token
|
|
280
|
+
if headers
|
|
281
|
+
headers.each { |k, v| req_headers[k.to_s] = v.to_s if v }
|
|
282
|
+
end
|
|
236
283
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
response = streaming_conn.post(full_path) do |req|
|
|
285
|
+
req.headers.merge!(req_headers)
|
|
286
|
+
req.body = envelope
|
|
287
|
+
req.options.on_data = proc do |chunk, _overall_size, _env|
|
|
288
|
+
next if chunk.nil? || chunk.empty?
|
|
289
|
+
buffer << chunk
|
|
240
290
|
|
|
241
|
-
|
|
291
|
+
while buffer.bytesize >= 5
|
|
292
|
+
flags = buffer.getbyte(0)
|
|
293
|
+
length = buffer.byteslice(1, 4).unpack1("N")
|
|
242
294
|
|
|
243
|
-
|
|
244
|
-
buffer = buffer.byteslice(5 + length..-1) || "".b
|
|
295
|
+
break if length.nil? || buffer.bytesize < 5 + length
|
|
245
296
|
|
|
246
|
-
|
|
297
|
+
message_bytes = buffer.byteslice(5, length)
|
|
298
|
+
buffer = buffer.byteslice(5 + length..-1) || "".b
|
|
247
299
|
|
|
248
|
-
|
|
300
|
+
next if message_bytes.nil? || message_bytes.empty?
|
|
249
301
|
|
|
250
|
-
|
|
251
|
-
msg = JSON.parse(message_str)
|
|
252
|
-
msg = msg["result"] if msg["result"]
|
|
302
|
+
message_str = message_bytes.force_encoding("UTF-8")
|
|
253
303
|
|
|
254
|
-
|
|
304
|
+
begin
|
|
305
|
+
msg = JSON.parse(message_str)
|
|
306
|
+
msg = msg["result"] if msg["result"]
|
|
255
307
|
|
|
256
|
-
|
|
257
|
-
stderr_data = nil
|
|
308
|
+
result[:events] << msg
|
|
258
309
|
|
|
259
|
-
|
|
260
|
-
|
|
310
|
+
stdout_data = nil
|
|
311
|
+
stderr_data = nil
|
|
261
312
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
stdout_data = decode_base64(data_event["stdout"]) if data_event["stdout"]
|
|
265
|
-
stderr_data = decode_base64(data_event["stderr"]) if data_event["stderr"]
|
|
266
|
-
result[:stdout] += stdout_data if stdout_data
|
|
267
|
-
result[:stderr] += stderr_data if stderr_data
|
|
268
|
-
end
|
|
313
|
+
if msg["event"]
|
|
314
|
+
event = msg["event"]
|
|
269
315
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
316
|
+
data_event = event["Data"] || event["data"]
|
|
317
|
+
if data_event
|
|
318
|
+
stdout_data = decode_base64(data_event["stdout"]) if data_event["stdout"]
|
|
319
|
+
stderr_data = decode_base64(data_event["stderr"]) if data_event["stderr"]
|
|
320
|
+
result[:stdout] += stdout_data if stdout_data
|
|
321
|
+
result[:stderr] += stderr_data if stderr_data
|
|
274
322
|
end
|
|
275
323
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
result[:
|
|
279
|
-
end
|
|
280
|
-
if msg["stderr"]
|
|
281
|
-
stderr_data = decode_base64(msg["stderr"])
|
|
282
|
-
result[:stderr] += stderr_data
|
|
283
|
-
end
|
|
284
|
-
if msg["exitCode"] || msg["exit_code"]
|
|
285
|
-
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
|
|
324
|
+
end_event = event["End"] || event["end"]
|
|
325
|
+
if end_event
|
|
326
|
+
result[:exit_code] = parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
|
|
286
327
|
end
|
|
328
|
+
end
|
|
287
329
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
# Skip unparseable messages
|
|
330
|
+
if msg["stdout"]
|
|
331
|
+
stdout_data = decode_base64(msg["stdout"])
|
|
332
|
+
result[:stdout] += stdout_data
|
|
333
|
+
end
|
|
334
|
+
if msg["stderr"]
|
|
335
|
+
stderr_data = decode_base64(msg["stderr"])
|
|
336
|
+
result[:stderr] += stderr_data
|
|
296
337
|
end
|
|
338
|
+
if msg["exitCode"] || msg["exit_code"]
|
|
339
|
+
result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
on_event.call(
|
|
343
|
+
stdout: stdout_data,
|
|
344
|
+
stderr: stderr_data,
|
|
345
|
+
exit_code: result[:exit_code],
|
|
346
|
+
event: msg
|
|
347
|
+
)
|
|
348
|
+
rescue JSON::ParserError
|
|
349
|
+
# Skip unparseable messages
|
|
297
350
|
end
|
|
298
351
|
end
|
|
299
352
|
end
|
|
300
353
|
end
|
|
354
|
+
|
|
355
|
+
unless response.status.between?(200, 299)
|
|
356
|
+
handle_error(response)
|
|
357
|
+
end
|
|
301
358
|
end
|
|
302
359
|
|
|
303
360
|
result
|
|
@@ -328,24 +385,55 @@ module E2B
|
|
|
328
385
|
end
|
|
329
386
|
|
|
330
387
|
def build_http(url, timeout)
|
|
331
|
-
|
|
332
|
-
|
|
388
|
+
# Respect HTTP proxy env vars (http_proxy, https_proxy, no_proxy) —
|
|
389
|
+
# Faraday handles this automatically for non-streaming RPCs, but
|
|
390
|
+
# Net::HTTP requires explicit proxy configuration.
|
|
391
|
+
proxy = resolve_proxy(url)
|
|
392
|
+
http = if proxy
|
|
393
|
+
Net::HTTP.new(url.host, url.port, proxy.host, proxy.port, proxy.user, proxy.password)
|
|
394
|
+
else
|
|
395
|
+
Net::HTTP.new(url.host, url.port)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
http.use_ssl = (url.scheme == "https")
|
|
333
399
|
http.open_timeout = 30
|
|
334
400
|
http.read_timeout = timeout
|
|
335
401
|
http.keep_alive_timeout = 30
|
|
336
402
|
|
|
337
|
-
|
|
338
|
-
|
|
403
|
+
if http.use_ssl?
|
|
404
|
+
ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
|
|
405
|
+
http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
406
|
+
end
|
|
339
407
|
|
|
340
408
|
http
|
|
341
409
|
end
|
|
342
410
|
|
|
343
|
-
def
|
|
411
|
+
def resolve_proxy(url)
|
|
412
|
+
no_proxy = ENV["no_proxy"] || ENV["NO_PROXY"]
|
|
413
|
+
if no_proxy
|
|
414
|
+
no_proxy_hosts = no_proxy.split(",").map(&:strip)
|
|
415
|
+
return nil if no_proxy_hosts.any? { |h| url.host.end_with?(h) || h == "*" }
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
proxy_env = url.scheme == "https" ? (ENV["https_proxy"] || ENV["HTTPS_PROXY"]) : (ENV["http_proxy"] || ENV["HTTP_PROXY"])
|
|
419
|
+
return nil unless proxy_env
|
|
420
|
+
|
|
421
|
+
URI.parse(proxy_env)
|
|
422
|
+
rescue URI::InvalidURIError
|
|
423
|
+
nil
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def with_retry(operation, max_retries: 3, abort_if: nil)
|
|
344
427
|
retry_count = 0
|
|
345
428
|
|
|
346
429
|
begin
|
|
347
430
|
yield
|
|
348
431
|
rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, EOFError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
432
|
+
if abort_if && abort_if.call
|
|
433
|
+
log_debug("#{operation}: not retrying (#{e.class}); request had observable side effects")
|
|
434
|
+
raise E2B::E2BError, "#{operation} failed after partial response: #{e.message}"
|
|
435
|
+
end
|
|
436
|
+
|
|
349
437
|
retry_count += 1
|
|
350
438
|
|
|
351
439
|
if retry_count <= max_retries
|
|
@@ -467,7 +555,11 @@ module E2B
|
|
|
467
555
|
def create_connect_envelope(json_message)
|
|
468
556
|
flags = "\x00".b
|
|
469
557
|
length = [json_message.bytesize].pack("N")
|
|
470
|
-
|
|
558
|
+
# Force binary encoding on the payload so concatenation with the binary
|
|
559
|
+
# frame header (flags + length) doesn't raise Encoding::CompatibilityError
|
|
560
|
+
# when the packed length contains bytes >= 0x80 (json_message.bytesize >= 32768)
|
|
561
|
+
# or json_message itself has multibyte UTF-8 characters.
|
|
562
|
+
flags + length + json_message.b
|
|
471
563
|
end
|
|
472
564
|
|
|
473
565
|
def parse_exit_code(value)
|
|
@@ -480,7 +572,7 @@ module E2B
|
|
|
480
572
|
elsif str =~ /^(\d+)$/
|
|
481
573
|
$1.to_i
|
|
482
574
|
else
|
|
483
|
-
|
|
575
|
+
1
|
|
484
576
|
end
|
|
485
577
|
end
|
|
486
578
|
|
|
@@ -109,6 +109,10 @@ module E2B
|
|
|
109
109
|
|
|
110
110
|
# Check if a path exists
|
|
111
111
|
#
|
|
112
|
+
# Only NotFoundError is treated as "does not exist". Other errors
|
|
113
|
+
# (auth, network, server) propagate so callers can distinguish
|
|
114
|
+
# "file is gone" from "we couldn't ask".
|
|
115
|
+
#
|
|
112
116
|
# @param path [String] Path to check
|
|
113
117
|
# @param user [String] Username context
|
|
114
118
|
# @param request_timeout [Integer] Request timeout in seconds
|
|
@@ -116,7 +120,7 @@ module E2B
|
|
|
116
120
|
def exists?(path, user: nil, request_timeout: 30)
|
|
117
121
|
get_info(path, user: user, request_timeout: request_timeout)
|
|
118
122
|
true
|
|
119
|
-
rescue E2B::NotFoundError
|
|
123
|
+
rescue E2B::NotFoundError
|
|
120
124
|
false
|
|
121
125
|
end
|
|
122
126
|
|
data/lib/e2b/template.rb
CHANGED
|
@@ -134,7 +134,8 @@ module E2B
|
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
def build(template, name: nil, alias_name: nil, tags: nil, cpu_count: 2, memory_mb: 1024,
|
|
137
|
+
def build(template, name: nil, alias_name: nil, tags: nil, cpu_count: 2, memory_mb: 1024,
|
|
138
|
+
disk_size_mb: nil, skip_cache: false,
|
|
138
139
|
on_build_logs: nil, api_key: nil, access_token: nil, domain: nil, **opts)
|
|
139
140
|
on_build_logs&.call(Models::TemplateLogEntryStart.new(timestamp: Time.now, message: "Build started"))
|
|
140
141
|
|
|
@@ -145,6 +146,7 @@ module E2B
|
|
|
145
146
|
tags: tags,
|
|
146
147
|
cpu_count: cpu_count,
|
|
147
148
|
memory_mb: memory_mb,
|
|
149
|
+
disk_size_mb: disk_size_mb,
|
|
148
150
|
skip_cache: skip_cache,
|
|
149
151
|
on_build_logs: on_build_logs,
|
|
150
152
|
api_key: api_key,
|
|
@@ -168,7 +170,8 @@ module E2B
|
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
def build_in_background(template, name: nil, alias_name: nil, tags: nil, cpu_count: 2, memory_mb: 1024,
|
|
171
|
-
|
|
173
|
+
disk_size_mb: nil, skip_cache: false, on_build_logs: nil,
|
|
174
|
+
api_key: nil, access_token: nil, domain: nil, **opts)
|
|
172
175
|
alias_name ||= opts[:alias] || opts["alias"]
|
|
173
176
|
resolved_name = normalize_build_name(name: name, alias_name: alias_name)
|
|
174
177
|
template.send(:force_build!) if skip_cache
|
|
@@ -179,12 +182,14 @@ module E2B
|
|
|
179
182
|
tags_message = Array(tags).any? ? " with tags #{Array(tags).join(', ')}" : ""
|
|
180
183
|
on_build_logs&.call(log_entry("Requesting build for template: #{resolved_name}#{tags_message}"))
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
body = {
|
|
183
186
|
name: resolved_name,
|
|
184
187
|
tags: tags,
|
|
185
188
|
cpuCount: cpu_count,
|
|
186
189
|
memoryMB: memory_mb
|
|
187
|
-
}
|
|
190
|
+
}
|
|
191
|
+
body[:diskSizeMB] = disk_size_mb if disk_size_mb
|
|
192
|
+
create_response = http_client.post("/v3/templates", body: body)
|
|
188
193
|
|
|
189
194
|
build_info = Models::BuildInfo.new(
|
|
190
195
|
alias_name: resolved_name,
|
data/lib/e2b/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: e2b
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tao Luo
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: faraday
|
|
@@ -57,20 +57,6 @@ dependencies:
|
|
|
57
57
|
- - "~>"
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '0.2'
|
|
60
|
-
- !ruby/object:Gem::Dependency
|
|
61
|
-
name: ostruct
|
|
62
|
-
requirement: !ruby/object:Gem::Requirement
|
|
63
|
-
requirements:
|
|
64
|
-
- - "~>"
|
|
65
|
-
- !ruby/object:Gem::Version
|
|
66
|
-
version: '0.6'
|
|
67
|
-
type: :runtime
|
|
68
|
-
prerelease: false
|
|
69
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
-
requirements:
|
|
71
|
-
- - "~>"
|
|
72
|
-
- !ruby/object:Gem::Version
|
|
73
|
-
version: '0.6'
|
|
74
60
|
- !ruby/object:Gem::Dependency
|
|
75
61
|
name: rake
|
|
76
62
|
requirement: !ruby/object:Gem::Requirement
|