| /* |
| * |
| * Copyright 2016 gRPC authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| #include <grpc/support/port_platform.h> |
| |
| #include "src/core/lib/iomgr/port.h" |
| #if GRPC_ARES == 1 && defined(GPR_WINDOWS) |
| |
| #include <ares.h> |
| |
| #include <grpc/support/alloc.h> |
| #include <grpc/support/log.h> |
| #include <grpc/support/log_windows.h> |
| #include <grpc/support/string_util.h> |
| #include <grpc/support/time.h> |
| #include <string.h> |
| #include "src/core/lib/gpr/string.h" |
| #include "src/core/lib/gprpp/memory.h" |
| #include "src/core/lib/iomgr/combiner.h" |
| #include "src/core/lib/iomgr/socket_windows.h" |
| #include "src/core/lib/iomgr/tcp_windows.h" |
| #include "src/core/lib/slice/slice_internal.h" |
| |
| #include "src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_ev_driver.h" |
| #include "src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper.h" |
| |
| /* TODO(apolcyn): remove this hack after fixing upstream. |
| * Our grpc/c-ares code on Windows uses the ares_set_socket_functions API, |
| * which uses "struct iovec" type, which on Windows is defined inside of |
| * a c-ares header that is not public. |
| * See https://github.com/c-ares/c-ares/issues/206. */ |
| struct iovec { |
| void* iov_base; |
| size_t iov_len; |
| }; |
| |
| namespace grpc_core { |
| |
| /* c-ares creates its own sockets and is meant to read them when readable and |
| * write them when writeable. To fit this socket usage model into the grpc |
| * windows poller (which gives notifications when attempted reads and writes are |
| * actually fulfilled rather than possible), this GrpcPolledFdWindows class |
| * takes advantage of the ares_set_socket_functions API and acts as a virtual |
| * socket. It holds its own read and write buffers which are written to and read |
| * from c-ares and are used with the grpc windows poller, and it, e.g., |
| * manufactures virtual socket error codes when it e.g. needs to tell the c-ares |
| * library to wait for an async read. */ |
| class GrpcPolledFdWindows : public GrpcPolledFd { |
| public: |
| enum WriteState { |
| WRITE_IDLE, |
| WRITE_REQUESTED, |
| WRITE_PENDING, |
| WRITE_WAITING_FOR_VERIFICATION_UPON_RETRY, |
| }; |
| |
| GrpcPolledFdWindows(ares_socket_t as, grpc_combiner* combiner) |
| : read_buf_(grpc_empty_slice()), |
| write_buf_(grpc_empty_slice()), |
| write_state_(WRITE_IDLE), |
| gotten_into_driver_list_(false) { |
| gpr_asprintf(&name_, "c-ares socket: %" PRIdPTR, as); |
| winsocket_ = grpc_winsocket_create(as, name_); |
| combiner_ = GRPC_COMBINER_REF(combiner, name_); |
| GRPC_CLOSURE_INIT(&outer_read_closure_, |
| &GrpcPolledFdWindows::OnIocpReadable, this, |
| grpc_combiner_scheduler(combiner_)); |
| GRPC_CLOSURE_INIT(&outer_write_closure_, |
| &GrpcPolledFdWindows::OnIocpWriteable, this, |
| grpc_combiner_scheduler(combiner_)); |
| } |
| |
| ~GrpcPolledFdWindows() { |
| GRPC_COMBINER_UNREF(combiner_, name_); |
| grpc_slice_unref_internal(read_buf_); |
| grpc_slice_unref_internal(write_buf_); |
| GPR_ASSERT(read_closure_ == nullptr); |
| GPR_ASSERT(write_closure_ == nullptr); |
| grpc_winsocket_destroy(winsocket_); |
| gpr_free(name_); |
| } |
| |
| void ScheduleAndNullReadClosure(grpc_error* error) { |
| GRPC_CLOSURE_SCHED(read_closure_, error); |
| read_closure_ = nullptr; |
| } |
| |
| void ScheduleAndNullWriteClosure(grpc_error* error) { |
| GRPC_CLOSURE_SCHED(write_closure_, error); |
| write_closure_ = nullptr; |
| } |
| |
| void RegisterForOnReadableLocked(grpc_closure* read_closure) override { |
| GPR_ASSERT(read_closure_ == nullptr); |
| read_closure_ = read_closure; |
| GPR_ASSERT(GRPC_SLICE_LENGTH(read_buf_) == 0); |
| grpc_slice_unref_internal(read_buf_); |
| read_buf_ = GRPC_SLICE_MALLOC(4192); |
| WSABUF buffer; |
| buffer.buf = (char*)GRPC_SLICE_START_PTR(read_buf_); |
| buffer.len = GRPC_SLICE_LENGTH(read_buf_); |
| memset(&winsocket_->read_info.overlapped, 0, sizeof(OVERLAPPED)); |
| recv_from_source_addr_len_ = sizeof(recv_from_source_addr_); |
| DWORD flags = 0; |
| if (WSARecvFrom(grpc_winsocket_wrapped_socket(winsocket_), &buffer, 1, |
| nullptr, &flags, (sockaddr*)recv_from_source_addr_, |
| &recv_from_source_addr_len_, |
| &winsocket_->read_info.overlapped, nullptr)) { |
| char* msg = gpr_format_message(WSAGetLastError()); |
| grpc_error* error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(msg); |
| GRPC_CARES_TRACE_LOG( |
| "RegisterForOnReadableLocked: WSARecvFrom error:|%s|. fd:|%s|", msg, |
| GetName()); |
| gpr_free(msg); |
| if (WSAGetLastError() != WSA_IO_PENDING) { |
| ScheduleAndNullReadClosure(error); |
| return; |
| } |
| } |
| grpc_socket_notify_on_read(winsocket_, &outer_read_closure_); |
| } |
| |
| void RegisterForOnWriteableLocked(grpc_closure* write_closure) override { |
| GRPC_CARES_TRACE_LOG( |
| "RegisterForOnWriteableLocked. fd:|%s|. Current write state: %d", |
| GetName(), write_state_); |
| GPR_ASSERT(write_closure_ == nullptr); |
| write_closure_ = write_closure; |
| switch (write_state_) { |
| case WRITE_IDLE: |
| ScheduleAndNullWriteClosure(GRPC_ERROR_NONE); |
| break; |
| case WRITE_REQUESTED: |
| write_state_ = WRITE_PENDING; |
| SendWriteBuf(nullptr, &winsocket_->write_info.overlapped); |
| grpc_socket_notify_on_write(winsocket_, &outer_write_closure_); |
| break; |
| case WRITE_PENDING: |
| case WRITE_WAITING_FOR_VERIFICATION_UPON_RETRY: |
| abort(); |
| } |
| } |
| |
| bool IsFdStillReadableLocked() override { |
| return GRPC_SLICE_LENGTH(read_buf_) > 0; |
| } |
| |
| void ShutdownLocked(grpc_error* error) override { |
| grpc_winsocket_shutdown(winsocket_); |
| } |
| |
| ares_socket_t GetWrappedAresSocketLocked() override { |
| return grpc_winsocket_wrapped_socket(winsocket_); |
| } |
| |
| const char* GetName() override { return name_; } |
| |
| ares_ssize_t RecvFrom(void* data, ares_socket_t data_len, int flags, |
| struct sockaddr* from, ares_socklen_t* from_len) { |
| GRPC_CARES_TRACE_LOG( |
| "RecvFrom called on fd:|%s|. Current read buf length:|%d|", GetName(), |
| GRPC_SLICE_LENGTH(read_buf_)); |
| if (GRPC_SLICE_LENGTH(read_buf_) == 0) { |
| WSASetLastError(WSAEWOULDBLOCK); |
| return -1; |
| } |
| ares_ssize_t bytes_read = 0; |
| for (size_t i = 0; i < GRPC_SLICE_LENGTH(read_buf_) && i < data_len; i++) { |
| ((char*)data)[i] = GRPC_SLICE_START_PTR(read_buf_)[i]; |
| bytes_read++; |
| } |
| read_buf_ = grpc_slice_sub_no_ref(read_buf_, bytes_read, |
| GRPC_SLICE_LENGTH(read_buf_)); |
| /* c-ares overloads this recv_from virtual socket function to receive |
| * data on both UDP and TCP sockets, and from is nullptr for TCP. */ |
| if (from != nullptr) { |
| GPR_ASSERT(*from_len <= recv_from_source_addr_len_); |
| memcpy(from, &recv_from_source_addr_, recv_from_source_addr_len_); |
| *from_len = recv_from_source_addr_len_; |
| } |
| return bytes_read; |
| } |
| |
| grpc_slice FlattenIovec(const struct iovec* iov, int iov_count) { |
| int total = 0; |
| for (int i = 0; i < iov_count; i++) { |
| total += iov[i].iov_len; |
| } |
| grpc_slice out = GRPC_SLICE_MALLOC(total); |
| size_t cur = 0; |
| for (int i = 0; i < iov_count; i++) { |
| for (int k = 0; k < iov[i].iov_len; k++) { |
| GRPC_SLICE_START_PTR(out)[cur++] = ((char*)iov[i].iov_base)[k]; |
| } |
| } |
| return out; |
| } |
| |
| int SendWriteBuf(LPDWORD bytes_sent_ptr, LPWSAOVERLAPPED overlapped) { |
| WSABUF buf; |
| buf.len = GRPC_SLICE_LENGTH(write_buf_); |
| buf.buf = (char*)GRPC_SLICE_START_PTR(write_buf_); |
| DWORD flags = 0; |
| int out = WSASend(grpc_winsocket_wrapped_socket(winsocket_), &buf, 1, |
| bytes_sent_ptr, flags, overlapped, nullptr); |
| GRPC_CARES_TRACE_LOG( |
| "WSASend: name:%s. buf len:%d. bytes sent: %d. overlapped %p. return " |
| "val: %d", |
| GetName(), buf.len, *bytes_sent_ptr, overlapped, out); |
| return out; |
| } |
| |
| ares_ssize_t TrySendWriteBufSyncNonBlocking() { |
| GPR_ASSERT(write_state_ == WRITE_IDLE); |
| ares_ssize_t total_sent; |
| DWORD bytes_sent = 0; |
| if (SendWriteBuf(&bytes_sent, nullptr) != 0) { |
| char* msg = gpr_format_message(WSAGetLastError()); |
| GRPC_CARES_TRACE_LOG( |
| "TrySendWriteBufSyncNonBlocking: SendWriteBuf error:|%s|. fd:|%s|", |
| msg, GetName()); |
| gpr_free(msg); |
| if (WSAGetLastError() == WSA_IO_PENDING) { |
| WSASetLastError(WSAEWOULDBLOCK); |
| write_state_ = WRITE_REQUESTED; |
| } |
| } |
| write_buf_ = grpc_slice_sub_no_ref(write_buf_, bytes_sent, |
| GRPC_SLICE_LENGTH(write_buf_)); |
| return bytes_sent; |
| } |
| |
| ares_ssize_t SendV(const struct iovec* iov, int iov_count) { |
| GRPC_CARES_TRACE_LOG("SendV called on fd:|%s|. Current write state: %d", |
| GetName(), write_state_); |
| switch (write_state_) { |
| case WRITE_IDLE: |
| GPR_ASSERT(GRPC_SLICE_LENGTH(write_buf_) == 0); |
| grpc_slice_unref_internal(write_buf_); |
| write_buf_ = FlattenIovec(iov, iov_count); |
| return TrySendWriteBufSyncNonBlocking(); |
| case WRITE_REQUESTED: |
| case WRITE_PENDING: |
| WSASetLastError(WSAEWOULDBLOCK); |
| return -1; |
| case WRITE_WAITING_FOR_VERIFICATION_UPON_RETRY: |
| grpc_slice currently_attempted = FlattenIovec(iov, iov_count); |
| GPR_ASSERT(GRPC_SLICE_LENGTH(currently_attempted) >= |
| GRPC_SLICE_LENGTH(write_buf_)); |
| ares_ssize_t total_sent = 0; |
| for (size_t i = 0; i < GRPC_SLICE_LENGTH(write_buf_); i++) { |
| GPR_ASSERT(GRPC_SLICE_START_PTR(currently_attempted)[i] == |
| GRPC_SLICE_START_PTR(write_buf_)[i]); |
| total_sent++; |
| } |
| grpc_slice_unref_internal(write_buf_); |
| write_buf_ = |
| grpc_slice_sub_no_ref(currently_attempted, total_sent, |
| GRPC_SLICE_LENGTH(currently_attempted)); |
| write_state_ = WRITE_IDLE; |
| total_sent += TrySendWriteBufSyncNonBlocking(); |
| return total_sent; |
| } |
| abort(); |
| } |
| |
| int Connect(const struct sockaddr* target, ares_socklen_t target_len) { |
| SOCKET s = grpc_winsocket_wrapped_socket(winsocket_); |
| GRPC_CARES_TRACE_LOG("Connect: fd:|%s|", GetName()); |
| int out = |
| WSAConnect(s, target, target_len, nullptr, nullptr, nullptr, nullptr); |
| if (out != 0) { |
| char* msg = gpr_format_message(WSAGetLastError()); |
| GRPC_CARES_TRACE_LOG("Connect error code:|%d|, msg:|%s|. fd:|%s|", |
| WSAGetLastError(), msg, GetName()); |
| gpr_free(msg); |
| // c-ares expects a posix-style connect API |
| out = -1; |
| } |
| return out; |
| } |
| |
| static void OnIocpReadable(void* arg, grpc_error* error) { |
| GrpcPolledFdWindows* polled_fd = static_cast<GrpcPolledFdWindows*>(arg); |
| polled_fd->OnIocpReadableInner(error); |
| } |
| |
| void OnIocpReadableInner(grpc_error* error) { |
| if (error == GRPC_ERROR_NONE) { |
| if (winsocket_->read_info.wsa_error != 0) { |
| /* WSAEMSGSIZE would be due to receiving more data |
| * than our read buffer's fixed capacity. Assume that |
| * the connection is TCP and read the leftovers |
| * in subsequent c-ares reads. */ |
| if (winsocket_->read_info.wsa_error != WSAEMSGSIZE) { |
| GRPC_ERROR_UNREF(error); |
| char* msg = gpr_format_message(winsocket_->read_info.wsa_error); |
| GRPC_CARES_TRACE_LOG( |
| "OnIocpReadableInner. winsocket error:|%s|. fd:|%s|", msg, |
| GetName()); |
| error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(msg); |
| gpr_free(msg); |
| } |
| } |
| } |
| if (error == GRPC_ERROR_NONE) { |
| read_buf_ = grpc_slice_sub_no_ref(read_buf_, 0, |
| winsocket_->read_info.bytes_transfered); |
| } else { |
| grpc_slice_unref_internal(read_buf_); |
| read_buf_ = grpc_empty_slice(); |
| } |
| GRPC_CARES_TRACE_LOG( |
| "OnIocpReadable finishing. read buf length now:|%d|. :fd:|%s|", |
| GRPC_SLICE_LENGTH(read_buf_), GetName()); |
| ScheduleAndNullReadClosure(error); |
| } |
| |
| static void OnIocpWriteable(void* arg, grpc_error* error) { |
| GrpcPolledFdWindows* polled_fd = static_cast<GrpcPolledFdWindows*>(arg); |
| polled_fd->OnIocpWriteableInner(error); |
| } |
| |
| void OnIocpWriteableInner(grpc_error* error) { |
| GRPC_CARES_TRACE_LOG("OnIocpWriteableInner. fd:|%s|", GetName()); |
| if (error == GRPC_ERROR_NONE) { |
| if (winsocket_->write_info.wsa_error != 0) { |
| char* msg = gpr_format_message(winsocket_->write_info.wsa_error); |
| GRPC_CARES_TRACE_LOG( |
| "OnIocpWriteableInner. winsocket error:|%s|. fd:|%s|", msg, |
| GetName()); |
| GRPC_ERROR_UNREF(error); |
| error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(msg); |
| gpr_free(msg); |
| } |
| } |
| GPR_ASSERT(write_state_ == WRITE_PENDING); |
| if (error == GRPC_ERROR_NONE) { |
| write_state_ = WRITE_WAITING_FOR_VERIFICATION_UPON_RETRY; |
| write_buf_ = grpc_slice_sub_no_ref( |
| write_buf_, 0, winsocket_->write_info.bytes_transfered); |
| } else { |
| grpc_slice_unref_internal(write_buf_); |
| write_buf_ = grpc_empty_slice(); |
| } |
| ScheduleAndNullWriteClosure(error); |
| } |
| |
| bool gotten_into_driver_list() const { return gotten_into_driver_list_; } |
| void set_gotten_into_driver_list() { gotten_into_driver_list_ = true; } |
| |
| grpc_combiner* combiner_; |
| char recv_from_source_addr_[200]; |
| ares_socklen_t recv_from_source_addr_len_; |
| grpc_slice read_buf_; |
| grpc_slice write_buf_; |
| grpc_closure* read_closure_ = nullptr; |
| grpc_closure* write_closure_ = nullptr; |
| grpc_closure outer_read_closure_; |
| grpc_closure outer_write_closure_; |
| grpc_winsocket* winsocket_; |
| WriteState write_state_; |
| char* name_ = nullptr; |
| bool gotten_into_driver_list_; |
| }; |
| |
| struct SockToPolledFdEntry { |
| SockToPolledFdEntry(SOCKET s, GrpcPolledFdWindows* fd) |
| : socket(s), polled_fd(fd) {} |
| SOCKET socket; |
| GrpcPolledFdWindows* polled_fd; |
| SockToPolledFdEntry* next = nullptr; |
| }; |
| |
| /* A SockToPolledFdMap can make ares_socket_t types (SOCKET's on windows) |
| * to GrpcPolledFdWindow's, and is used to find the appropriate |
| * GrpcPolledFdWindows to handle a virtual socket call when c-ares makes that |
| * socket call on the ares_socket_t type. Instances are owned by and one-to-one |
| * with a GrpcPolledFdWindows factory and event driver */ |
| class SockToPolledFdMap { |
| public: |
| SockToPolledFdMap(grpc_combiner* combiner) { |
| combiner_ = GRPC_COMBINER_REF(combiner, "sock to polled fd map"); |
| } |
| |
| ~SockToPolledFdMap() { |
| GPR_ASSERT(head_ == nullptr); |
| GRPC_COMBINER_UNREF(combiner_, "sock to polled fd map"); |
| } |
| |
| void AddNewSocket(SOCKET s, GrpcPolledFdWindows* polled_fd) { |
| SockToPolledFdEntry* new_node = New<SockToPolledFdEntry>(s, polled_fd); |
| new_node->next = head_; |
| head_ = new_node; |
| } |
| |
| GrpcPolledFdWindows* LookupPolledFd(SOCKET s) { |
| for (SockToPolledFdEntry* node = head_; node != nullptr; |
| node = node->next) { |
| if (node->socket == s) { |
| GPR_ASSERT(node->polled_fd != nullptr); |
| return node->polled_fd; |
| } |
| } |
| abort(); |
| } |
| |
| void RemoveEntry(SOCKET s) { |
| GPR_ASSERT(head_ != nullptr); |
| SockToPolledFdEntry** prev = &head_; |
| for (SockToPolledFdEntry* node = head_; node != nullptr; |
| node = node->next) { |
| if (node->socket == s) { |
| *prev = node->next; |
| Delete(node); |
| return; |
| } |
| prev = &node->next; |
| } |
| abort(); |
| } |
| |
| /* These virtual socket functions are called from within the c-ares |
| * library. These methods generally dispatch those socket calls to the |
| * appropriate methods. The virtual "socket" and "close" methods are |
| * special and instead create/add and remove/destroy GrpcPolledFdWindows |
| * objects. |
| */ |
| static ares_socket_t Socket(int af, int type, int protocol, void* user_data) { |
| SockToPolledFdMap* map = static_cast<SockToPolledFdMap*>(user_data); |
| SOCKET s = WSASocket(af, type, protocol, nullptr, 0, WSA_FLAG_OVERLAPPED); |
| if (s == INVALID_SOCKET) { |
| return s; |
| } |
| grpc_tcp_set_non_block(s); |
| GrpcPolledFdWindows* polled_fd = |
| New<GrpcPolledFdWindows>(s, map->combiner_); |
| map->AddNewSocket(s, polled_fd); |
| return s; |
| } |
| |
| static int Connect(ares_socket_t as, const struct sockaddr* target, |
| ares_socklen_t target_len, void* user_data) { |
| SockToPolledFdMap* map = static_cast<SockToPolledFdMap*>(user_data); |
| GrpcPolledFdWindows* polled_fd = map->LookupPolledFd(as); |
| return polled_fd->Connect(target, target_len); |
| } |
| |
| static ares_ssize_t SendV(ares_socket_t as, const struct iovec* iov, |
| int iovec_count, void* user_data) { |
| SockToPolledFdMap* map = static_cast<SockToPolledFdMap*>(user_data); |
| GrpcPolledFdWindows* polled_fd = map->LookupPolledFd(as); |
| return polled_fd->SendV(iov, iovec_count); |
| } |
| |
| static ares_ssize_t RecvFrom(ares_socket_t as, void* data, size_t data_len, |
| int flags, struct sockaddr* from, |
| ares_socklen_t* from_len, void* user_data) { |
| SockToPolledFdMap* map = static_cast<SockToPolledFdMap*>(user_data); |
| GrpcPolledFdWindows* polled_fd = map->LookupPolledFd(as); |
| return polled_fd->RecvFrom(data, data_len, flags, from, from_len); |
| } |
| |
| static int CloseSocket(SOCKET s, void* user_data) { |
| SockToPolledFdMap* map = static_cast<SockToPolledFdMap*>(user_data); |
| GrpcPolledFdWindows* polled_fd = map->LookupPolledFd(s); |
| map->RemoveEntry(s); |
| // If a gRPC polled fd has not made it in to the driver's list yet, then |
| // the driver has not and will never see this socket. |
| if (!polled_fd->gotten_into_driver_list()) { |
| polled_fd->ShutdownLocked(GRPC_ERROR_CREATE_FROM_STATIC_STRING( |
| "Shut down c-ares fd before without it ever having made it into the " |
| "driver's list")); |
| return 0; |
| } |
| return 0; |
| } |
| |
| private: |
| SockToPolledFdEntry* head_ = nullptr; |
| grpc_combiner* combiner_; |
| }; |
| |
| const struct ares_socket_functions custom_ares_sock_funcs = { |
| &SockToPolledFdMap::Socket /* socket */, |
| &SockToPolledFdMap::CloseSocket /* close */, |
| &SockToPolledFdMap::Connect /* connect */, |
| &SockToPolledFdMap::RecvFrom /* recvfrom */, |
| &SockToPolledFdMap::SendV /* sendv */, |
| }; |
| |
| class GrpcPolledFdFactoryWindows : public GrpcPolledFdFactory { |
| public: |
| GrpcPolledFdFactoryWindows(grpc_combiner* combiner) |
| : sock_to_polled_fd_map_(combiner) {} |
| |
| GrpcPolledFd* NewGrpcPolledFdLocked(ares_socket_t as, |
| grpc_pollset_set* driver_pollset_set, |
| grpc_combiner* combiner) override { |
| GrpcPolledFdWindows* polled_fd = sock_to_polled_fd_map_.LookupPolledFd(as); |
| // Set a flag so that the virtual socket "close" method knows it |
| // doesn't need to call ShutdownLocked, since now the driver will. |
| polled_fd->set_gotten_into_driver_list(); |
| return polled_fd; |
| } |
| |
| void ConfigureAresChannelLocked(ares_channel channel) override { |
| ares_set_socket_functions(channel, &custom_ares_sock_funcs, |
| &sock_to_polled_fd_map_); |
| } |
| |
| private: |
| SockToPolledFdMap sock_to_polled_fd_map_; |
| }; |
| |
| UniquePtr<GrpcPolledFdFactory> NewGrpcPolledFdFactory(grpc_combiner* combiner) { |
| return UniquePtr<GrpcPolledFdFactory>( |
| New<GrpcPolledFdFactoryWindows>(combiner)); |
| } |
| |
| } // namespace grpc_core |
| |
| #endif /* GRPC_ARES == 1 && defined(GPR_WINDOWS) */ |