Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 1 | #!/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 | |
| 40 | this_dir = File.expand_path(File.dirname(__FILE__)) |
| 41 | lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib') |
| 42 | pb_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 | |
| 47 | require 'forwardable' |
Tim Emiola | 69a672e | 2015-11-10 14:59:15 -0800 | [diff] [blame] | 48 | require 'logger' |
Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 49 | require 'optparse' |
| 50 | |
| 51 | require 'grpc' |
| 52 | |
| 53 | require 'test/proto/empty' |
| 54 | require 'test/proto/messages' |
| 55 | require 'test/proto/test_services' |
| 56 | |
Tim Emiola | 69a672e | 2015-11-10 14:59:15 -0800 | [diff] [blame] | 57 | # DebugIsTruncated extends the default Logger to truncate debug messages |
| 58 | class 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 |
| 93 | end |
| 94 | |
| 95 | # RubyLogger defines a logger for gRPC based on the standard ruby logger. |
| 96 | module RubyLogger |
| 97 | def logger |
| 98 | LOGGER |
| 99 | end |
| 100 | |
| 101 | LOGGER = DebugIsTruncated.new(STDOUT) |
Tim Emiola | a082435 | 2015-11-11 13:43:16 -0800 | [diff] [blame] | 102 | LOGGER.level = Logger::WARN |
Tim Emiola | 69a672e | 2015-11-10 14:59:15 -0800 | [diff] [blame] | 103 | end |
| 104 | |
| 105 | # GRPC is the general RPC module |
| 106 | module GRPC |
| 107 | # Inject the noop #logger if no module-level logger method has been injected. |
| 108 | extend RubyLogger |
| 109 | end |
| 110 | |
Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 111 | # loads the certificates by the test server. |
| 112 | def 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 } |
| 117 | end |
| 118 | |
| 119 | # creates a ServerCredentials from the test certificates. |
| 120 | def test_server_creds |
| 121 | certs = load_test_certs |
Tim Emiola | 73a540a | 2015-08-28 18:56:17 -0700 | [diff] [blame] | 122 | GRPC::Core::ServerCredentials.new( |
| 123 | nil, [{private_key: certs[1], cert_chain: certs[2]}], false) |
Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 124 | end |
| 125 | |
| 126 | # produces a string of null chars (\0) of length l. |
| 127 | def nulls(l) |
| 128 | fail 'requires #{l} to be +ve' if l < 0 |
| 129 | [].pack('x' * l).force_encoding('utf-8') |
| 130 | end |
| 131 | |
| 132 | # A EnumeratorQueue wraps a Queue yielding the items added to it via each_item. |
| 133 | class 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 |
| 151 | end |
| 152 | |
| 153 | # A runnable implementation of the schema-specified testing service, with each |
| 154 | # service method implemented as required by the interop testing spec. |
| 155 | class 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 Emiola | 69a672e | 2015-11-10 14:59:15 -0800 | [diff] [blame] | 171 | sum = sizes.inject(0) { |s, x| s + x } |
Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 172 | 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 |
| 213 | end |
| 214 | |
| 215 | # validates the the command line options, returning them as a Hash. |
| 216 | def 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 Tattermusch | 73b3eea | 2015-10-15 17:52:06 -0700 | [diff] [blame] | 226 | opts.on('--use_tls USE_TLS', ['false', 'true'], |
| 227 | 'require a secure connection?') do |v| |
| 228 | options['secure'] = v == 'true' |
Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 229 | end |
| 230 | end.parse! |
| 231 | |
| 232 | if options['port'].nil? |
| 233 | fail(OptionParser::MissingArgument, 'please specify --port') |
| 234 | end |
| 235 | options |
| 236 | end |
| 237 | |
| 238 | def 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 Emiola | c03138a | 2015-09-24 13:11:03 -0700 | [diff] [blame] | 246 | s.add_http2_port(host, :this_port_is_insecure) |
Tim Emiola | f4ee961 | 2015-08-14 18:47:16 -0700 | [diff] [blame] | 247 | GRPC.logger.info("... running insecurely on #{host}") |
| 248 | end |
| 249 | s.handle(TestTarget) |
| 250 | s.run_till_terminated |
| 251 | end |
| 252 | |
| 253 | main |