withruntime 0.26.1 → 0.27.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/lib/withruntime/tunnel.rb +1 -1
- data/lib/withruntime/version.rb +1 -1
- data/lib/withruntime/websocket.rb +139 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f79ec4779408c96eaf66e899816f7329afd4af057cc9eccd445681e1dc7b6085
|
|
4
|
+
data.tar.gz: 9996a989720990ddf7df18b3ec982c1c0edf6be77b3bac0d3eb7625ab0645b7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f5f55d187c9de060e19224c3fb91da8efa2dd93a7e65b66380837e097be183c000f4799557135cdb76adc0e0dfd890e85bfca8ec5859a6c519a811a3b2616be
|
|
7
|
+
data.tar.gz: 603a8941b52711efa41d7a3b9c73e65f0e59a44ce901216822781106abda51480651678ee3aa71ba4115b476c8e1fe7e6171e58b388cee2b51e92b94bb780ec5
|
data/lib/withruntime/tunnel.rb
CHANGED
|
@@ -7,7 +7,7 @@ module WithRuntime
|
|
|
7
7
|
def initialize(sandbox)
|
|
8
8
|
@lock, @credit, @send_lock = Mutex.new, ConditionVariable.new, Mutex.new
|
|
9
9
|
@streams, @next, @sent, @credited, @closed = {}, 1, 0, 0, false
|
|
10
|
-
@socket =
|
|
10
|
+
@socket = ResumableSocket.new(sandbox.transport, sandbox.path("/tunnel"))
|
|
11
11
|
deadline = @socket.now + @socket.timeout
|
|
12
12
|
loop do
|
|
13
13
|
frame = @socket.read(deadline)
|
data/lib/withruntime/version.rb
CHANGED
|
@@ -6,7 +6,8 @@ require "digest"
|
|
|
6
6
|
module WithRuntime
|
|
7
7
|
# One bounded RFC6455 connection, using the transport's origin, key and proxy rules.
|
|
8
8
|
class WebSocket
|
|
9
|
-
|
|
9
|
+
# The close code the server sent, once it closed.
|
|
10
|
+
attr_reader :timeout, :close_code
|
|
10
11
|
def initialize(transport, path, query = nil)
|
|
11
12
|
@transport = transport
|
|
12
13
|
@timeout = transport.timeout
|
|
@@ -46,7 +47,7 @@ module WithRuntime
|
|
|
46
47
|
status, fields = headers(deadline)
|
|
47
48
|
accept = [Digest::SHA1.digest(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")].pack("m0")
|
|
48
49
|
unless status.match?(/\AHTTP\/1\.[01] 101 /) && fields["sec-websocket-accept"] == accept && fields["upgrade"].to_s.downcase == "websocket"
|
|
49
|
-
raise Error.new("The server refused the WebSocket upgrade.", code: "websocket_refused")
|
|
50
|
+
raise Error.new("The server refused the WebSocket upgrade.", code: "websocket_refused", status: status[/\AHTTP\/1\.[01] (\d{3})/, 1].to_i)
|
|
50
51
|
end
|
|
51
52
|
rescue *Transport::NETWORK_ERRORS => error
|
|
52
53
|
close
|
|
@@ -119,7 +120,10 @@ module WithRuntime
|
|
|
119
120
|
raise Error.new("WebSocket frame exceeds its bound.", code: "stream_overflow")
|
|
120
121
|
end
|
|
121
122
|
data = exact(length, deadline)
|
|
122
|
-
|
|
123
|
+
if op == 8
|
|
124
|
+
@close_code = data.bytesize >= 2 ? data.unpack1("n") : 1005
|
|
125
|
+
return nil
|
|
126
|
+
end
|
|
123
127
|
if op == 9
|
|
124
128
|
send_frame(10, data)
|
|
125
129
|
next
|
|
@@ -150,6 +154,137 @@ module WithRuntime
|
|
|
150
154
|
end
|
|
151
155
|
end
|
|
152
156
|
|
|
157
|
+
# A terminal's or tunnel's WebSocket that outlives a restart of the API behind it
|
|
158
|
+
# (packages/cloud/src/api/tunnels.ts, "Hand-off"): on {"type":"restart"} it holds what is
|
|
159
|
+
# sent and answers; on {"type":"resume"} and close 1012 it opens the socket again with
|
|
160
|
+
# ?resume=<token>, sends {"type":"replay"} first, then what it held. Output continues from
|
|
161
|
+
# the byte it stopped at, and the reader sees no close.
|
|
162
|
+
class ResumableSocket
|
|
163
|
+
# RFC 6455 close code 1012, Service Restart.
|
|
164
|
+
SERVICE_RESTART = 1012
|
|
165
|
+
# How long reconnecting may take: within the token's 30 s, after which the API ends it.
|
|
166
|
+
RESUME_SECONDS = 25
|
|
167
|
+
attr_reader :timeout, :resumes, :replays
|
|
168
|
+
def initialize(transport, path, query = nil)
|
|
169
|
+
@transport, @path = transport, path
|
|
170
|
+
@socket = WebSocket.new(transport, path, query)
|
|
171
|
+
@timeout = @socket.timeout
|
|
172
|
+
# Orders sends, the restart's answer and the switch to a new socket. close never takes
|
|
173
|
+
# it, so it can close a socket under a send blocked holding it.
|
|
174
|
+
@send_lock = Mutex.new
|
|
175
|
+
@paused, @held, @closed, @resumes, @replays = false, [], false, 0, 0
|
|
176
|
+
end
|
|
177
|
+
def now = @socket.now
|
|
178
|
+
def send_frame(op, bytes)
|
|
179
|
+
@send_lock.synchronize do
|
|
180
|
+
raise IOError, "WebSocket closed" if @closed
|
|
181
|
+
next @held << [op, bytes.b] if @paused
|
|
182
|
+
@socket.send_frame(op, bytes)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
# The next message the reader sees; the restart's own are not.
|
|
186
|
+
def read(deadline = nil)
|
|
187
|
+
loop do
|
|
188
|
+
socket = @socket
|
|
189
|
+
message = socket.read(deadline)
|
|
190
|
+
unless message
|
|
191
|
+
socket.close
|
|
192
|
+
token = @token
|
|
193
|
+
next if token && socket.close_code == SERVICE_RESTART && !@closed && resume(token)
|
|
194
|
+
said, @said = @said, nil
|
|
195
|
+
return said
|
|
196
|
+
end
|
|
197
|
+
if message[0] == 1 && message[1].bytesize <= 4096
|
|
198
|
+
reply = begin
|
|
199
|
+
JSON.parse(message[1])
|
|
200
|
+
rescue JSON::ParserError
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
if reply.is_a?(Hash) && reply["type"] == "restart"
|
|
204
|
+
@send_lock.synchronize do
|
|
205
|
+
@paused = true
|
|
206
|
+
socket.send_frame(1, JSON.generate(type: "restart"))
|
|
207
|
+
end
|
|
208
|
+
next
|
|
209
|
+
end
|
|
210
|
+
if reply.is_a?(Hash) && reply["type"] == "resume" && reply["token"]
|
|
211
|
+
@token, @replay = reply["token"], reply["replay"]
|
|
212
|
+
next
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
return message
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
def close
|
|
219
|
+
@closed = true
|
|
220
|
+
@attempt&.close
|
|
221
|
+
# First unblocks a send in flight; then, under its lock, the socket a resume may have
|
|
222
|
+
# swapped in meanwhile.
|
|
223
|
+
@socket.close
|
|
224
|
+
@send_lock.synchronize do
|
|
225
|
+
@held.clear
|
|
226
|
+
@socket.close
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# Opens the socket again where it stopped; false when it cannot.
|
|
233
|
+
def resume(token)
|
|
234
|
+
replay = @replay
|
|
235
|
+
@token = @replay = nil
|
|
236
|
+
deadline = now + RESUME_SECONDS
|
|
237
|
+
wait = 0.05
|
|
238
|
+
while !@closed && now < deadline
|
|
239
|
+
socket = nil
|
|
240
|
+
begin
|
|
241
|
+
socket = @attempt = WebSocket.new(@transport, @path, { "resume" => token })
|
|
242
|
+
# A close that came while it opened.
|
|
243
|
+
break socket.close if @closed
|
|
244
|
+
# A server that never answers must not hold this past the deadline.
|
|
245
|
+
message = socket.read(deadline)
|
|
246
|
+
reply = message && message[0] == 1 ? JSON.parse(message[1]) : nil
|
|
247
|
+
if reply.is_a?(Hash) && reply["type"] == "ready" && reply["resumed"]
|
|
248
|
+
return @send_lock.synchronize do
|
|
249
|
+
if @closed
|
|
250
|
+
socket.close
|
|
251
|
+
next false
|
|
252
|
+
end
|
|
253
|
+
@socket = socket
|
|
254
|
+
@resumes += 1
|
|
255
|
+
if replay && !replay.empty?
|
|
256
|
+
@replays += 1
|
|
257
|
+
socket.send_frame(1, JSON.generate(type: "replay", data: replay))
|
|
258
|
+
end
|
|
259
|
+
@held.each { |op, bytes| socket.send_frame(op, bytes) }
|
|
260
|
+
@held.clear
|
|
261
|
+
@paused = false
|
|
262
|
+
true
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
socket.close
|
|
266
|
+
if reply.is_a?(Hash) && reply["type"] == "error"
|
|
267
|
+
# Refused for good (the sandbox stopped, say): the reader reads the error.
|
|
268
|
+
@said = message
|
|
269
|
+
return false
|
|
270
|
+
end
|
|
271
|
+
rescue Error => error
|
|
272
|
+
socket&.close
|
|
273
|
+
# A 4xx other than 429: the token or the sandbox is not good, and asking again
|
|
274
|
+
# changes nothing.
|
|
275
|
+
return false if error.status.between?(400, 499) && error.status != 429
|
|
276
|
+
rescue JSON::ParserError, IOError, SystemCallError
|
|
277
|
+
socket&.close
|
|
278
|
+
ensure
|
|
279
|
+
@attempt = nil
|
|
280
|
+
end
|
|
281
|
+
sleep wait
|
|
282
|
+
wait = [wait * 2, 1].min
|
|
283
|
+
end
|
|
284
|
+
false
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
153
288
|
# Producer never waits for a reader. The stream owner decides how overflow is reported.
|
|
154
289
|
class StreamBuffer
|
|
155
290
|
def initialize
|
|
@@ -193,7 +328,7 @@ module WithRuntime
|
|
|
193
328
|
class Terminal
|
|
194
329
|
attr_reader :process_id, :exit_code
|
|
195
330
|
def initialize(sandbox, **options)
|
|
196
|
-
@socket =
|
|
331
|
+
@socket = ResumableSocket.new(sandbox.transport, sandbox.path("/terminal"), Fields.body(options))
|
|
197
332
|
@buffer = StreamBuffer.new
|
|
198
333
|
deadline = @socket.now + @socket.timeout
|
|
199
334
|
loop do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: withruntime
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Runtime
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'Linux sandboxes for agents, and every other Runtime Cloud product: streaming
|
|
14
14
|
commands and terminals, files and watches, images, durable volumes and backups,
|