blob: 67877a191fc96505f5040cd826568235921a439a [file] [log] [blame]
Tim Emiolaf4ee9612015-08-14 18:47:16 -07001#!/usr/bin/env ruby
2
3# Copyright 2015, Google Inc.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are
8# met:
9#
10# * Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12# * Redistributions in binary form must reproduce the above
13# copyright notice, this list of conditions and the following disclaimer
14# in the documentation and/or other materials provided with the
15# distribution.
16# * Neither the name of Google Inc. nor the names of its
17# contributors may be used to endorse or promote products derived from
18# this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32# interop_server is a Testing app that runs a gRPC interop testing server.
33#
34# It helps validate interoperation b/w gRPC in different environments
35#
36# Helps validate interoperation b/w different gRPC implementations.
37#
38# Usage: $ path/to/interop_server.rb --port
39
40this_dir = File.expand_path(File.dirname(__FILE__))
41lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
42pb_dir = File.dirname(File.dirname(this_dir))
43$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
44$LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
45$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
46
47require 'forwardable'
Tim Emiola69a672e2015-11-10 14:59:15 -080048require 'logger'
Tim Emiolaf4ee9612015-08-14 18:47:16 -070049require 'optparse'
50
51require 'grpc'
52
53require 'test/proto/empty'
54require 'test/proto/messages'
55require 'test/proto/test_services'
56
Tim Emiola69a672e2015-11-10 14:59:15 -080057# DebugIsTruncated extends the default Logger to truncate debug messages
58class DebugIsTruncated < Logger
59 def debug(s)
60 super(truncate(s, 1024))
61 end
62
63 # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
64 #
65 # 'Once upon a time in a world far far away'.truncate(27)
66 # # => "Once upon a time in a wo..."
67 #
68 # Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
69 #
70 # 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
71 # # => "Once upon a time in a..."
72 #
73 # 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
74 # # => "Once upon a time in a..."
75 #
76 # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
77 # for a total length not exceeding <tt>length</tt>:
78 #
79 # 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
80 # # => "And they f... (continued)"
81 def truncate(s, truncate_at, options = {})
82 return s unless s.length > truncate_at
83 omission = options[:omission] || '...'
84 with_extra_room = truncate_at - omission.length
85 stop = \
86 if options[:separator]
87 rindex(options[:separator], with_extra_room) || with_extra_room
88 else
89 with_extra_room
90 end
91 "#{s[0, stop]}#{omission}"
92 end
93end
94
95# RubyLogger defines a logger for gRPC based on the standard ruby logger.
96module RubyLogger
97 def logger
98 LOGGER
99 end
100
101 LOGGER = DebugIsTruncated.new(STDOUT)
Tim Emiolaa0824352015-11-11 13:43:16 -0800102 LOGGER.level = Logger::WARN
Tim Emiola69a672e2015-11-10 14:59:15 -0800103end
104
105# GRPC is the general RPC module
106module GRPC
107 # Inject the noop #logger if no module-level logger method has been injected.
108 extend RubyLogger
109end
110
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700111# loads the certificates by the test server.
112def load_test_certs
113 this_dir = File.expand_path(File.dirname(__FILE__))
114 data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
115 files = ['ca.pem', 'server1.key', 'server1.pem']
116 files.map { |f| File.open(File.join(data_dir, f)).read }
117end
118
119# creates a ServerCredentials from the test certificates.
120def test_server_creds
121 certs = load_test_certs
Tim Emiola73a540a2015-08-28 18:56:17 -0700122 GRPC::Core::ServerCredentials.new(
123 nil, [{private_key: certs[1], cert_chain: certs[2]}], false)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700124end
125
126# produces a string of null chars (\0) of length l.
127def nulls(l)
128 fail 'requires #{l} to be +ve' if l < 0
129 [].pack('x' * l).force_encoding('utf-8')
130end
131
132# A EnumeratorQueue wraps a Queue yielding the items added to it via each_item.
133class EnumeratorQueue
134 extend Forwardable
135 def_delegators :@q, :push
136
137 def initialize(sentinel)
138 @q = Queue.new
139 @sentinel = sentinel
140 end
141
142 def each_item
143 return enum_for(:each_item) unless block_given?
144 loop do
145 r = @q.pop
146 break if r.equal?(@sentinel)
147 fail r if r.is_a? Exception
148 yield r
149 end
150 end
151end
152
153# A runnable implementation of the schema-specified testing service, with each
154# service method implemented as required by the interop testing spec.
155class TestTarget < Grpc::Testing::TestService::Service
156 include Grpc::Testing
157 include Grpc::Testing::PayloadType
158
159 def empty_call(_empty, _call)
160 Empty.new
161 end
162
163 def unary_call(simple_req, _call)
164 req_size = simple_req.response_size
165 SimpleResponse.new(payload: Payload.new(type: :COMPRESSABLE,
166 body: nulls(req_size)))
167 end
168
169 def streaming_input_call(call)
170 sizes = call.each_remote_read.map { |x| x.payload.body.length }
Tim Emiola69a672e2015-11-10 14:59:15 -0800171 sum = sizes.inject(0) { |s, x| s + x }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700172 StreamingInputCallResponse.new(aggregated_payload_size: sum)
173 end
174
175 def streaming_output_call(req, _call)
176 cls = StreamingOutputCallResponse
177 req.response_parameters.map do |p|
178 cls.new(payload: Payload.new(type: req.response_type,
179 body: nulls(p.size)))
180 end
181 end
182
183 def full_duplex_call(reqs)
184 # reqs is a lazy Enumerator of the requests sent by the client.
185 q = EnumeratorQueue.new(self)
186 cls = StreamingOutputCallResponse
187 Thread.new do
188 begin
189 GRPC.logger.info('interop-server: started receiving')
190 reqs.each do |req|
191 resp_size = req.response_parameters[0].size
192 GRPC.logger.info("read a req, response size is #{resp_size}")
193 resp = cls.new(payload: Payload.new(type: req.response_type,
194 body: nulls(resp_size)))
195 q.push(resp)
196 end
197 GRPC.logger.info('interop-server: finished receiving')
198 q.push(self)
199 rescue StandardError => e
200 GRPC.logger.info('interop-server: failed')
201 GRPC.logger.warn(e)
202 q.push(e) # share the exception with the enumerator
203 end
204 end
205 q.each_item
206 end
207
208 def half_duplex_call(reqs)
209 # TODO: update with unique behaviour of the half_duplex_call if that's
210 # ever required by any of the tests.
211 full_duplex_call(reqs)
212 end
213end
214
215# validates the the command line options, returning them as a Hash.
216def parse_options
217 options = {
218 'port' => nil,
219 'secure' => false
220 }
221 OptionParser.new do |opts|
222 opts.banner = 'Usage: --port port'
223 opts.on('--port PORT', 'server port') do |v|
224 options['port'] = v
225 end
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700226 opts.on('--use_tls USE_TLS', ['false', 'true'],
227 'require a secure connection?') do |v|
228 options['secure'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700229 end
230 end.parse!
231
232 if options['port'].nil?
233 fail(OptionParser::MissingArgument, 'please specify --port')
234 end
235 options
236end
237
238def main
239 opts = parse_options
240 host = "0.0.0.0:#{opts['port']}"
241 s = GRPC::RpcServer.new
242 if opts['secure']
243 s.add_http2_port(host, test_server_creds)
244 GRPC.logger.info("... running securely on #{host}")
245 else
Tim Emiolac03138a2015-09-24 13:11:03 -0700246 s.add_http2_port(host, :this_port_is_insecure)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700247 GRPC.logger.info("... running insecurely on #{host}")
248 end
249 s.handle(TestTarget)
250 s.run_till_terminated
251end
252
253main