diff --git a/src/node/ext/call.cc b/src/node/ext/call.cc index 6434c2f0d5f..3261b780f77 100644 --- a/src/node/ext/call.cc +++ b/src/node/ext/call.cc @@ -33,6 +33,7 @@ #include +#include "grpc/support/log.h" #include "grpc/grpc.h" #include "grpc/support/time.h" #include "byte_buffer.h" @@ -173,31 +174,43 @@ NAN_METHOD(Call::AddMetadata) { return NanThrowTypeError("addMetadata can only be called on Call objects"); } Call *call = ObjectWrap::Unwrap(args.This()); - for (int i = 0; !args[i]->IsUndefined(); i++) { - if (!args[i]->IsObject()) { + if (!args[0]->IsObject()) { + return NanThrowTypeError("addMetadata's first argument must be an object"); + } + Handle metadata = args[0]->ToObject(); + Handle keys(metadata->GetOwnPropertyNames()); + for (unsigned int i = 0; i < keys->Length(); i++) { + Handle current_key(keys->Get(i)->ToString()); + if (!metadata->Get(current_key)->IsArray()) { return NanThrowTypeError( - "addMetadata arguments must be objects with key and value"); + "addMetadata's first argument's values must be arrays"); } - Handle item = args[i]->ToObject(); - Handle key = item->Get(NanNew("key")); - if (!key->IsString()) { - return NanThrowTypeError( - "objects passed to addMetadata must have key->string"); - } - Handle value = item->Get(NanNew("value")); - if (!Buffer::HasInstance(value)) { - return NanThrowTypeError( - "objects passed to addMetadata must have value->Buffer"); - } - grpc_metadata metadata; - NanUtf8String utf8_key(key); - metadata.key = *utf8_key; - metadata.value = Buffer::Data(value); - metadata.value_length = Buffer::Length(value); - grpc_call_error error = - grpc_call_add_metadata(call->wrapped_call, &metadata, 0); - if (error != GRPC_CALL_OK) { - return NanThrowError("addMetadata failed", error); + NanUtf8String utf8_key(current_key); + Handle values = Local::Cast(metadata->Get(current_key)); + for (unsigned int j = 0; j < values->Length(); j++) { + Handle value = values->Get(j); + grpc_metadata metadata; + grpc_call_error error; + metadata.key = *utf8_key; + if (Buffer::HasInstance(value)) { + metadata.value = Buffer::Data(value); + metadata.value_length = Buffer::Length(value); + error = grpc_call_add_metadata(call->wrapped_call, &metadata, 0); + } else if (value->IsString()) { + Handle string_value = value->ToString(); + NanUtf8String utf8_value(string_value); + metadata.value = *utf8_value; + metadata.value_length = string_value->Length(); + gpr_log(GPR_DEBUG, "adding metadata: %s, %s, %d", metadata.key, + metadata.value, metadata.value_length); + error = grpc_call_add_metadata(call->wrapped_call, &metadata, 0); + } else { + return NanThrowTypeError( + "addMetadata values must be strings or buffers"); + } + if (error != GRPC_CALL_OK) { + return NanThrowError("addMetadata failed", error); + } } } NanReturnUndefined(); diff --git a/src/node/ext/event.cc b/src/node/ext/event.cc index 2ca38b7448e..fcf046b6978 100644 --- a/src/node/ext/event.cc +++ b/src/node/ext/event.cc @@ -31,6 +31,8 @@ * */ +#include + #include #include #include "grpc/grpc.h" @@ -43,6 +45,7 @@ namespace grpc { namespace node { +using ::node::Buffer; using v8::Array; using v8::Date; using v8::Handle; @@ -53,6 +56,36 @@ using v8::Persistent; using v8::String; using v8::Value; +Handle ParseMetadata(grpc_metadata *metadata_elements, size_t length) { + NanEscapableScope(); + std::map size_map; + std::map index_map; + + for (unsigned int i = 0; i < length; i++) { + char *key = metadata_elements[i].key; + if (size_map.count(key)) { + size_map[key] += 1; + } + index_map[key] = 0; + } + Handle metadata_object = NanNew(); + for (unsigned int i = 0; i < length; i++) { + grpc_metadata* elem = &metadata_elements[i]; + Handle key_string = String::New(elem->key); + Handle array; + if (metadata_object->Has(key_string)) { + array = Handle::Cast(metadata_object->Get(key_string)); + } else { + array = NanNew(size_map[elem->key]); + metadata_object->Set(key_string, array); + } + array->Set(index_map[elem->key], + NanNewBufferHandle(elem->value, elem->value_length)); + index_map[elem->key] += 1; + } + return NanEscapeScope(metadata_object); +} + Handle GetEventData(grpc_event *event) { NanEscapableScope(); size_t count; @@ -72,18 +105,7 @@ Handle GetEventData(grpc_event *event) { case GRPC_CLIENT_METADATA_READ: count = event->data.client_metadata_read.count; items = event->data.client_metadata_read.elements; - metadata = NanNew(static_cast(count)); - for (unsigned int i = 0; i < count; i++) { - Handle item_obj = NanNew(); - item_obj->Set(NanNew("key"), - NanNew(items[i].key)); - item_obj->Set( - NanNew("value"), - NanNew(items[i].value, - static_cast(items[i].value_length))); - metadata->Set(i, item_obj); - } - return NanEscapeScope(metadata); + return NanEscapeScope(ParseMetadata(items, count)); case GRPC_FINISHED: status = NanNew(); status->Set(NanNew("code"), NanNew(event->data.finished.status)); @@ -93,18 +115,7 @@ Handle GetEventData(grpc_event *event) { } count = event->data.finished.metadata_count; items = event->data.finished.metadata_elements; - metadata = NanNew(static_cast(count)); - for (unsigned int i = 0; i < count; i++) { - Handle item_obj = NanNew(); - item_obj->Set(NanNew("key"), - NanNew(items[i].key)); - item_obj->Set( - NanNew("value"), - NanNew(items[i].value, - static_cast(items[i].value_length))); - metadata->Set(i, item_obj); - } - status->Set(NanNew("metadata"), metadata); + status->Set(NanNew("metadata"), ParseMetadata(items, count)); return NanEscapeScope(status); case GRPC_SERVER_RPC_NEW: rpc_new = NanNew(); @@ -133,7 +144,7 @@ Handle GetEventData(grpc_event *event) { static_cast(items[i].value_length))); metadata->Set(i, item_obj); } - rpc_new->Set(NanNew("metadata"), metadata); + rpc_new->Set(NanNew("metadata"), ParseMetadata(items, count)); return NanEscapeScope(rpc_new); default: return NanEscapeScope(NanNull()); diff --git a/src/node/test/call_test.js b/src/node/test/call_test.js index b37c44abaf5..dfa9aaa1a78 100644 --- a/src/node/test/call_test.js +++ b/src/node/test/call_test.js @@ -99,24 +99,30 @@ describe('call', function() { }); }); describe('addMetadata', function() { - it('should succeed with objects containing keys and values', function() { + it('should succeed with a map from strings to string arrays', function() { var call = new grpc.Call(channel, 'method', getDeadline(1)); assert.doesNotThrow(function() { - call.addMetadata(); + call.addMetadata({'key': ['value']}); + }); + assert.doesNotThrow(function() { + call.addMetadata({'key1': ['value1'], 'key2': ['value2']}); }); + }); + it('should succeed with a map from strings to buffer arrays', function() { + var call = new grpc.Call(channel, 'method', getDeadline(1)); assert.doesNotThrow(function() { - call.addMetadata({'key' : 'key', - 'value' : new Buffer('value')}); + call.addMetadata({'key': [new Buffer('value')]}); }); assert.doesNotThrow(function() { - call.addMetadata({'key' : 'key1', - 'value' : new Buffer('value1')}, - {'key' : 'key2', - 'value' : new Buffer('value2')}); + call.addMetadata({'key1': [new Buffer('value1')], + 'key2': [new Buffer('value2')]}); }); }); it('should fail with other parameter types', function() { var call = new grpc.Call(channel, 'method', getDeadline(1)); + assert.throws(function() { + call.addMetadata(); + }); assert.throws(function() { call.addMetadata(null); }, TypeError); @@ -133,7 +139,7 @@ describe('call', function() { function() {done();}, 0); assert.throws(function() { - call.addMetadata({'key' : 'key', 'value' : new Buffer('value') }); + call.addMetadata({'key': ['value']}); }, function(err) { return err.code === grpc.callError.ALREADY_INVOKED; }); diff --git a/src/node/test/end_to_end_test.js b/src/node/test/end_to_end_test.js index f8cb660d2df..1f53df23f3b 100644 --- a/src/node/test/end_to_end_test.js +++ b/src/node/test/end_to_end_test.js @@ -68,18 +68,61 @@ describe('end-to-end', function() { server.shutdown(); }); it('should start and end a request without error', function(complete) { - var done = multiDone(function() { - complete(); - }, 2); + var done = multiDone(complete, 2); var deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 3); var status_text = 'xyz'; var call = new grpc.Call(channel, 'dummy_method', deadline); - call.invoke(function(event) { + call.invoke(function(event) { + assert.strictEqual(event.type, + grpc.completionType.CLIENT_METADATA_READ); + },function(event) { + assert.strictEqual(event.type, grpc.completionType.FINISHED); + var status = event.data; + assert.strictEqual(status.code, grpc.status.OK); + assert.strictEqual(status.details, status_text); + done(); + }, 0); + + server.requestCall(function(event) { + assert.strictEqual(event.type, grpc.completionType.SERVER_RPC_NEW); + var server_call = event.call; + assert.notEqual(server_call, null); + server_call.serverAccept(function(event) { + assert.strictEqual(event.type, grpc.completionType.FINISHED); + }, 0); + server_call.serverEndInitialMetadata(0); + server_call.startWriteStatus( + grpc.status.OK, + status_text, + function(event) { + assert.strictEqual(event.type, + grpc.completionType.FINISH_ACCEPTED); + assert.strictEqual(event.data, grpc.opError.OK); + done(); + }); + }); + call.writesDone(function(event) { + assert.strictEqual(event.type, + grpc.completionType.FINISH_ACCEPTED); + assert.strictEqual(event.data, grpc.opError.OK); + }); + }); + it('should successfully send and receive metadata', function(complete) { + var done = multiDone(complete, 2); + var deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 3); + var status_text = 'xyz'; + var call = new grpc.Call(channel, + 'dummy_method', + deadline); + call.addMetadata({'client_key': ['client_value']}); + call.invoke(function(event) { assert.strictEqual(event.type, grpc.completionType.CLIENT_METADATA_READ); + assert.strictEqual(event.data.server_key[0].toString(), 'server_value'); },function(event) { assert.strictEqual(event.type, grpc.completionType.FINISHED); var status = event.data; @@ -90,11 +133,14 @@ describe('end-to-end', function() { server.requestCall(function(event) { assert.strictEqual(event.type, grpc.completionType.SERVER_RPC_NEW); + assert.strictEqual(event.data.metadata.client_key[0].toString(), + 'client_value'); var server_call = event.call; assert.notEqual(server_call, null); server_call.serverAccept(function(event) { assert.strictEqual(event.type, grpc.completionType.FINISHED); }, 0); + server_call.addMetadata({'server_key': ['server_value']}); server_call.serverEndInitialMetadata(0); server_call.startWriteStatus( grpc.status.OK, @@ -115,10 +161,7 @@ describe('end-to-end', function() { it('should send and receive data without error', function(complete) { var req_text = 'client_request'; var reply_text = 'server_response'; - var done = multiDone(function() { - complete(); - server.shutdown(); - }, 6); + var done = multiDone(complete, 6); var deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 3); var status_text = 'success'; diff --git a/src/node/test/server_test.js b/src/node/test/server_test.js index 5fad9a5564e..a3e1edf50f9 100644 --- a/src/node/test/server_test.js +++ b/src/node/test/server_test.js @@ -75,6 +75,9 @@ describe('echo server', function() { channel = new grpc.Channel('localhost:' + port_num); }); + after(function() { + server.shutdown(); + }); it('should echo inputs as responses', function(done) { done = multiDone(done, 4); @@ -95,7 +98,6 @@ describe('echo server', function() { var status = event.data; assert.strictEqual(status.code, grpc.status.OK); assert.strictEqual(status.details, status_text); - server.shutdown(); done(); }, 0); call.startWrite( diff --git a/tools/dockerfile/grpc_java/Dockerfile b/tools/dockerfile/grpc_java/Dockerfile index f234f514e6f..a5508cad7fc 100644 --- a/tools/dockerfile/grpc_java/Dockerfile +++ b/tools/dockerfile/grpc_java/Dockerfile @@ -1,13 +1,11 @@ # Dockerfile for the gRPC Java dev image FROM grpc/java_base -RUN cd /var/local/git/grpc-java/lib/okhttp && \ - mvn -pl okhttp -am install -RUN cd /var/local/git/grpc-java/lib/netty && \ - mvn -pl codec-http2 -am -DskipTests install +RUN git clone --recursive --depth 1 git@github.com:google/grpc-java.git /var/local/git/grpc-java +RUN cd /var/local/git/grpc-java/lib/netty && \ + mvn -pl codec-http2 -am -DskipTests install clean RUN cd /var/local/git/grpc-java && \ - protoc --version>ver.txt && \ - mvn install + ./gradlew build # Specify the default command such that the interop server runs on its known testing port CMD ["/var/local/git/grpc-java/run-test-server.sh", "--use_tls=true", "--port=8030"] diff --git a/tools/dockerfile/grpc_java_base/Dockerfile b/tools/dockerfile/grpc_java_base/Dockerfile index 3271d1b2c2d..73382ed8c9e 100644 --- a/tools/dockerfile/grpc_java_base/Dockerfile +++ b/tools/dockerfile/grpc_java_base/Dockerfile @@ -9,35 +9,36 @@ RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true RUN echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee /etc/apt/sources.list.d/webupd8team-java.list RUN echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee -a /etc/apt/sources.list.d/webupd8team-java.list RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys EEA14886 -RUN apt-get update && apt-get -y install oracle-java8-installer +RUN apt-get update && apt-get -y install oracle-java8-installer && \ + apt-get clean && rm -r /var/cache/oracle-jdk8-installer/ # Install maven -RUN wget http://mirror.olnevhost.net/pub/apache/maven/binaries/apache-maven-3.2.1-bin.tar.gz && \ - tar xvf apache-maven-3.2.1-bin.tar.gz -C /var/local +RUN wget -O - http://mirror.olnevhost.net/pub/apache/maven/binaries/apache-maven-3.2.1-bin.tar.gz | \ + tar xz -C /var/local ENV JAVA_HOME /usr/lib/jvm/java-8-oracle ENV M2_HOME /var/local/apache-maven-3.2.1 ENV PATH $PATH:$JAVA_HOME/bin:$M2_HOME/bin ENV LD_LIBRARY_PATH /usr/local/lib -# Install a GitHub SSH service credential that gives access to the GitHub repo while it's private -# TODO: remove this once the repo is public -ADD .ssh .ssh -RUN chmod 600 .ssh/github.rsa -RUN mkdir -p $HOME/.ssh && echo 'Host github.com' > $HOME/.ssh/config -RUN echo " IdentityFile /.ssh/github.rsa" >> $HOME/.ssh/config -RUN echo 'StrictHostKeyChecking no' >> $HOME/.ssh/config - # Get the protobuf source from GitHub and install it -RUN git clone --recursive --branch v2.6.1 git@github.com:google/protobuf.git /var/local/git/protobuf -RUN cd /var/local/git/protobuf && \ - ./autogen.sh && \ +RUN wget -O - https://github.com/google/protobuf/releases/download/v2.6.1/protobuf-2.6.1.tar.bz2 | \ + tar xj && \ + cd protobuf-2.6.1 && \ ./configure --prefix=/usr && \ - make -j12 && make check && make install && make clean + make -j12 && make check && make install && \ + rm -r "$(pwd)" + +# Install a GitHub SSH service credential that gives access to the GitHub repo while it's private +# TODO: remove this once the repo is public +COPY .ssh/github.rsa /root/.ssh/id_rsa +RUN echo 'Host github.com\nStrictHostKeyChecking no' > /root/.ssh/config -RUN cd /var/local/git/grpc-java/lib/okhttp && \ - mvn -pl okhttp -am validate -RUN cd /var/local/git/grpc-java/lib/netty && \ - mvn -pl codec-http2 -am validate -RUN cd /var/local/git/grpc-java && \ - mvn validate +# Trigger download of as many Maven and Gradle artifacts as possible. We don't build grpc-java +# because we don't want to install netty +RUN git clone --recursive --depth 1 git@github.com:google/grpc-java.git && \ + cd grpc-java/lib/netty && \ + mvn -pl codec-http2 -am -DskipTests verify && \ + cd ../.. && \ + ./gradlew && \ + rm -r "$(pwd)"