async-htty 0.1.0 → 0.2.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
- checksums.yaml.gz.sig +0 -0
- data/lib/async/htty/server.rb +28 -4
- data/lib/async/htty/version.rb +1 -1
- data/readme.md +7 -0
- data/releases.md +7 -0
- data/test/async/htty/server.rb +107 -47
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9d30babccdd7bf9fe410add7bb9dd2a6f7dafaa4d642bb377bf431f2aea4818
|
|
4
|
+
data.tar.gz: 8cd9da81dce5256dd99c9724491f07c94217fda9efd604f468c67b61a1b4f379
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9a43f93f8c0a6a73e3114ea24ff950fcce09d49bd06752ca121e720ea8f566d220b41ee000514819bd1b537551e7ef74b0e191f3c6709e73a44083b0278e667
|
|
7
|
+
data.tar.gz: 0d9d94b0125c871ee18a5f95c98e45249e84b2397ef751f9c4b56b9b3175842b438630a998fd063fe0f848f75570c6a9f94b66ddc7044cdd99451173ff6c387d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/async/htty/server.rb
CHANGED
|
@@ -24,9 +24,8 @@ module Async
|
|
|
24
24
|
block.call
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
-
|
|
28
27
|
|
|
29
|
-
def self.open(app = nil, input: $stdin, output: $stdout, env: ENV, **options, &block)
|
|
28
|
+
def self.open(app = nil, input: $stdin, output: $stdout, error: $stderr, env: ENV, **options, &block)
|
|
30
29
|
app ||= block
|
|
31
30
|
server = self.new(app, **options)
|
|
32
31
|
|
|
@@ -37,13 +36,38 @@ module Async
|
|
|
37
36
|
$stderr.puts "HTTY is not supported by this environment, visit https://htty.dev for more information."
|
|
38
37
|
raise UnsupportedError, "HTTY is not supported by this environment"
|
|
39
38
|
end
|
|
39
|
+
|
|
40
|
+
unless input.respond_to?(:tty?) && input.tty?
|
|
41
|
+
raise UnsupportedError, "HTTY requires a TTY input stream"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
original_input = input.dup
|
|
45
|
+
original_output = output.dup
|
|
46
|
+
original_error = error.dup
|
|
47
|
+
|
|
48
|
+
stream = ::IO::Stream::Duplex(original_input, original_output)
|
|
49
|
+
input.reopen(File::NULL)
|
|
50
|
+
output.reopen(File::NULL)
|
|
51
|
+
error.reopen(File::NULL)
|
|
40
52
|
|
|
41
53
|
Sync do |task|
|
|
42
|
-
with_raw_terminal(
|
|
43
|
-
stream = ::IO::Stream::Duplex(input, output)
|
|
54
|
+
with_raw_terminal(original_input) do
|
|
44
55
|
server.accept(stream, task: task)
|
|
45
56
|
end
|
|
46
57
|
end
|
|
58
|
+
|
|
59
|
+
ensure
|
|
60
|
+
if original_input
|
|
61
|
+
input.reopen(original_input)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if original_output
|
|
65
|
+
output.reopen(original_output)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if original_error
|
|
69
|
+
error.reopen(original_error)
|
|
70
|
+
end
|
|
47
71
|
end
|
|
48
72
|
|
|
49
73
|
def initialize(app, protocol: Protocol::HTTY)
|
data/lib/async/htty/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -14,8 +14,15 @@ 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.0
|
|
18
|
+
|
|
19
|
+
- Reopen `stdin`, `stdout`, and `stderr` to null devices to prevent output from interfering with HTTY's byte stream.
|
|
20
|
+
- Guard against non-TTY input streams, which are not supported by HTTY.
|
|
21
|
+
|
|
17
22
|
### v0.1.0
|
|
18
23
|
|
|
24
|
+
- Initial implementation.
|
|
25
|
+
|
|
19
26
|
## Contributing
|
|
20
27
|
|
|
21
28
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.2.0
|
|
4
|
+
|
|
5
|
+
- Reopen `stdin`, `stdout`, and `stderr` to null devices to prevent output from interfering with HTTY's byte stream.
|
|
6
|
+
- Guard against non-TTY input streams, which are not supported by HTTY.
|
|
7
|
+
|
|
3
8
|
## v0.1.0
|
|
9
|
+
|
|
10
|
+
- Initial implementation.
|
data/test/async/htty/server.rb
CHANGED
|
@@ -3,40 +3,24 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require "stringio"
|
|
7
6
|
require "protocol/http/middleware"
|
|
8
7
|
require "async/htty"
|
|
8
|
+
require "async/htty/fake_file"
|
|
9
9
|
|
|
10
10
|
describe Async::HTTY::Server do
|
|
11
11
|
let(:server) {subject.new(Protocol::HTTP::Middleware::Okay)}
|
|
12
12
|
let(:env) {{"HTTY" => "1"}}
|
|
13
|
+
let(:error) {Async::HTTY::FakeFile.new}
|
|
13
14
|
|
|
14
15
|
it "exposes the HTTY protocol by default" do
|
|
15
16
|
expect(server.protocol).to be == Async::HTTY::Protocol::HTTY
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
it "switches tty input into raw mode while accepting a session" do
|
|
19
|
-
input =
|
|
20
|
-
output =
|
|
20
|
+
input = Async::HTTY::FakeFile.new(tty: true)
|
|
21
|
+
output = Async::HTTY::FakeFile.new
|
|
21
22
|
connection = Object.new
|
|
22
23
|
protocol = Object.new
|
|
23
|
-
input.instance_variable_set(:@raw_called, false)
|
|
24
|
-
|
|
25
|
-
def input.tty?
|
|
26
|
-
true
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def input.raw
|
|
30
|
-
@raw_called = true
|
|
31
|
-
yield
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def input.raw_called?
|
|
35
|
-
@raw_called
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def input.timeout
|
|
39
|
-
end
|
|
40
24
|
|
|
41
25
|
def connection.each
|
|
42
26
|
end
|
|
@@ -55,49 +39,113 @@ describe Async::HTTY::Server do
|
|
|
55
39
|
connection
|
|
56
40
|
end
|
|
57
41
|
|
|
58
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
|
|
42
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
59
43
|
|
|
60
|
-
expect(input
|
|
44
|
+
expect(input).to be(:raw_called?)
|
|
45
|
+
expect(input).not.to be(:raw?)
|
|
61
46
|
expect(output.string).to be == ""
|
|
62
47
|
end
|
|
63
48
|
|
|
64
49
|
it "leaves raw mode if protocol setup fails" do
|
|
65
|
-
input =
|
|
66
|
-
output =
|
|
50
|
+
input = Async::HTTY::FakeFile.new(tty: true)
|
|
51
|
+
output = Async::HTTY::FakeFile.new
|
|
67
52
|
protocol = Object.new
|
|
68
|
-
input.instance_variable_set(:@raw_exited, false)
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
protocol.define_singleton_method(:server) do |_stream|
|
|
55
|
+
raise EOFError, "aborted"
|
|
72
56
|
end
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
expect do
|
|
59
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
60
|
+
end.to raise_exception(EOFError, message: be =~ /aborted/)
|
|
61
|
+
|
|
62
|
+
expect(input).to be(:raw_exited?)
|
|
63
|
+
expect(input).not.to be(:raw?)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "reopens stdio streams while accepting and restores them afterwards" do
|
|
67
|
+
input = Async::HTTY::FakeFile.new("request", tty: true)
|
|
68
|
+
output = Async::HTTY::FakeFile.new
|
|
69
|
+
error = Async::HTTY::FakeFile.new("diagnostics")
|
|
70
|
+
connection = Object.new
|
|
71
|
+
protocol = Object.new
|
|
72
|
+
reopened_to_null = nil
|
|
73
|
+
duplex_input = nil
|
|
74
|
+
duplex_output = nil
|
|
75
|
+
|
|
76
|
+
def connection.each
|
|
78
77
|
end
|
|
79
78
|
|
|
80
|
-
def
|
|
81
|
-
|
|
79
|
+
def connection.closed?
|
|
80
|
+
false
|
|
82
81
|
end
|
|
83
82
|
|
|
84
|
-
def
|
|
83
|
+
def connection.send_goaway
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def connection.close
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
protocol.define_singleton_method(:server) do |stream|
|
|
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
|
|
93
|
+
|
|
94
|
+
stream.write("response", flush: true)
|
|
95
|
+
|
|
96
|
+
connection
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
100
|
+
|
|
101
|
+
expect(reopened_to_null).to be == [true, true, true]
|
|
102
|
+
expect(duplex_input).not.to be == input
|
|
103
|
+
expect(duplex_output).not.to be == output
|
|
104
|
+
expect(duplex_input.string).to be == "request"
|
|
105
|
+
|
|
106
|
+
expect(input).not.to be(:reopened_to_null?)
|
|
107
|
+
expect(output).not.to be(:reopened_to_null?)
|
|
108
|
+
expect(error).not.to be(:reopened_to_null?)
|
|
109
|
+
|
|
110
|
+
expect(input.reopen_events).to be == [:null, :file]
|
|
111
|
+
expect(output.reopen_events).to be == [:null, :file]
|
|
112
|
+
expect(error.reopen_events).to be == [:null, :file]
|
|
113
|
+
|
|
114
|
+
expect(input.string).to be == "request"
|
|
115
|
+
expect(output.string).to be == "response"
|
|
116
|
+
expect(error.string).to be == "diagnostics"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "restores stdio streams if protocol setup fails" do
|
|
120
|
+
input = Async::HTTY::FakeFile.new("request", tty: true)
|
|
121
|
+
output = Async::HTTY::FakeFile.new("response")
|
|
122
|
+
error = Async::HTTY::FakeFile.new("diagnostics")
|
|
123
|
+
protocol = Object.new
|
|
124
|
+
|
|
125
|
+
protocol.define_singleton_method(:server) do |_stream|
|
|
88
126
|
raise EOFError, "aborted"
|
|
89
127
|
end
|
|
90
128
|
|
|
91
129
|
expect do
|
|
92
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
|
|
130
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
93
131
|
end.to raise_exception(EOFError, message: be =~ /aborted/)
|
|
94
132
|
|
|
95
|
-
expect(input
|
|
133
|
+
expect(input).not.to be(:reopened_to_null?)
|
|
134
|
+
expect(output).not.to be(:reopened_to_null?)
|
|
135
|
+
expect(error).not.to be(:reopened_to_null?)
|
|
136
|
+
|
|
137
|
+
expect(input.reopen_events).to be == [:null, :file]
|
|
138
|
+
expect(output.reopen_events).to be == [:null, :file]
|
|
139
|
+
expect(error.reopen_events).to be == [:null, :file]
|
|
140
|
+
|
|
141
|
+
expect(input.string).to be == "request"
|
|
142
|
+
expect(output.string).to be == "response"
|
|
143
|
+
expect(error.string).to be == "diagnostics"
|
|
96
144
|
end
|
|
97
145
|
|
|
98
146
|
it "sends command-side GOAWAY before closing the connection" do
|
|
99
|
-
input =
|
|
100
|
-
output =
|
|
147
|
+
input = Async::HTTY::FakeFile.new(tty: true)
|
|
148
|
+
output = Async::HTTY::FakeFile.new
|
|
101
149
|
connection = Object.new
|
|
102
150
|
protocol = Object.new
|
|
103
151
|
events = []
|
|
@@ -121,14 +169,14 @@ describe Async::HTTY::Server do
|
|
|
121
169
|
connection
|
|
122
170
|
end
|
|
123
171
|
|
|
124
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
|
|
172
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
125
173
|
|
|
126
174
|
expect(events).to be == [:goaway, :close]
|
|
127
175
|
end
|
|
128
176
|
|
|
129
177
|
it "opens a server with default stdio-style arguments" do
|
|
130
|
-
input =
|
|
131
|
-
output =
|
|
178
|
+
input = Async::HTTY::FakeFile.new(tty: true)
|
|
179
|
+
output = Async::HTTY::FakeFile.new
|
|
132
180
|
accepted = false
|
|
133
181
|
|
|
134
182
|
server = Object.new
|
|
@@ -148,7 +196,7 @@ describe Async::HTTY::Server do
|
|
|
148
196
|
server
|
|
149
197
|
end
|
|
150
198
|
|
|
151
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
|
|
199
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
152
200
|
|
|
153
201
|
expect(accepted).to be == true
|
|
154
202
|
expect(output.string).to be == ""
|
|
@@ -156,11 +204,23 @@ describe Async::HTTY::Server do
|
|
|
156
204
|
|
|
157
205
|
it "raises a typed error when HTTY is disabled" do
|
|
158
206
|
expect do
|
|
159
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input:
|
|
207
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: Async::HTTY::FakeFile.new, output: Async::HTTY::FakeFile.new, error: error, env: {"HTTY" => "0"})
|
|
160
208
|
end.to raise_exception(Async::HTTY::DisabledError, message: be =~ /disabled/)
|
|
161
209
|
|
|
162
210
|
expect(Async::HTTY::DisabledError).to be < Async::HTTY::UnsupportedError
|
|
163
211
|
end
|
|
212
|
+
|
|
213
|
+
it "raises a typed error when stdin is not a tty" do
|
|
214
|
+
input = Async::HTTY::FakeFile.new
|
|
215
|
+
output = Async::HTTY::FakeFile.new
|
|
216
|
+
|
|
217
|
+
expect do
|
|
218
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env)
|
|
219
|
+
end.to raise_exception(Async::HTTY::UnsupportedError, message: be =~ /TTY input/)
|
|
220
|
+
|
|
221
|
+
expect(input.reopen_events).to be == []
|
|
222
|
+
expect(output.reopen_events).to be == []
|
|
223
|
+
end
|
|
164
224
|
|
|
165
225
|
it "prints help and raises a typed error when HTTY is not advertised" do
|
|
166
226
|
error_output = StringIO.new
|
|
@@ -170,7 +230,7 @@ describe Async::HTTY::Server do
|
|
|
170
230
|
$stderr = error_output
|
|
171
231
|
|
|
172
232
|
expect do
|
|
173
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input:
|
|
233
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: Async::HTTY::FakeFile.new, output: Async::HTTY::FakeFile.new, env: {})
|
|
174
234
|
end.to raise_exception(Async::HTTY::UnsupportedError, message: be =~ /not supported/)
|
|
175
235
|
ensure
|
|
176
236
|
$stderr = original_stderr
|
|
@@ -180,8 +240,8 @@ describe Async::HTTY::Server do
|
|
|
180
240
|
end
|
|
181
241
|
|
|
182
242
|
it "opens a server within its own async context when no task is provided" do
|
|
183
|
-
input =
|
|
184
|
-
output =
|
|
243
|
+
input = Async::HTTY::FakeFile.new(tty: true)
|
|
244
|
+
output = Async::HTTY::FakeFile.new
|
|
185
245
|
accepted = false
|
|
186
246
|
|
|
187
247
|
server = Object.new
|
|
@@ -201,7 +261,7 @@ describe Async::HTTY::Server do
|
|
|
201
261
|
server
|
|
202
262
|
end
|
|
203
263
|
|
|
204
|
-
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, env: env, protocol: protocol)
|
|
264
|
+
subject.open(Protocol::HTTP::Middleware::Okay, input: input, output: output, error: error, env: env, protocol: protocol)
|
|
205
265
|
|
|
206
266
|
expect(accepted).to be == true
|
|
207
267
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|