async-http 0.18.0 → 0.19.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/async-http.gemspec +1 -1
- data/lib/async/http/accept_encoding.rb +65 -0
- data/lib/async/http/body.rb +2 -244
- data/lib/async/http/body/buffered.rb +107 -0
- data/lib/async/http/body/chunked.rb +67 -0
- data/lib/async/http/body/deflate.rb +106 -0
- data/lib/async/http/body/fixed.rb +83 -0
- data/lib/async/http/body/inflate.rb +55 -0
- data/lib/async/http/body/readable.rb +73 -0
- data/lib/async/http/body/streamable.rb +52 -0
- data/lib/async/http/body/wrapper.rb +58 -0
- data/lib/async/http/body/writable.rb +81 -0
- data/lib/async/http/client.rb +14 -19
- data/lib/async/http/content_encoding.rb +71 -0
- data/lib/async/http/middleware.rb +55 -0
- data/lib/async/http/middleware/builder.rb +50 -0
- data/lib/async/http/protocol/http10.rb +13 -11
- data/lib/async/http/protocol/http11.rb +24 -14
- data/lib/async/http/protocol/http2.rb +62 -48
- data/lib/async/http/reference.rb +173 -0
- data/lib/async/http/relative_location.rb +75 -0
- data/lib/async/http/request.rb +8 -2
- data/lib/async/http/response.rb +12 -2
- data/lib/async/http/server.rb +4 -2
- data/lib/async/http/statistics.rb +131 -0
- data/lib/async/http/url_endpoint.rb +17 -1
- data/lib/async/http/version.rb +1 -1
- metadata +20 -5
- data/lib/async/http/deflate_body.rb +0 -124
data/lib/async/http/client.rb
CHANGED
@@ -21,6 +21,8 @@
|
|
21
21
|
require 'async/io/endpoint'
|
22
22
|
|
23
23
|
require_relative 'protocol'
|
24
|
+
require_relative 'body/streamable'
|
25
|
+
require_relative 'middleware'
|
24
26
|
|
25
27
|
module Async
|
26
28
|
module HTTP
|
@@ -54,27 +56,20 @@ module Async
|
|
54
56
|
@connections.close
|
55
57
|
end
|
56
58
|
|
57
|
-
|
59
|
+
include Verbs
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
begin
|
70
|
-
return yield response if block_given?
|
71
|
-
ensure
|
72
|
-
# This forces the stream to complete reading.
|
73
|
-
response.finish
|
74
|
-
end
|
75
|
-
|
76
|
-
return response
|
61
|
+
def call(request)
|
62
|
+
connection = @connections.acquire
|
63
|
+
|
64
|
+
request.authority ||= @authority
|
65
|
+
response = connection.call(request)
|
66
|
+
|
67
|
+
# The connection won't be released until the body is completely read/released.
|
68
|
+
Body::Streamable.wrap(response) do
|
69
|
+
@connections.release(connection)
|
77
70
|
end
|
71
|
+
|
72
|
+
return response
|
78
73
|
end
|
79
74
|
|
80
75
|
protected
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'middleware'
|
22
|
+
|
23
|
+
require_relative 'body/buffered'
|
24
|
+
require_relative 'body/deflate'
|
25
|
+
|
26
|
+
module Async
|
27
|
+
module HTTP
|
28
|
+
# Encode a response according the the request's acceptable encodings.
|
29
|
+
class ContentEncoding < Middleware
|
30
|
+
DEFAULT_WRAPPERS = {
|
31
|
+
'gzip' => Body::Deflate.method(:for)
|
32
|
+
}
|
33
|
+
|
34
|
+
DEFAULT_CONTENT_TYPES = %r{^(text/.*?)|(.*?/json)|(.*?/javascript)$}
|
35
|
+
|
36
|
+
def initialize(app, content_types = DEFAULT_CONTENT_TYPES, wrappers = DEFAULT_WRAPPERS)
|
37
|
+
super(app)
|
38
|
+
|
39
|
+
@content_types = content_types
|
40
|
+
@wrappers = wrappers
|
41
|
+
end
|
42
|
+
|
43
|
+
def call(request, *)
|
44
|
+
response = super
|
45
|
+
|
46
|
+
if !response.body.empty? and accept_encoding = request.headers['accept-encoding']
|
47
|
+
if content_type = response.headers['content-type'] and @content_types.match?(content_type)
|
48
|
+
# TODO use http-accept and sort by priority
|
49
|
+
encodings = accept_encoding.split(/\s*,\s*/)
|
50
|
+
|
51
|
+
body = response.body
|
52
|
+
|
53
|
+
encodings.each do |name|
|
54
|
+
if wrapper = @wrappers[name]
|
55
|
+
response.headers['content-encoding'] = name
|
56
|
+
|
57
|
+
body = wrapper.call(body)
|
58
|
+
|
59
|
+
break
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
response.body = body
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return response
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Async
|
22
|
+
module HTTP
|
23
|
+
VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
|
24
|
+
|
25
|
+
module Verbs
|
26
|
+
VERBS.each do |verb|
|
27
|
+
define_method(verb.downcase) do |location, headers = {}, body = []|
|
28
|
+
self.call(Request[verb, location.to_str, headers, body])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Middleware
|
34
|
+
def initialize(app)
|
35
|
+
@app = app
|
36
|
+
end
|
37
|
+
|
38
|
+
def close
|
39
|
+
@app.close
|
40
|
+
end
|
41
|
+
|
42
|
+
include Verbs
|
43
|
+
|
44
|
+
def call(*args)
|
45
|
+
@app.call(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
module Okay
|
49
|
+
def self.call
|
50
|
+
Response.local(200, {}, [])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Async
|
22
|
+
module HTTP
|
23
|
+
class Middleware
|
24
|
+
class Builder
|
25
|
+
def initialize(default_app = nil, &block)
|
26
|
+
@use = []
|
27
|
+
@run = default_app
|
28
|
+
|
29
|
+
instance_eval(&block) if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def use(middleware, *args, &block)
|
33
|
+
@use << proc {|app| middleware.new(app, *args, &block)}
|
34
|
+
end
|
35
|
+
|
36
|
+
def run(app)
|
37
|
+
@app = app
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_app
|
41
|
+
app = @use.reverse.inject(app) {|app, use| use.call(app).freeze}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.build(&block)
|
46
|
+
Builder.new(&block).to_app
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -38,9 +38,11 @@ module Async
|
|
38
38
|
# Server loop.
|
39
39
|
def receive_requests
|
40
40
|
while request = Request.new(*self.read_request)
|
41
|
-
|
41
|
+
response = yield request
|
42
42
|
|
43
|
-
|
43
|
+
response.version ||= request.version
|
44
|
+
|
45
|
+
write_response(response.version, response.status, response.headers, response.body)
|
44
46
|
|
45
47
|
unless keep_alive?(request.headers) && keep_alive?(headers)
|
46
48
|
@keep_alive = false
|
@@ -53,18 +55,18 @@ module Async
|
|
53
55
|
end
|
54
56
|
|
55
57
|
def write_body(body, chunked = true)
|
56
|
-
|
57
|
-
body
|
58
|
-
|
59
|
-
@stream.write("Content-Length: #{buffer.bytesize}\r\n\r\n")
|
60
|
-
@stream.write(buffer)
|
58
|
+
# We don't support chunked encoding.
|
59
|
+
super(body, false)
|
61
60
|
end
|
62
61
|
|
63
62
|
def read_body(headers)
|
64
|
-
if
|
65
|
-
return
|
66
|
-
|
67
|
-
|
63
|
+
if body = super
|
64
|
+
return body
|
65
|
+
end
|
66
|
+
|
67
|
+
# Technically, with HTTP/1.0, if no content-length is specified, we just need to read everything until the connection is closed.
|
68
|
+
if !keep_alive?(headers)
|
69
|
+
return Body::Remainder.new(@stream)
|
68
70
|
end
|
69
71
|
end
|
70
72
|
end
|
@@ -23,6 +23,9 @@ require 'async/io/protocol/line'
|
|
23
23
|
require_relative '../request'
|
24
24
|
require_relative '../response'
|
25
25
|
|
26
|
+
require_relative '../body/chunked'
|
27
|
+
require_relative '../body/fixed'
|
28
|
+
|
26
29
|
module Async
|
27
30
|
module HTTP
|
28
31
|
module Protocol
|
@@ -68,13 +71,15 @@ module Async
|
|
68
71
|
while true
|
69
72
|
request = Request.new(*read_request)
|
70
73
|
|
71
|
-
|
74
|
+
response = yield request
|
75
|
+
|
76
|
+
response.version ||= request.version
|
72
77
|
|
73
|
-
write_response(
|
78
|
+
write_response(response.version, response.status, response.headers, response.body)
|
74
79
|
|
75
80
|
request.finish
|
76
81
|
|
77
|
-
unless keep_alive?(request.headers) and keep_alive?(headers)
|
82
|
+
unless keep_alive?(request.headers) and keep_alive?(response.headers)
|
78
83
|
@keep_alive = false
|
79
84
|
|
80
85
|
break
|
@@ -85,11 +90,11 @@ module Async
|
|
85
90
|
end
|
86
91
|
end
|
87
92
|
|
88
|
-
|
89
|
-
|
90
|
-
Async.logger.debug(self) {"#{method} #{path} #{headers.inspect}"}
|
93
|
+
def call(request)
|
94
|
+
request.version ||= self.version
|
91
95
|
|
92
|
-
|
96
|
+
Async.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
|
97
|
+
write_request(request.authority, request.method, request.path, request.version, request.headers, request.body)
|
93
98
|
|
94
99
|
return Response.new(*read_response)
|
95
100
|
rescue EOFError
|
@@ -158,7 +163,10 @@ module Async
|
|
158
163
|
end
|
159
164
|
|
160
165
|
def write_body(body, chunked = true)
|
161
|
-
if
|
166
|
+
if body.empty?
|
167
|
+
@stream.write("Content-Length: 0\r\n\r\n")
|
168
|
+
body.read
|
169
|
+
elsif chunked
|
162
170
|
@stream.write("Transfer-Encoding: chunked\r\n\r\n")
|
163
171
|
|
164
172
|
body.each do |chunk|
|
@@ -172,19 +180,21 @@ module Async
|
|
172
180
|
|
173
181
|
@stream.write("0\r\n\r\n")
|
174
182
|
else
|
175
|
-
|
176
|
-
|
183
|
+
body = Body::Buffered.for(body)
|
184
|
+
|
185
|
+
@stream.write("Content-Length: #{body.bytesize}\r\n\r\n")
|
177
186
|
|
178
|
-
|
179
|
-
|
187
|
+
body.each do |chunk|
|
188
|
+
@stream.write(chunk)
|
189
|
+
end
|
180
190
|
end
|
181
191
|
end
|
182
192
|
|
183
193
|
def read_body(headers)
|
184
194
|
if headers['transfer-encoding'] == 'chunked'
|
185
|
-
return
|
195
|
+
return Body::Chunked.new(self)
|
186
196
|
elsif content_length = headers['content-length']
|
187
|
-
return
|
197
|
+
return Body::Fixed.new(@stream, Integer(content_length))
|
188
198
|
end
|
189
199
|
end
|
190
200
|
end
|
@@ -20,6 +20,7 @@
|
|
20
20
|
|
21
21
|
require_relative '../request'
|
22
22
|
require_relative '../response'
|
23
|
+
require_relative '../body/writable'
|
23
24
|
|
24
25
|
require 'async/notification'
|
25
26
|
|
@@ -63,12 +64,6 @@ module Async
|
|
63
64
|
@controller.on(:frame_received) do |frame|
|
64
65
|
Async.logger.debug(self) {"Received frame: #{frame.inspect}"}
|
65
66
|
end
|
66
|
-
|
67
|
-
if @controller.is_a? ::HTTP2::Client
|
68
|
-
@controller.send_connection_preface
|
69
|
-
end
|
70
|
-
|
71
|
-
@reader = read_in_background
|
72
67
|
end
|
73
68
|
|
74
69
|
# Multiple requests can be processed at the same time.
|
@@ -80,6 +75,14 @@ module Async
|
|
80
75
|
@reader.alive?
|
81
76
|
end
|
82
77
|
|
78
|
+
def version
|
79
|
+
VERSION
|
80
|
+
end
|
81
|
+
|
82
|
+
def start_connection
|
83
|
+
@reader ||= read_in_background
|
84
|
+
end
|
85
|
+
|
83
86
|
def read_in_background(task: Task.current)
|
84
87
|
task.async do |nested_task|
|
85
88
|
buffer = Async::IO::BinaryString.new
|
@@ -102,11 +105,13 @@ module Async
|
|
102
105
|
# emits new streams opened by the client
|
103
106
|
@controller.on(:stream) do |stream|
|
104
107
|
request = Request.new
|
105
|
-
request.version =
|
108
|
+
request.version = self.version
|
106
109
|
request.headers = {}
|
107
|
-
|
110
|
+
body = Body::Writable.new
|
111
|
+
request.body = body
|
108
112
|
|
109
113
|
stream.on(:headers) do |headers|
|
114
|
+
# puts "Got request headers: #{headers.inspect}"
|
110
115
|
headers.each do |key, value|
|
111
116
|
if key == METHOD
|
112
117
|
request.method = value
|
@@ -121,63 +126,66 @@ module Async
|
|
121
126
|
end
|
122
127
|
|
123
128
|
stream.on(:data) do |chunk|
|
124
|
-
request
|
129
|
+
# puts "Got request data: #{chunk.inspect}"
|
130
|
+
body.write(chunk.to_s) unless chunk.empty?
|
125
131
|
end
|
126
132
|
|
127
133
|
stream.on(:half_close) do
|
134
|
+
# puts "Generating response..."
|
128
135
|
response = yield request
|
129
136
|
|
130
|
-
|
137
|
+
# puts "Finishing body..."
|
138
|
+
body.finish
|
131
139
|
|
140
|
+
# puts "Sending response..."
|
132
141
|
# send response
|
133
|
-
headers = {STATUS => response
|
134
|
-
headers.update(response
|
142
|
+
headers = {STATUS => response.status.to_s}
|
143
|
+
headers.update(response.headers)
|
135
144
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
145
|
+
# puts "Sending headers #{headers}"
|
146
|
+
if response.body.empty?
|
147
|
+
stream.headers(headers, end_stream: true)
|
148
|
+
response.body.read
|
149
|
+
else
|
150
|
+
stream.headers(headers, end_stream: false)
|
151
|
+
|
152
|
+
# puts "Streaming body..."
|
153
|
+
response.body.each do |chunk|
|
154
|
+
# puts "Sending chunk #{chunk.inspect}"
|
155
|
+
stream.data(chunk, end_stream: false)
|
156
|
+
end
|
157
|
+
|
158
|
+
# puts "Ending stream..."
|
159
|
+
stream.data("", end_stream: true)
|
140
160
|
end
|
141
|
-
|
142
|
-
stream.data("", end_stream: true)
|
143
161
|
end
|
144
162
|
end
|
145
163
|
|
164
|
+
start_connection
|
146
165
|
@reader.wait
|
147
166
|
end
|
148
167
|
|
149
|
-
|
150
|
-
|
151
|
-
|
168
|
+
def call(request)
|
169
|
+
request.version ||= self.version
|
170
|
+
|
152
171
|
stream = @controller.new_stream
|
153
172
|
|
154
|
-
|
173
|
+
headers = {
|
155
174
|
SCHEME => HTTPS,
|
156
|
-
METHOD => method,
|
157
|
-
PATH => path,
|
158
|
-
AUTHORITY => authority,
|
159
|
-
}.merge(headers)
|
160
|
-
|
161
|
-
stream.headers(internal_headers, end_stream: body.nil?)
|
162
|
-
|
163
|
-
if body
|
164
|
-
body.each do |chunk|
|
165
|
-
stream.data(chunk, end_stream: false)
|
166
|
-
end
|
167
|
-
|
168
|
-
stream.data("", end_stream: true)
|
169
|
-
end
|
175
|
+
METHOD => request.method.to_s,
|
176
|
+
PATH => request.path.to_s,
|
177
|
+
AUTHORITY => request.authority.to_s,
|
178
|
+
}.merge(request.headers)
|
170
179
|
|
171
180
|
finished = Async::Notification.new
|
172
181
|
|
173
182
|
response = Response.new
|
174
|
-
response.version =
|
183
|
+
response.version = self.version
|
175
184
|
response.headers = {}
|
176
|
-
|
185
|
+
body = Body::Writable.new
|
186
|
+
response.body = body
|
177
187
|
|
178
188
|
stream.on(:headers) do |headers|
|
179
|
-
# Async.logger.debug(self) {"Stream headers: #{headers.inspect}"}
|
180
|
-
|
181
189
|
headers.each do |key, value|
|
182
190
|
if key == STATUS
|
183
191
|
response.status = value.to_i
|
@@ -192,20 +200,26 @@ module Async
|
|
192
200
|
end
|
193
201
|
|
194
202
|
stream.on(:data) do |chunk|
|
195
|
-
|
196
|
-
response.body.write(chunk.to_s) unless chunk.empty?
|
203
|
+
body.write(chunk.to_s) unless chunk.empty?
|
197
204
|
end
|
198
205
|
|
199
|
-
stream.on(:
|
200
|
-
|
206
|
+
stream.on(:close) do
|
207
|
+
body.finish
|
201
208
|
end
|
202
209
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
210
|
+
if request.body.empty?
|
211
|
+
stream.headers(headers, end_stream: true)
|
212
|
+
else
|
213
|
+
stream.headers(headers, end_stream: false)
|
214
|
+
|
215
|
+
request.body.each do |chunk|
|
216
|
+
stream.data(chunk, end_stream: false)
|
217
|
+
end
|
218
|
+
|
219
|
+
stream.data("", end_stream: true)
|
207
220
|
end
|
208
221
|
|
222
|
+
start_connection
|
209
223
|
@stream.flush
|
210
224
|
|
211
225
|
# Async.logger.debug(self) {"Stream flushed, waiting for signal."}
|