async-htty 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9d30babccdd7bf9fe410add7bb9dd2a6f7dafaa4d642bb377bf431f2aea4818
4
- data.tar.gz: 8cd9da81dce5256dd99c9724491f07c94217fda9efd604f468c67b61a1b4f379
3
+ metadata.gz: 9d7783c87883c9c45dd3d05a11b2060fea115b7a1b5a2435bcb3ebe3a2f0bb06
4
+ data.tar.gz: 235ff150ef9cf4966538b2899a2471df665544606d8599345f62f863772d6632
5
5
  SHA512:
6
- metadata.gz: b9a43f93f8c0a6a73e3114ea24ff950fcce09d49bd06752ca121e720ea8f566d220b41ee000514819bd1b537551e7ef74b0e191f3c6709e73a44083b0278e667
7
- data.tar.gz: 0d9d94b0125c871ee18a5f95c98e45249e84b2397ef751f9c4b56b9b3175842b438630a998fd063fe0f848f75570c6a9f94b66ddc7044cdd99451173ff6c387d
6
+ metadata.gz: af3056573867c1b572c48a713c28a07cfa7d33d363d1a5c94ef555faee4dfae35e8aeb482ed5d14cd70c11cdcf5bc78d6a5e1296c9f3b61eb4ad958f674b3166
7
+ data.tar.gz: 97bf7592db90bc2df78b28e39f04ea14d53ea725443e56693c3f5daf5661e2b8ce018b6e2fb5692991619a0d5dc0fc676f291131e3291931c23f1b02c71d2b55
checksums.yaml.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- �;�$�qǂ���jS-G��ɤЪ$�"&|�rH� ���X�^ 6A�#�\:��"g�.gݦ���8�>���%�U����шOyH�� ��B����kIQ�N��������ptds2��^BN���rpS%��Ib>QP.(%�_%ܩʽ�X+��,�x'9�B:[&���n���9�&*�f��׆�QoP]�����Ͳ���� m�Φ�*R|�<<���͐hn23�?�����
2
- ع��ԩ_E@�����l��3�%#ɖ*�Բ�0�%q��1HIMRӛ�/�
1
+ ����2+����X�rg ��oiF��THj)��+�����S�JʾT�N�3F��n�#�5�#�J#���N4Y�q��E�ĸz����=E��[lAc���t��CBU>$�)�R�M�_ppF��b �;L�ѩ�<��
2
+ H,^SO{�&8h�9 <����`q� ��(�n6j��ˡY��,���y��� ƥ��V��yQ���Zaj��"тEJ {Q�����i-Ʋ�>�gC%*у�=j�7�! �)i��N���P�D|qf;������Ο|� �!e�5C:���V�~,p���Jl����=�+�oj�/Ϝ9#�wd��0Lo�ڋH����'���$pJ�.ƨŢ��@X���㽂�#������|�
@@ -0,0 +1,25 @@
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
+ if framer = self.framer
17
+ self.send_goaway
18
+ framer.flush
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -6,13 +6,18 @@
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
12
13
  module Protocol
13
14
  module HTTY
14
15
  def self.client(stream, settings: ::Async::HTTP::Protocol::HTTP2::CLIENT_SETTINGS)
15
- stream = ::Protocol::HTTY::Stream.open(stream, bootstrap: :read)
16
+ mode = stream.read_bootstrap
17
+
18
+ unless mode == ::Protocol::HTTY::Stream::RAW_MODE
19
+ raise ::Protocol::HTTY::ProtocolError, "Expected HTTY bootstrap mode #{::Protocol::HTTY::Stream::RAW_MODE.inspect}, got #{mode.inspect}"
20
+ end
16
21
 
17
22
  client = ::Async::HTTP::Protocol::HTTP2::Client.new(stream)
18
23
  client.send_connection_preface(settings)
@@ -22,9 +27,9 @@ module Async
22
27
  end
23
28
 
24
29
  def self.server(stream, settings: ::Async::HTTP::Protocol::HTTP2::SERVER_SETTINGS)
25
- stream = ::Protocol::HTTY::Stream.open(stream, bootstrap: :write)
30
+ stream.write_bootstrap
26
31
 
27
- server = ::Async::HTTP::Protocol::HTTP2::Server.new(stream)
32
+ server = Server.new(stream)
28
33
  server.read_connection_preface(settings)
29
34
  server.start_connection
30
35
 
@@ -45,7 +45,7 @@ module Async
45
45
  original_output = output.dup
46
46
  original_error = error.dup
47
47
 
48
- stream = ::IO::Stream::Duplex(original_input, original_output)
48
+ stream = ::Protocol::HTTY::Stream.new(original_input, original_output)
49
49
  input.reopen(File::NULL)
50
50
  output.reopen(File::NULL)
51
51
  error.reopen(File::NULL)
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module HTTY
8
- VERSION = "0.2.0"
8
+ VERSION = "0.3.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -14,6 +14,16 @@ 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.3.0
18
+
19
+ - Pass explicit terminal input and output endpoints into `Protocol::HTTY::Stream`, avoiding buffered duplex reads across the HTTY HTTP/2 transport.
20
+ - Expect the HTTY protocol adapter to receive a prepared `Protocol::HTTY::Stream` instance before performing bootstrap and HTTP/2 setup.
21
+
22
+ ### v0.2.1
23
+
24
+ - Send a server-side GOAWAY when the HTTY client closes an HTTP/2 session, allowing terminal clients to detach cleanly.
25
+ - Add PTY coverage for binary request/response bodies across the full byte range.
26
+
17
27
  ### v0.2.0
18
28
 
19
29
  - 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,15 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.0
4
+
5
+ - Pass explicit terminal input and output endpoints into `Protocol::HTTY::Stream`, avoiding buffered duplex reads across the HTTY HTTP/2 transport.
6
+ - Expect the HTTY protocol adapter to receive a prepared `Protocol::HTTY::Stream` instance before performing bootstrap and HTTP/2 setup.
7
+
8
+ ## v0.2.1
9
+
10
+ - Send a server-side GOAWAY when the HTTY client closes an HTTP/2 session, allowing terminal clients to detach cleanly.
11
+ - Add PTY coverage for binary request/response bodies across the full byte range.
12
+
3
13
  ## v0.2.0
4
14
 
5
15
  - 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
@@ -29,11 +65,34 @@ describe Async::HTTY::Protocol::HTTY do
29
65
  end
30
66
 
31
67
  def client_stream(pipes)
32
- IO::Stream::Duplex(pipes[:client_input], pipes[:client_output])
68
+ Protocol::HTTY::Stream.new(pipes[:client_input], pipes[:client_output])
33
69
  end
34
70
 
35
71
  def server_stream(pipes)
36
- IO::Stream::Duplex(pipes[:server_input], pipes[:server_output])
72
+ Protocol::HTTY::Stream.new(pipes[:server_input], pipes[:server_output])
73
+ end
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
37
96
  end
38
97
 
39
98
  it "can carry an HTTP/2 request over HTTY bootstrap and raw transport" do
@@ -60,6 +119,97 @@ 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
+ stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
127
+ stream.read_bootstrap
128
+
129
+ framer = Protocol::HTTP2::Framer.new(stream)
130
+ client = EchoClient.new(framer)
131
+ client.send_connection_preface
132
+
133
+ request = client.create_stream
134
+ request.send_headers(
135
+ [
136
+ [":method", "POST"],
137
+ [":path", "/echo"],
138
+ [":scheme", "http"],
139
+ [":authority", "htty.local"],
140
+ ["content-length", payload.bytesize.to_s],
141
+ ["content-type", "application/octet-stream"],
142
+ ]
143
+ )
144
+ request.send_data(payload)
145
+ request.send_data(nil)
146
+
147
+ until request.closed?
148
+ client.read_frame
149
+ end
150
+
151
+ expect(request.response_headers.to_h[":status"]).to be == "200"
152
+ expect(request.body).to be == payload
153
+ ensure
154
+ client&.send_goaway
155
+ client&.close
156
+ end
157
+ end
158
+
159
+ it "returns from accept when the client sends GOAWAY while a response body is active" do
160
+ pipes = make_pipes
161
+ body = Protocol::HTTP::Body::Writable.new
162
+
163
+ Sync do |task|
164
+ request_started = Async::Notification.new
165
+ server_finished = Async::Notification.new
166
+
167
+ server = Async::HTTY::Server.for do |request|
168
+ request_started.signal
169
+ Protocol::HTTP::Response[200, {}, body]
170
+ end
171
+
172
+ server_task = task.async do
173
+ server.accept(server_stream(pipes))
174
+ ensure
175
+ server_finished.signal
176
+ end
177
+
178
+ stream = client_stream(pipes)
179
+ stream.read_bootstrap
180
+ framer = Protocol::HTTP2::Framer.new(stream)
181
+ client = Protocol::HTTP2::Client.new(framer)
182
+ client.send_connection_preface
183
+
184
+ request = client.create_stream
185
+ request.send_headers(
186
+ [[":method", "GET"], [":path", "/"], [":scheme", "http"], [":authority", "htty.local"]],
187
+ Protocol::HTTP2::END_STREAM
188
+ )
189
+
190
+ request_started.wait
191
+ client.send_goaway
192
+
193
+ task.with_timeout(1) do
194
+ server_finished.wait
195
+ end
196
+
197
+ frame = task.with_timeout(1) do
198
+ loop do
199
+ frame = framer.read_frame(client.local_settings.maximum_frame_size)
200
+ break frame if frame.is_a?(Protocol::HTTP2::GoawayFrame)
201
+ end
202
+ end
203
+
204
+ expect(frame).to be(:is_a?, Protocol::HTTP2::GoawayFrame)
205
+ ensure
206
+ body.close
207
+ client&.close
208
+ server_task&.stop
209
+ close_pipes(pipes)
210
+ end
211
+ end
212
+
63
213
  it "ignores terminal noise before the HTTY bootstrap" do
64
214
  pipes = make_pipes
65
215
 
@@ -88,8 +88,8 @@ describe Async::HTTY::Server do
88
88
 
89
89
  protocol.define_singleton_method(:server) do |stream|
90
90
  reopened_to_null = [input.reopened_to_null?, output.reopened_to_null?, error.reopened_to_null?]
91
- duplex_input = stream.io.input
92
- duplex_output = stream.io.output
91
+ duplex_input = stream.input
92
+ duplex_output = stream.output
93
93
 
94
94
  stream.write("response", flush: true)
95
95
 
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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.2'
89
+ version: '0.3'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.2'
96
+ version: '0.3'
97
97
  executables: []
98
98
  extensions: []
99
99
  extra_rdoc_files: []
@@ -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