async-htty 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9d30babccdd7bf9fe410add7bb9dd2a6f7dafaa4d642bb377bf431f2aea4818
4
- data.tar.gz: 8cd9da81dce5256dd99c9724491f07c94217fda9efd604f468c67b61a1b4f379
3
+ metadata.gz: f033a6514d7f18ec5f7c94b4f547ef2fa00e025eedce18d004387dde4baae5bb
4
+ data.tar.gz: 6d6a4cf76032a9e403c3f38f0ed4a46d1f6af61dd61ca3b712c68c93eabea6f8
5
5
  SHA512:
6
- metadata.gz: b9a43f93f8c0a6a73e3114ea24ff950fcce09d49bd06752ca121e720ea8f566d220b41ee000514819bd1b537551e7ef74b0e191f3c6709e73a44083b0278e667
7
- data.tar.gz: 0d9d94b0125c871ee18a5f95c98e45249e84b2397ef751f9c4b56b9b3175842b438630a998fd063fe0f848f75570c6a9f94b66ddc7044cdd99451173ff6c387d
6
+ metadata.gz: 1f027635ab2786cae7850b213eba3c37e3ac9762409d746a0d54e83d6a9366963d598843f7ccedbd729298c877181062dce7c28f20154c91a6a944cdea2430c3
7
+ data.tar.gz: b3bfefe4d878d7a404ffc3f3d66798f9acd5e6e17b03743849f58a281dde09a3d63c1fc85bb7918f4b8ff6a340c4335e2ddaaf1d20e882d1f270c6f953ba3f82
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/http/protocol/http2/server"
7
+
8
+ module Async
9
+ module HTTY
10
+ module Protocol
11
+ module HTTY
12
+ class Server < ::Async::HTTP::Protocol::HTTP2::Server
13
+ def receive_goaway(frame)
14
+ super
15
+
16
+ unless self.framer.nil?
17
+ self.send_goaway
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -6,6 +6,7 @@
6
6
  require "protocol/htty"
7
7
 
8
8
  require "async/http/protocol/http2"
9
+ require_relative "htty/server"
9
10
 
10
11
  module Async
11
12
  module HTTY
@@ -24,7 +25,7 @@ module Async
24
25
  def self.server(stream, settings: ::Async::HTTP::Protocol::HTTP2::SERVER_SETTINGS)
25
26
  stream = ::Protocol::HTTY::Stream.open(stream, bootstrap: :write)
26
27
 
27
- server = ::Async::HTTP::Protocol::HTTP2::Server.new(stream)
28
+ server = Server.new(stream)
28
29
  server.read_connection_preface(settings)
29
30
  server.start_connection
