clientura 0.0.6
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 +7 -0
- data/.codeclimate.yml +29 -0
- data/.rspec +2 -0
- data/.rubocop.yml +10 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +166 -0
- data/LICENSE.txt +20 -0
- data/README.md +71 -0
- data/README.rdoc +19 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/clientura.gemspec +114 -0
- data/lib/clientura/client/endpoint.rb +5 -0
- data/lib/clientura/client/middleware_function_context.rb +21 -0
- data/lib/clientura/client/request.rb +34 -0
- data/lib/clientura/client.rb +143 -0
- data/lib/clientura/version.rb +3 -0
- data/lib/clientura.rb +11 -0
- data/spec/clientura/adequate_spec.rb +204 -0
- data/spec/clientura/client_spec.rb +175 -0
- data/spec/clientura_spec.rb +9 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/support/test_server.rb +123 -0
- metadata +309 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'clientura/client/request'
|
2
|
+
require 'clientura/client/endpoint'
|
3
|
+
require 'clientura/client/middleware_function_context'
|
4
|
+
|
5
|
+
module Clientura
|
6
|
+
module Client
|
7
|
+
def self.included(klass)
|
8
|
+
klass.extend ClassMethods
|
9
|
+
klass.include InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def registered_endpoints
|
14
|
+
@registered_endpoints ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def registered_pipes
|
18
|
+
@registered_pipes ||= {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def registered_middleware
|
22
|
+
@registered_middleware ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
[:get, :post, :put, :patch, :delete].each do |verb|
|
26
|
+
define_method verb do |name, path: nil|
|
27
|
+
register_endpoint(name, verb: verb, path: path || name.to_s)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def register_endpoint(name, verb:, path:)
|
32
|
+
registered_endpoints[name] = Endpoint.new verb,
|
33
|
+
path,
|
34
|
+
[*@middleware_context],
|
35
|
+
[*@pipes_context]
|
36
|
+
|
37
|
+
define_method name do |args = {}|
|
38
|
+
call_endpoint name, args
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method "#{name}_promise" do |args = {}|
|
42
|
+
Concurrent::Promise.execute { send(name, args) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def pipe(name, callable)
|
47
|
+
registered_pipes[name] = callable
|
48
|
+
end
|
49
|
+
|
50
|
+
def middleware(name, callable)
|
51
|
+
registered_middleware[name] = callable
|
52
|
+
end
|
53
|
+
|
54
|
+
def pipe_through(*pipes)
|
55
|
+
pipes = pipes.map { |o| normalize_mapper o }
|
56
|
+
|
57
|
+
@pipes_context ||= []
|
58
|
+
@pipes_context.push(*pipes)
|
59
|
+
yield
|
60
|
+
@pipes_context.pop pipes.count
|
61
|
+
end
|
62
|
+
|
63
|
+
def use_middleware(*middleware)
|
64
|
+
middleware = middleware.map { |o| normalize_mapper o }
|
65
|
+
|
66
|
+
@middleware_context ||= []
|
67
|
+
@middleware_context.push(*middleware)
|
68
|
+
yield
|
69
|
+
@middleware_context.pop middleware.count
|
70
|
+
end
|
71
|
+
|
72
|
+
def normalize_mapper(mapper)
|
73
|
+
case mapper
|
74
|
+
when Array
|
75
|
+
name, *config = mapper
|
76
|
+
{ name: name, config: config }
|
77
|
+
else
|
78
|
+
{ name: mapper, config: [] }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module InstanceMethods
|
84
|
+
def self.included(klass)
|
85
|
+
define_method :registered_endpoints do
|
86
|
+
klass.registered_endpoints
|
87
|
+
end
|
88
|
+
|
89
|
+
define_method :registered_pipes do
|
90
|
+
klass.registered_pipes
|
91
|
+
end
|
92
|
+
|
93
|
+
define_method :registered_middleware do
|
94
|
+
klass.registered_middleware
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_writer :config
|
99
|
+
|
100
|
+
def config
|
101
|
+
@config ||= {}
|
102
|
+
end
|
103
|
+
|
104
|
+
def save_config(args)
|
105
|
+
self.config = config.merge args
|
106
|
+
end
|
107
|
+
|
108
|
+
def call_endpoint(name_, args)
|
109
|
+
endpoint = registered_endpoints.fetch(name_)
|
110
|
+
|
111
|
+
path = if endpoint.path.respond_to?(:call)
|
112
|
+
endpoint.path.call(args)
|
113
|
+
else
|
114
|
+
endpoint.path
|
115
|
+
end
|
116
|
+
|
117
|
+
middlewares = endpoint.middleware.map do |name:, config:|
|
118
|
+
{ callable: registered_middleware.fetch(name), config: config }
|
119
|
+
end
|
120
|
+
|
121
|
+
request = middlewares
|
122
|
+
.reduce Request.new do |request_, callable:, config:|
|
123
|
+
middleware = MiddlewareFunctionContext.new(request: request_,
|
124
|
+
client: self,
|
125
|
+
args: args,
|
126
|
+
callable: callable,
|
127
|
+
config: config)
|
128
|
+
middleware.call
|
129
|
+
end
|
130
|
+
|
131
|
+
response = request.send(endpoint.verb, path)
|
132
|
+
|
133
|
+
endpoints = endpoint.pipes.map do |name:, config:|
|
134
|
+
-> (res) { registered_pipes.fetch(name).call(res, *config) }
|
135
|
+
end
|
136
|
+
|
137
|
+
endpoints.reduce response do |response_, pipe|
|
138
|
+
pipe.call response_
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/clientura.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
describe 'Ability to use this for solving real world problems' do
|
2
|
+
# Purpose of this spec is to define some real world use cases in a clean and
|
3
|
+
# readable manner to serve as an example of usage.
|
4
|
+
|
5
|
+
subject { instance }
|
6
|
+
|
7
|
+
let(:instance) { client_class.new(client_config) }
|
8
|
+
let(:client_config) { { uri: 'http://localhost:3001/real_world/' } }
|
9
|
+
let(:client_class) do
|
10
|
+
Class.new do
|
11
|
+
include Clientura::Client
|
12
|
+
|
13
|
+
middleware :configurable_uri, -> { update(:uri) { client.config[:uri] } }
|
14
|
+
middleware :header_with_token, -> { headers AuthToken: client.config[:token] }
|
15
|
+
middleware :pass_all_as_query_string, -> { update(:params) { args } }
|
16
|
+
middleware :namespace, -> (namespace) { update(:uri) { |uri| URI.join uri, namespace } }
|
17
|
+
middleware :bad_middleware, -> { raise 'Some Exception' }
|
18
|
+
middleware :slow_middleware, -> { sleep 0.1; self }
|
19
|
+
|
20
|
+
pipe :body_retriever, -> (res) { res.body.to_s }
|
21
|
+
pipe :parser, -> (res, parser) { parser.parse res }
|
22
|
+
pipe :answer_header, -> (res) { res.headers['Answer'] }
|
23
|
+
pipe :data_retriever, -> (res) { res['data'] }
|
24
|
+
|
25
|
+
use_middleware :configurable_uri do
|
26
|
+
get :root, path: '/'
|
27
|
+
|
28
|
+
use_middleware :header_with_token do
|
29
|
+
get :pass_token
|
30
|
+
end
|
31
|
+
|
32
|
+
pipe_through :body_retriever do
|
33
|
+
get :get_some, path: 'some'
|
34
|
+
put :put_some, path: 'some'
|
35
|
+
patch :patch_some, path: 'some'
|
36
|
+
post :post_some, path: 'some'
|
37
|
+
delete :delete_some, path: 'some'
|
38
|
+
|
39
|
+
pipe_through [:parser, JSON] do
|
40
|
+
get :parse_response
|
41
|
+
|
42
|
+
pipe_through :data_retriever do
|
43
|
+
get :comments, path: -> (params) { "comments/#{params[:id]}" }
|
44
|
+
get :users, path: -> (params) { "users/#{params[:id]}" }
|
45
|
+
get :attachments, path: -> (params) { "attachments/#{params[:id]}" }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
use_middleware :pass_all_as_query_string do
|
50
|
+
get :pass_query_string
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
use_middleware [:namespace, 'namespace/'] do
|
55
|
+
get :namespaced
|
56
|
+
end
|
57
|
+
|
58
|
+
pipe_through :answer_header do
|
59
|
+
get :get_answer_header
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
use_middleware :bad_middleware do
|
64
|
+
get :raise_some_exception
|
65
|
+
end
|
66
|
+
|
67
|
+
use_middleware :slow_middleware, :bad_middleware do
|
68
|
+
get :slowly_raise_some_exception
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(uri:, token: nil)
|
72
|
+
save_config uri: uri, token: token
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'My wish to see this at least working' do
|
78
|
+
subject { super().root.status }
|
79
|
+
|
80
|
+
it { should eq 200 }
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'My desire to use different verbs' do
|
84
|
+
describe 'GET' do
|
85
|
+
subject { super().get_some }
|
86
|
+
|
87
|
+
it { should eq 'get' }
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'POST' do
|
91
|
+
subject { super().post_some }
|
92
|
+
|
93
|
+
it { should eq 'post' }
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'PUT' do
|
97
|
+
subject { super().put_some }
|
98
|
+
|
99
|
+
it { should eq 'put' }
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'PATCH' do
|
103
|
+
subject { super().patch_some }
|
104
|
+
|
105
|
+
it { should eq 'patch' }
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'DELETE' do
|
109
|
+
subject { super().delete_some }
|
110
|
+
|
111
|
+
it { should eq 'delete' }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe 'My desire to see some exceptions' do
|
116
|
+
subject { -> { instance.raise_some_exception } }
|
117
|
+
|
118
|
+
it { should raise_error 'Some Exception' }
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'My desire to work with promise' do
|
122
|
+
context 'when no errors raised' do
|
123
|
+
subject { super().root_promise }
|
124
|
+
|
125
|
+
it { should be_pending }
|
126
|
+
|
127
|
+
it 'has correct value' do
|
128
|
+
expect(subject.value.status).to eq 200
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'when awaited' do
|
132
|
+
before { subject.value }
|
133
|
+
|
134
|
+
it 'fulfills' do
|
135
|
+
expect(subject).to be_fulfilled
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'when error raised' do
|
141
|
+
subject { super().slowly_raise_some_exception_promise }
|
142
|
+
|
143
|
+
it { should be_pending }
|
144
|
+
|
145
|
+
it 'has nil value' do
|
146
|
+
expect(subject.value).to be nil
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'when awaited' do
|
150
|
+
before { subject.value }
|
151
|
+
|
152
|
+
it 'rejects' do
|
153
|
+
expect(subject).to be_rejected
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'has correct message' do
|
157
|
+
expect(subject.reason.message).to eq 'Some Exception'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe 'My desire to pass some token in header' do
|
164
|
+
subject { super().pass_token.status }
|
165
|
+
|
166
|
+
let(:client_config) { super().merge token: token }
|
167
|
+
|
168
|
+
context 'when token is bad' do
|
169
|
+
let(:token) { 'Bad token!' }
|
170
|
+
|
171
|
+
it { should eq 403 }
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'when token is good' do
|
175
|
+
let(:token) { 'Secret' }
|
176
|
+
|
177
|
+
it { should eq 200 }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe 'My desire to parse response' do
|
182
|
+
subject { super().parse_response }
|
183
|
+
|
184
|
+
it { should eq 'data' => 'Awesome!' }
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'My desire to pass query string' do
|
188
|
+
subject { super().pass_query_string echo: 'Awesome!' }
|
189
|
+
|
190
|
+
it { should eq 'Awesome!' }
|
191
|
+
end
|
192
|
+
|
193
|
+
describe 'My desire to get response header' do
|
194
|
+
subject { super().get_answer_header }
|
195
|
+
|
196
|
+
it { should eq 'Awesome!' }
|
197
|
+
end
|
198
|
+
|
199
|
+
describe 'My desire to namespace routes' do
|
200
|
+
subject { super().namespaced.status }
|
201
|
+
|
202
|
+
it { should eq 200 }
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
describe Clientura::Client do
|
2
|
+
subject { client }
|
3
|
+
|
4
|
+
let(:uri) { 'http://localhost:3001' }
|
5
|
+
let(:client) { klass.new(uri: uri, token: 'InitializationToken') }
|
6
|
+
let(:klass) do
|
7
|
+
Class.new do
|
8
|
+
include Clientura::Client
|
9
|
+
|
10
|
+
middleware :static_token, -> (token) { headers(Token: token) }
|
11
|
+
middleware :init_token, -> { headers(token: client.config[:token]) }
|
12
|
+
middleware :token_passer, -> { headers(token: args[:token]) }
|
13
|
+
middleware :send_as_json, -> { update(:json) { args } }
|
14
|
+
middleware :pass_as_query, -> { update(:params) { |p| p.merge args } }
|
15
|
+
middleware :configurable_uri, lambda {
|
16
|
+
update(:uri) { |uri| URI.join client.config[:uri], uri }
|
17
|
+
}
|
18
|
+
|
19
|
+
pipe :body_retriever, -> (res) { res.body.to_s }
|
20
|
+
pipe :parser, -> (res, parser) { parser.parse res }
|
21
|
+
pipe :data_retriever, -> (res) { res['data'] }
|
22
|
+
|
23
|
+
use_middleware :configurable_uri do
|
24
|
+
pipe_through :body_retriever do
|
25
|
+
get :resource_raw, path: 'res'
|
26
|
+
|
27
|
+
pipe_through [:parser, JSON] do
|
28
|
+
get :root, path: '/'
|
29
|
+
get :resource, path: 'res'
|
30
|
+
get :name
|
31
|
+
get :echo_argument, path: -> (params) { "echo_argument/#{params[:argument]}" }
|
32
|
+
|
33
|
+
use_middleware :pass_as_query do
|
34
|
+
get :echo_param
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
pipe_through :body_retriever, [:parser, JSON] do
|
40
|
+
use_middleware [:static_token, 'StaticToken'] do
|
41
|
+
get :try_static_token, path: 'echo_header'
|
42
|
+
end
|
43
|
+
|
44
|
+
use_middleware [:static_token, 'Token'] do
|
45
|
+
get :echo_header_const, path: 'echo_header'
|
46
|
+
end
|
47
|
+
|
48
|
+
use_middleware :token_passer do
|
49
|
+
get :try_token_passer, path: 'echo_header'
|
50
|
+
get :echo_header
|
51
|
+
end
|
52
|
+
|
53
|
+
use_middleware :init_token do
|
54
|
+
get :try_init_token, path: 'echo_header'
|
55
|
+
end
|
56
|
+
|
57
|
+
get :configurable_uri_resource, path: 'res'
|
58
|
+
|
59
|
+
use_middleware :send_as_json do
|
60
|
+
post :send_json, path: 'data'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
use_middleware :configurable_uri do
|
66
|
+
pipe_through :body_retriever, [:parser, JSON], :data_retriever do
|
67
|
+
use_middleware :send_as_json do
|
68
|
+
post :sum
|
69
|
+
end
|
70
|
+
|
71
|
+
get :comments, path: -> (params) { "comments/#{params[:id]}" }
|
72
|
+
get :users, path: -> (params) { "users/#{params[:id]}" }
|
73
|
+
get :tags, path: -> (params) { "tags/#{params[:id]}" }
|
74
|
+
|
75
|
+
use_middleware :pass_as_query do
|
76
|
+
get :left_operand
|
77
|
+
get :right_operand
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(uri:, token:)
|
83
|
+
save_config uri: URI.parse(uri), token: token
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#root' do
|
89
|
+
subject { client.root }
|
90
|
+
|
91
|
+
it { should eq 'data' => 'root' }
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#resource' do
|
95
|
+
subject { client.resource }
|
96
|
+
|
97
|
+
it { should eq 'data' => 'resource' }
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#name' do
|
101
|
+
subject { client.name }
|
102
|
+
|
103
|
+
it { should eq 'data' => 'name' }
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#echo_argument' do
|
107
|
+
subject { client.echo_argument argument: 'Argument' }
|
108
|
+
|
109
|
+
it { should eq 'data' => 'Argument' }
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#echo_param' do
|
113
|
+
subject { client.echo_param param: 'Param' }
|
114
|
+
|
115
|
+
it { should eq 'data' => 'Param' }
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#echo_header' do
|
119
|
+
subject { client.echo_header token: 'Token' }
|
120
|
+
|
121
|
+
it { should eq 'data' => 'Token' }
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#echo_header_const' do
|
125
|
+
subject { client.echo_header_const }
|
126
|
+
|
127
|
+
it { should eq 'data' => 'Token' }
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#resource_raw' do
|
131
|
+
subject { client.resource_raw }
|
132
|
+
|
133
|
+
it { should eq JSON.dump data: 'resource' }
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'try_static_token' do
|
137
|
+
subject { client.try_static_token }
|
138
|
+
|
139
|
+
it { should eq 'data' => 'StaticToken' }
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'try_token_passer' do
|
143
|
+
subject { client.try_token_passer token: 'PassedToken' }
|
144
|
+
|
145
|
+
it { should eq 'data' => 'PassedToken' }
|
146
|
+
end
|
147
|
+
|
148
|
+
describe 'try_init_token' do
|
149
|
+
subject { client.try_init_token }
|
150
|
+
|
151
|
+
it { should eq 'data' => 'InitializationToken' }
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'configurable_uri_resource' do
|
155
|
+
subject { client.configurable_uri_resource }
|
156
|
+
|
157
|
+
it { should eq 'data' => 'resource' }
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#send_json' do
|
161
|
+
subject { client.send_json json_data: { foo: :bar } }
|
162
|
+
|
163
|
+
it { should eq 'data' => { 'foo' => 'bar' } }
|
164
|
+
end
|
165
|
+
|
166
|
+
describe '#sum' do
|
167
|
+
subject { client.sum sum: sum }
|
168
|
+
|
169
|
+
let(:sum) { client.left_operand(key: 'Secret') + client.right_operand(key: 'Secret') }
|
170
|
+
|
171
|
+
it { should eq true }
|
172
|
+
end
|
173
|
+
|
174
|
+
# it('__run_server__', :focus) { binding.pry }
|
175
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'codeclimate-test-reporter'
|
3
|
+
|
4
|
+
module SimpleCov::Configuration
|
5
|
+
def clean_filters
|
6
|
+
@filters = []
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
SimpleCov.configure do
|
11
|
+
clean_filters
|
12
|
+
load_profile 'test_frameworks'
|
13
|
+
end
|
14
|
+
|
15
|
+
ENV['COVERAGE'] && SimpleCov.start do
|
16
|
+
add_filter '/.rvm/'
|
17
|
+
end
|
18
|
+
|
19
|
+
CodeClimate::TestReporter.start
|
20
|
+
|
21
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
22
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
23
|
+
|
24
|
+
require 'bundler'
|
25
|
+
Bundler.require(:test)
|
26
|
+
require 'clientura'
|
27
|
+
|
28
|
+
# Requires supporting files with custom matchers and macros, etc,
|
29
|
+
# in ./support/ and its subdirectories.
|
30
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
31
|
+
|
32
|
+
RSpec.configure do |config|
|
33
|
+
# config.expose_current_running_example_as :example
|
34
|
+
config.raise_errors_for_deprecations!
|
35
|
+
config.run_all_when_everything_filtered = true
|
36
|
+
config.filter_run focus: true
|
37
|
+
# config.mock_with :rspec
|
38
|
+
# config.use_transactional_fixtures = false
|
39
|
+
|
40
|
+
config.order = :rand
|
41
|
+
|
42
|
+
config.before(:suite) do |_example|
|
43
|
+
pid = Process.fork do
|
44
|
+
Rack::Handler::Thin.run TestServer, Port: 3001
|
45
|
+
# trap(:INT) { Rack::Handler::WEBrick.shutdown }
|
46
|
+
# Rack::Handler::WEBrick.run TestServer.new,
|
47
|
+
# Port: 3001,
|
48
|
+
# Logger: WEBrick::Log.new('/dev/null'),
|
49
|
+
# AccessLog: []
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
|
53
|
+
at_exit do
|
54
|
+
Process.kill('INT', pid)
|
55
|
+
begin
|
56
|
+
Process.wait(pid)
|
57
|
+
rescue Errno::ECHILD
|
58
|
+
# ignore this error...I think it means the child process has already exited.
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
sleep 1
|
63
|
+
end
|
64
|
+
end
|