30
31
 
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module HTTY
8
- VERSION = "0.2.0"
8
+ VERSION = "0.2.1"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -14,6 +14,11 @@ Please see the [project documentation](https://socketry.github.io/async-htty/) f
14
14
 
15
15
  Please see the [project releases](https://socketry.github.io/async-htty/releases/index) for all releases.
16
16
 
17
+ ### v0.2.1
18
+
19
+ - Send a server-side GOAWAY when the HTTY client closes an HTTP/2 session, allowing terminal clients to detach cleanly.
20
+ - Add PTY coverage for binary request/response bodies across the full byte range.
21
+
17
22
  ### v0.2.0
18
23
 
19
24
  - Reopen `stdin`, `stdout`, and `stderr` to null devices to prevent output from interfering with HTTY's byte stream.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.2.1
4
+
5
+ - Send a server-side GOAWAY when the HTTY client closes an HTTP/2 session, allowing terminal clients to detach cleanly.
6
+ - Add PTY coverage for binary request/response bodies across the full byte range.
7
+
3
8
  ## v0.2.0
4
9
 
5
10
  - Reopen `stdin`, `stdout`, and `stderr` to null devices to prevent output from interfering with HTTY's byte stream.
@@ -4,12 +4,48 @@
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
6
  require "async"
7
+ require "protocol/http/body/writable"
7
8
  require "protocol/http/request"
8
9
  require "protocol/http/response"
9
10
  require "protocol/http2"
11
+ require "protocol/http2/client"
12
+ require "protocol/http2/stream"
13
+ require "rbconfig"
10
14
  require "async/htty"
11
15
 
16
+ require "async/htty/pty_stream"
17
+
18
+ class EchoResponseStream < Protocol::HTTP2::Stream
19
+ attr :response_headers
20
+ attr :body
21
+
22
+ def initialize(...)
23
+ super
24
+ @response_headers = []
25
+ @body = +"".b
26
+ end
27
+
28
+ def process_headers(frame)
29
+ @response_headers = super
30
+ end
31
+
32
+ def process_data(frame)
33
+ data = super
34
+ @body << data.b if data
35
+ return data
36
+ end
37
+ end
38
+
39
+ class EchoClient < Protocol::HTTP2::Client
40
+ def create_stream(id = next_stream_id)
41
+ EchoResponseStream.create(self, id)
42
+ end
43
+ end
44
+
12
45
  describe Async::HTTY::Protocol::HTTY do
46
+ let(:root) {File.expand_path("../../../..", __dir__)}
47
+ let(:ruby_load_path) {File.join(root, "lib")}
48
+
13
49
  def make_pipes
14
50
  server_input, client_output = IO.pipe
15
51
  client_input, server_output = IO.pipe
@@ -36,6 +72,29 @@ describe Async::HTTY::Protocol::HTTY do
36
72
  IO::Stream::Duplex(pipes[:server_input], pipes[:server_output])
37
73
  end
38
74
 
75
+ def spawn_fixture(name)
76
+ environment = {
77
+ "HTTY" => "1",
78
+ "RUBYLIB" => [ruby_load_path, ENV["RUBYLIB"]].compact.join(":"),
79
+ }
80
+ executable = File.join(root, "fixtures", "async", "htty", "executables", name)
81
+
82
+ PTY.spawn(environment, RbConfig.ruby, executable)
83
+ end
84
+
85
+ def with_fixture(name)
86
+ input, output, pid = spawn_fixture(name)
87
+ input.binmode
88
+ output.binmode
89
+
90
+ stream = Async::HTTY::PTYStream.new(input, output)
91
+
92
+ yield stream
93
+ ensure
94
+ stream&.close
95
+ Process.wait(pid) rescue nil
96
+ end
97
+
39
98
  it "can carry an HTTP/2 request over HTTY bootstrap and raw transport" do
40
99
  pipes = make_pipes
41
100
 
@@ -60,6 +119,95 @@ describe Async::HTTY::Protocol::HTTY do
60
119
  end
61
120
  end
62
121
 
122
+ it "round trips all byte values over a real PTY" do
123
+ payload = (0x00..0xff).to_a.pack("C*")
124
+
125
+ with_fixture("echo_body.rb") do |stream|
126
+ Protocol::HTTY::Stream.new(stream).read_bootstrap
127
+
128
+ framer = Protocol::HTTP2::Framer.new(stream)
129
+ client = EchoClient.new(framer)
130
+ client.send_connection_preface
131
+
132
+ request = client.create_stream
133
+ request.send_headers(
134
+ [
135
+ [":method", "POST"],
136
+ [":path", "/echo"],
137
+ [":scheme", "http"],
138
+ [":authority", "htty.local"],
139
+ ["content-length", payload.bytesize.to_s],
140
+ ["content-type", "application/octet-stream"],
141
+ ]
142
+ )
143
+ request.send_data(payload)
144
+ request.send_data(nil)
145
+
146
+ until request.closed?
147
+ client.read_frame
148
+ end
149
+
150
+ expect(request.response_headers.to_h[":status"]).to be == "200"
151
+ expect(request.body).to be == payload
152
+ ensure
153
+ client&.send_goaway
154
+ client&.close
155
+ end
156
+ end
157
+
158
+ it "returns from accept when the client sends GOAWAY while a response body is active" do
159
+ pipes = make_pipes
160
+ body = Protocol::HTTP::Body::Writable.new
161
+
162
+ Sync do |task|
163
+ request_started = Async::Notification.new
164
+ server_finished = Async::Notification.new
165
+
166
+ server = Async::HTTY::Server.for do |request|
167
+ request_started.signal
168
+ Protocol::HTTP::Response[200, {}, body]
169
+ end
170
+
171
+ server_task = task.async do
172
+ server.accept(server_stream(pipes))
173
+ ensure
174
+ server_finished.signal
175
+ end
176
+
177
+ stream = Protocol::HTTY::Stream.open(client_stream(pipes), bootstrap: :read)
178
+ framer = Protocol::HTTP2::Framer.new(stream)
179
+ client = Protocol::HTTP2::Client.new(framer)
180
+ client.send_connection_preface
181
+
182
+ request = client.create_stream
183
+ request.send_headers(
184
+ [[":method", "GET"], [":path", "/"], [":scheme", "http"], [":authority", "htty.local"]],
185
+ Protocol::HTTP2::END_STREAM
186
+ )
187
+
188
+ request_started.wait
189
+ client.send_goaway
190
+
191
+ task.with_timeout(1) do
192
+ server_finished.wait
193
+ end
194
+
195
+ frame = task.with_timeout(1) do
196
+ loop do
197
+ frame = framer.read_frame(client.local_settings.maximum_frame_size)
198
+ break frame if frame.is_a?(Protocol::HTTP2::GoawayFrame)
199
+ end
200
+ end
201
+
202
+ expect(frame).to be(:is_a?, Protocol::HTTP2::GoawayFrame)
203
+ ensure
204
+ body.close
205
+ client&.close
206
+ server_task&.stop
207
+ close_pipes(pipes)
208
+ end
209
+ end
210
+
63
211
  it "ignores terminal noise before the HTTY bootstrap" do
64
212
  pipes = make_pipes
65
213
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-htty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -105,6 +105,7 @@ files:
105
105
  - lib/async/htty/error.rb
106
106
  - lib/async/htty/protocol.rb
107
107
  - lib/async/htty/protocol/htty.rb
108
+ - lib/async/htty/protocol/htty/server.rb
108
109
  - lib/async/htty/server.rb
109
110
  - lib/async/htty/version.rb
110
111
  - license.md
metadata.gz.sig CHANGED
Binary file