mirror of https://github.com/grpc/grpc.git
[EventEngine] WindowsEndpoint (#31735)
* [EventEngine] WindowsEndpoint Initial sketch, all tests passing * Port fix from #28432 * GPR_WINDOWS guard * use MemoryAllocator::MakeReservation for allocated buffers * better logging (respect slice length) * Automated change: Fix sanity tests * improvements * Automated change: Fix sanity tests * InlinedVector<WSABUF, kMaxWSABUFCount> * initial attempt at socket util reunification * posix fixes + local run of sanitize.sh * posix socket includes * fix * Automated change: Fix sanity tests * remove unused include (breaks windows) * remove stale comment Co-authored-by: drfloob <drfloob@users.noreply.github.com>pull/31768/head
parent
808347ffe8
commit
557e558825
39 changed files with 1613 additions and 559 deletions
@ -0,0 +1,320 @@ |
|||||||
|
// Copyright 2022 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/event_engine/tcp_socket_utils.h" |
||||||
|
|
||||||
|
#include "src/core/lib/iomgr/port.h" |
||||||
|
|
||||||
|
#ifdef GRPC_POSIX_SOCKET_UTILS_COMMON |
||||||
|
#include <arpa/inet.h> // IWYU pragma: keep |
||||||
|
|
||||||
|
#ifdef GRPC_LINUX_TCP_H |
||||||
|
#include <linux/tcp.h> |
||||||
|
#else |
||||||
|
#include <netinet/in.h> // IWYU pragma: keep |
||||||
|
#include <netinet/tcp.h> |
||||||
|
#endif |
||||||
|
#include <fcntl.h> |
||||||
|
#include <sys/socket.h> |
||||||
|
#include <unistd.h> |
||||||
|
#endif // GRPC_POSIX_SOCKET_UTILS_COMMON
|
||||||
|
|
||||||
|
#ifdef GRPC_HAVE_UNIX_SOCKET |
||||||
|
#include <sys/stat.h> // IWYU pragma: keep |
||||||
|
#include <sys/un.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
#include <errno.h> |
||||||
|
#include <inttypes.h> |
||||||
|
#include <stdlib.h> |
||||||
|
#include <string.h> |
||||||
|
|
||||||
|
#include <utility> |
||||||
|
|
||||||
|
#include "absl/status/status.h" |
||||||
|
#include "absl/strings/str_cat.h" |
||||||
|
#include "absl/strings/str_format.h" |
||||||
|
|
||||||
|
#include <grpc/support/log.h> |
||||||
|
|
||||||
|
#include "src/core/lib/gprpp/host_port.h" |
||||||
|
#include "src/core/lib/gprpp/status_helper.h" |
||||||
|
#include "src/core/lib/iomgr/sockaddr.h" |
||||||
|
#include "src/core/lib/uri/uri_parser.h" |
||||||
|
|
||||||
|
namespace grpc_event_engine { |
||||||
|
namespace experimental { |
||||||
|
|
||||||
|
namespace { |
||||||
|
constexpr uint8_t kV4MappedPrefix[] = {0, 0, 0, 0, 0, 0, |
||||||
|
0, 0, 0, 0, 0xff, 0xff}; |
||||||
|
absl::StatusOr<std::string> GetScheme( |
||||||
|
const EventEngine::ResolvedAddress& resolved_address) { |
||||||
|
switch (resolved_address.address()->sa_family) { |
||||||
|
case AF_INET: |
||||||
|
return "ipv4"; |
||||||
|
case AF_INET6: |
||||||
|
return "ipv6"; |
||||||
|
case AF_UNIX: |
||||||
|
return "unix"; |
||||||
|
default: |
||||||
|
return absl::InvalidArgumentError(absl::StrFormat( |
||||||
|
"Unknown scheme: %d", resolved_address.address()->sa_family)); |
||||||
|
} |
||||||
|
} |
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ResolvedAddressIsV4Mapped( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr, |
||||||
|
EventEngine::ResolvedAddress* resolved_addr4_out) { |
||||||
|
const sockaddr* addr = resolved_addr.address(); |
||||||
|
if (addr->sa_family == AF_INET6) { |
||||||
|
const sockaddr_in6* addr6 = reinterpret_cast<const sockaddr_in6*>(addr); |
||||||
|
sockaddr_in* addr4_out = |
||||||
|
resolved_addr4_out == nullptr |
||||||
|
? nullptr |
||||||
|
: reinterpret_cast<sockaddr_in*>( |
||||||
|
const_cast<sockaddr*>(resolved_addr4_out->address())); |
||||||
|
|
||||||
|
if (memcmp(addr6->sin6_addr.s6_addr, kV4MappedPrefix, |
||||||
|
sizeof(kV4MappedPrefix)) == 0) { |
||||||
|
if (resolved_addr4_out != nullptr) { |
||||||
|
// Normalize ::ffff:0.0.0.0/96 to IPv4.
|
||||||
|
memset(addr4_out, 0, sizeof(sockaddr_in)); |
||||||
|
addr4_out->sin_family = AF_INET; |
||||||
|
// s6_addr32 would be nice, but it's non-standard.
|
||||||
|
memcpy(&addr4_out->sin_addr, &addr6->sin6_addr.s6_addr[12], 4); |
||||||
|
addr4_out->sin_port = addr6->sin6_port; |
||||||
|
*resolved_addr4_out = EventEngine::ResolvedAddress( |
||||||
|
reinterpret_cast<sockaddr*>(addr4_out), |
||||||
|
static_cast<socklen_t>(sizeof(sockaddr_in))); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
bool ResolvedAddressToV4Mapped( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr, |
||||||
|
EventEngine::ResolvedAddress* resolved_addr6_out) { |
||||||
|
GPR_ASSERT(&resolved_addr != resolved_addr6_out); |
||||||
|
const sockaddr* addr = resolved_addr.address(); |
||||||
|
sockaddr_in6* addr6_out = const_cast<sockaddr_in6*>( |
||||||
|
reinterpret_cast<const sockaddr_in6*>(resolved_addr6_out->address())); |
||||||
|
if (addr->sa_family == AF_INET) { |
||||||
|
const sockaddr_in* addr4 = reinterpret_cast<const sockaddr_in*>(addr); |
||||||
|
memset(resolved_addr6_out, 0, sizeof(*resolved_addr6_out)); |
||||||
|
addr6_out->sin6_family = AF_INET6; |
||||||
|
memcpy(&addr6_out->sin6_addr.s6_addr[0], kV4MappedPrefix, 12); |
||||||
|
memcpy(&addr6_out->sin6_addr.s6_addr[12], &addr4->sin_addr, 4); |
||||||
|
addr6_out->sin6_port = addr4->sin_port; |
||||||
|
*resolved_addr6_out = EventEngine::ResolvedAddress( |
||||||
|
reinterpret_cast<sockaddr*>(addr6_out), |
||||||
|
static_cast<socklen_t>(sizeof(sockaddr_in6))); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress ResolvedAddressMakeWild6(int port) { |
||||||
|
EventEngine::ResolvedAddress resolved_wild_out; |
||||||
|
sockaddr_in6* wild_out = reinterpret_cast<sockaddr_in6*>( |
||||||
|
const_cast<sockaddr*>(resolved_wild_out.address())); |
||||||
|
GPR_ASSERT(port >= 0 && port < 65536); |
||||||
|
memset(wild_out, 0, sizeof(sockaddr_in6)); |
||||||
|
wild_out->sin6_family = AF_INET6; |
||||||
|
wild_out->sin6_port = htons(static_cast<uint16_t>(port)); |
||||||
|
return EventEngine::ResolvedAddress( |
||||||
|
reinterpret_cast<sockaddr*>(wild_out), |
||||||
|
static_cast<socklen_t>(sizeof(sockaddr_in6))); |
||||||
|
} |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress ResolvedAddressMakeWild4(int port) { |
||||||
|
EventEngine::ResolvedAddress resolved_wild_out; |
||||||
|
sockaddr_in* wild_out = reinterpret_cast<sockaddr_in*>( |
||||||
|
const_cast<sockaddr*>(resolved_wild_out.address())); |
||||||
|
GPR_ASSERT(port >= 0 && port < 65536); |
||||||
|
memset(wild_out, 0, sizeof(sockaddr_in)); |
||||||
|
wild_out->sin_family = AF_INET; |
||||||
|
wild_out->sin_port = htons(static_cast<uint16_t>(port)); |
||||||
|
return EventEngine::ResolvedAddress( |
||||||
|
reinterpret_cast<sockaddr*>(wild_out), |
||||||
|
static_cast<socklen_t>(sizeof(sockaddr_in))); |
||||||
|
} |
||||||
|
|
||||||
|
int ResolvedAddressGetPort(const EventEngine::ResolvedAddress& resolved_addr) { |
||||||
|
const sockaddr* addr = resolved_addr.address(); |
||||||
|
switch (addr->sa_family) { |
||||||
|
case AF_INET: |
||||||
|
return ntohs((reinterpret_cast<const sockaddr_in*>(addr))->sin_port); |
||||||
|
case AF_INET6: |
||||||
|
return ntohs((reinterpret_cast<const sockaddr_in6*>(addr))->sin6_port); |
||||||
|
#ifdef GRPC_HAVE_UNIX_SOCKET |
||||||
|
case AF_UNIX: |
||||||
|
return 1; |
||||||
|
#endif |
||||||
|
default: |
||||||
|
gpr_log(GPR_ERROR, "Unknown socket family %d in ResolvedAddressGetPort", |
||||||
|
addr->sa_family); |
||||||
|
abort(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ResolvedAddressSetPort(EventEngine::ResolvedAddress& resolved_addr, |
||||||
|
int port) { |
||||||
|
sockaddr* addr = const_cast<sockaddr*>(resolved_addr.address()); |
||||||
|
switch (addr->sa_family) { |
||||||
|
case AF_INET: |
||||||
|
GPR_ASSERT(port >= 0 && port < 65536); |
||||||
|
(reinterpret_cast<sockaddr_in*>(addr))->sin_port = |
||||||
|
htons(static_cast<uint16_t>(port)); |
||||||
|
return; |
||||||
|
case AF_INET6: |
||||||
|
GPR_ASSERT(port >= 0 && port < 65536); |
||||||
|
(reinterpret_cast<sockaddr_in6*>(addr))->sin6_port = |
||||||
|
htons(static_cast<uint16_t>(port)); |
||||||
|
return; |
||||||
|
default: |
||||||
|
gpr_log(GPR_ERROR, "Unknown socket family %d in grpc_sockaddr_set_port", |
||||||
|
addr->sa_family); |
||||||
|
abort(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
absl::optional<int> ResolvedAddressIsWildcard( |
||||||
|
const EventEngine::ResolvedAddress& addr) { |
||||||
|
const EventEngine::ResolvedAddress* resolved_addr = &addr; |
||||||
|
EventEngine::ResolvedAddress addr4_normalized; |
||||||
|
if (ResolvedAddressIsV4Mapped(addr, &addr4_normalized)) { |
||||||
|
resolved_addr = &addr4_normalized; |
||||||
|
} |
||||||
|
if (resolved_addr->address()->sa_family == AF_INET) { |
||||||
|
// Check for 0.0.0.0
|
||||||
|
const sockaddr_in* addr4 = |
||||||
|
reinterpret_cast<const sockaddr_in*>(resolved_addr->address()); |
||||||
|
if (addr4->sin_addr.s_addr != 0) { |
||||||
|
return absl::nullopt; |
||||||
|
} |
||||||
|
return static_cast<int>(ntohs(addr4->sin_port)); |
||||||
|
} else if (resolved_addr->address()->sa_family == AF_INET6) { |
||||||
|
// Check for ::
|
||||||
|
const sockaddr_in6* addr6 = |
||||||
|
reinterpret_cast<const sockaddr_in6*>(resolved_addr->address()); |
||||||
|
int i; |
||||||
|
for (i = 0; i < 16; i++) { |
||||||
|
if (addr6->sin6_addr.s6_addr[i] != 0) { |
||||||
|
return absl::nullopt; |
||||||
|
} |
||||||
|
} |
||||||
|
return static_cast<int>(ntohs(addr6->sin6_port)); |
||||||
|
} else { |
||||||
|
return absl::nullopt; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
absl::StatusOr<std::string> ResolvedAddressToNormalizedString( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr) { |
||||||
|
EventEngine::ResolvedAddress addr_normalized; |
||||||
|
if (ResolvedAddressIsV4Mapped(resolved_addr, &addr_normalized)) { |
||||||
|
return ResolvedAddressToString(addr_normalized); |
||||||
|
} |
||||||
|
return ResolvedAddressToString(resolved_addr); |
||||||
|
} |
||||||
|
|
||||||
|
absl::StatusOr<std::string> ResolvedAddressToString( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr) { |
||||||
|
const int save_errno = errno; |
||||||
|
const sockaddr* addr = resolved_addr.address(); |
||||||
|
std::string out; |
||||||
|
#ifdef GRPC_HAVE_UNIX_SOCKET |
||||||
|
if (addr->sa_family == AF_UNIX) { |
||||||
|
const sockaddr_un* addr_un = reinterpret_cast<const sockaddr_un*>(addr); |
||||||
|
bool abstract = addr_un->sun_path[0] == '\0'; |
||||||
|
if (abstract) { |
||||||
|
#ifdef GPR_APPLE |
||||||
|
int len = resolved_addr.size() - sizeof(addr_un->sun_family) - |
||||||
|
sizeof(addr_un->sun_len); |
||||||
|
#else |
||||||
|
int len = resolved_addr.size() - sizeof(addr_un->sun_family); |
||||||
|
#endif |
||||||
|
if (len <= 0) { |
||||||
|
return absl::InvalidArgumentError("Empty UDS abstract path"); |
||||||
|
} |
||||||
|
out = std::string(addr_un->sun_path, len); |
||||||
|
} else { |
||||||
|
size_t maxlen = sizeof(addr_un->sun_path); |
||||||
|
if (strnlen(addr_un->sun_path, maxlen) == maxlen) { |
||||||
|
return absl::InvalidArgumentError("UDS path is not null-terminated"); |
||||||
|
} |
||||||
|
out = std::string(addr_un->sun_path); |
||||||
|
} |
||||||
|
return out; |
||||||
|
} |
||||||
|
#endif // GRPC_HAVE_UNIX_SOCKET
|
||||||
|
|
||||||
|
const void* ip = nullptr; |
||||||
|
int port = 0; |
||||||
|
uint32_t sin6_scope_id = 0; |
||||||
|
if (addr->sa_family == AF_INET) { |
||||||
|
const sockaddr_in* addr4 = reinterpret_cast<const sockaddr_in*>(addr); |
||||||
|
ip = &addr4->sin_addr; |
||||||
|
port = ntohs(addr4->sin_port); |
||||||
|
} else if (addr->sa_family == AF_INET6) { |
||||||
|
const sockaddr_in6* addr6 = reinterpret_cast<const sockaddr_in6*>(addr); |
||||||
|
ip = &addr6->sin6_addr; |
||||||
|
port = ntohs(addr6->sin6_port); |
||||||
|
sin6_scope_id = addr6->sin6_scope_id; |
||||||
|
} |
||||||
|
char ntop_buf[INET6_ADDRSTRLEN]; |
||||||
|
if (ip != nullptr && |
||||||
|
inet_ntop(addr->sa_family, ip, ntop_buf, sizeof(ntop_buf)) != nullptr) { |
||||||
|
if (sin6_scope_id != 0) { |
||||||
|
// Enclose sin6_scope_id with the format defined in RFC 6874
|
||||||
|
// section 2.
|
||||||
|
std::string host_with_scope = |
||||||
|
absl::StrFormat("%s%%%" PRIu32, ntop_buf, sin6_scope_id); |
||||||
|
out = grpc_core::JoinHostPort(host_with_scope, port); |
||||||
|
} else { |
||||||
|
out = grpc_core::JoinHostPort(ntop_buf, port); |
||||||
|
} |
||||||
|
} else { |
||||||
|
return absl::InvalidArgumentError( |
||||||
|
absl::StrCat("Unknown sockaddr family: ", addr->sa_family)); |
||||||
|
} |
||||||
|
// This is probably redundant, but we wouldn't want to log the wrong
|
||||||
|
// error.
|
||||||
|
errno = save_errno; |
||||||
|
return out; |
||||||
|
} |
||||||
|
|
||||||
|
absl::StatusOr<std::string> ResolvedAddressToURI( |
||||||
|
const EventEngine::ResolvedAddress& resolved_address) { |
||||||
|
if (resolved_address.size() == 0) { |
||||||
|
return absl::InvalidArgumentError("Empty address"); |
||||||
|
} |
||||||
|
auto scheme = GetScheme(resolved_address); |
||||||
|
GRPC_RETURN_IF_ERROR(scheme.status()); |
||||||
|
auto path = ResolvedAddressToString(resolved_address); |
||||||
|
GRPC_RETURN_IF_ERROR(path.status()); |
||||||
|
absl::StatusOr<grpc_core::URI> uri = |
||||||
|
grpc_core::URI::Create(*scheme, /*authority=*/"", std::move(path.value()), |
||||||
|
/*query_parameter_pairs=*/{}, /*fragment=*/""); |
||||||
|
if (!uri.ok()) return uri.status(); |
||||||
|
return uri->ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace experimental
|
||||||
|
} // namespace grpc_event_engine
|
@ -0,0 +1,85 @@ |
|||||||
|
// Copyright 2022 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.
|
||||||
|
#ifndef GRPC_CORE_LIB_EVENT_ENGINE_TCP_SOCKET_UTILS_H |
||||||
|
#define GRPC_CORE_LIB_EVENT_ENGINE_TCP_SOCKET_UTILS_H |
||||||
|
|
||||||
|
#include <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include <string> |
||||||
|
|
||||||
|
#include "absl/status/statusor.h" |
||||||
|
#include "absl/types/optional.h" |
||||||
|
|
||||||
|
#include <grpc/event_engine/event_engine.h> |
||||||
|
|
||||||
|
namespace grpc_event_engine { |
||||||
|
namespace experimental { |
||||||
|
|
||||||
|
// Returns true if resolved_addr is an IPv4-mapped IPv6 address within the
|
||||||
|
// ::ffff:0.0.0.0/96 range, or false otherwise.
|
||||||
|
// If resolved_addr4_out is non-NULL, the inner IPv4 address will be copied
|
||||||
|
// here when returning true.
|
||||||
|
bool ResolvedAddressIsV4Mapped( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr, |
||||||
|
EventEngine::ResolvedAddress* resolved_addr4_out); |
||||||
|
|
||||||
|
// If resolved_addr is an AF_INET address, writes the corresponding
|
||||||
|
// ::ffff:0.0.0.0/96 address to resolved_addr6_out and returns true. Otherwise
|
||||||
|
// returns false.
|
||||||
|
bool ResolvedAddressToV4Mapped( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr, |
||||||
|
EventEngine::ResolvedAddress* resolved_addr6_out); |
||||||
|
|
||||||
|
// Make wild card IPv6 address with specified port.
|
||||||
|
EventEngine::ResolvedAddress ResolvedAddressMakeWild6(int port); |
||||||
|
|
||||||
|
// Make wild card IPv4 address with specified port.
|
||||||
|
EventEngine::ResolvedAddress ResolvedAddressMakeWild4(int port); |
||||||
|
|
||||||
|
// Given a resolved address, return the port number in the address.
|
||||||
|
int ResolvedAddressGetPort(const EventEngine::ResolvedAddress& resolved_addr); |
||||||
|
|
||||||
|
// Modifies the address, setting the specified port number.
|
||||||
|
// The operation would only succeed if the passed address is an IPv4 or Ipv6
|
||||||
|
// address. Otherwise the function call would abort fail.
|
||||||
|
void ResolvedAddressSetPort(EventEngine::ResolvedAddress& resolved_addr, |
||||||
|
int port); |
||||||
|
|
||||||
|
// Returns the port number associated with the address if the given address is
|
||||||
|
// not a wildcard ipv6 or ipv6 address. Otherwise returns absl::nullopt
|
||||||
|
absl::optional<int> ResolvedAddressIsWildcard( |
||||||
|
const EventEngine::ResolvedAddress& addr); |
||||||
|
|
||||||
|
// Converts a EventEngine::ResolvedAddress into a newly-allocated
|
||||||
|
// human-readable string.
|
||||||
|
// Currently, only the AF_INET, AF_INET6, and AF_UNIX families are
|
||||||
|
// recognized.
|
||||||
|
absl::StatusOr<std::string> ResolvedAddressToString( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr); |
||||||
|
|
||||||
|
// Converts a EventEngine::ResolvedAddress into a newly-allocated
|
||||||
|
// human-readable string. See ResolvedAddressToString.
|
||||||
|
// This functional normalizes, so for example: ::ffff:0.0.0.0/96 IPv6
|
||||||
|
// addresses are displayed as plain IPv4.
|
||||||
|
absl::StatusOr<std::string> ResolvedAddressToNormalizedString( |
||||||
|
const EventEngine::ResolvedAddress& resolved_addr); |
||||||
|
|
||||||
|
// Returns the URI string corresponding to the resolved_address
|
||||||
|
absl::StatusOr<std::string> ResolvedAddressToURI( |
||||||
|
const EventEngine::ResolvedAddress& resolved_address); |
||||||
|
|
||||||
|
} // namespace experimental
|
||||||
|
} // namespace grpc_event_engine
|
||||||
|
|
||||||
|
#endif // GRPC_CORE_LIB_EVENT_ENGINE_TCP_SOCKET_UTILS_H
|
@ -0,0 +1,273 @@ |
|||||||
|
// Copyright 2022 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> |
||||||
|
|
||||||
|
#ifdef GPR_WINDOWS |
||||||
|
|
||||||
|
#include "absl/cleanup/cleanup.h" |
||||||
|
#include "absl/functional/any_invocable.h" |
||||||
|
#include "absl/status/status.h" |
||||||
|
#include "absl/strings/str_format.h" |
||||||
|
|
||||||
|
#include <grpc/event_engine/memory_allocator.h> |
||||||
|
#include <grpc/support/log_windows.h> |
||||||
|
|
||||||
|
#include "src/core/lib/event_engine/tcp_socket_utils.h" |
||||||
|
#include "src/core/lib/event_engine/trace.h" |
||||||
|
#include "src/core/lib/event_engine/windows/windows_endpoint.h" |
||||||
|
#include "src/core/lib/gprpp/debug_location.h" |
||||||
|
#include "src/core/lib/gprpp/status_helper.h" |
||||||
|
#include "src/core/lib/iomgr/error.h" |
||||||
|
|
||||||
|
namespace grpc_event_engine { |
||||||
|
namespace experimental { |
||||||
|
|
||||||
|
// TODO(hork): The previous implementation required internal ref counting. Add
|
||||||
|
// this when it becomes necessary.
|
||||||
|
// TODO(hork): The previous implementation required a 2-phase shutdown. Add this
|
||||||
|
// when it becomes necessary.
|
||||||
|
|
||||||
|
namespace { |
||||||
|
constexpr int64_t kDefaultTargetReadSize = 8192; |
||||||
|
constexpr int kMaxWSABUFCount = 16; |
||||||
|
|
||||||
|
void AbortOnEvent(absl::Status) { |
||||||
|
GPR_ASSERT(false && |
||||||
|
"INTERNAL ERROR: Asked to handle read/write event with an invalid " |
||||||
|
"callback"); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WindowsEndpoint::WindowsEndpoint( |
||||||
|
const EventEngine::ResolvedAddress& peer_address, |
||||||
|
std::unique_ptr<WinSocket> socket, MemoryAllocator&& allocator, |
||||||
|
const EndpointConfig& /* config */, Executor* executor) |
||||||
|
: peer_address_(peer_address), |
||||||
|
socket_(std::move(socket)), |
||||||
|
allocator_(std::move(allocator)), |
||||||
|
handle_read_event_(this), |
||||||
|
handle_write_event_(this), |
||||||
|
executor_(executor) { |
||||||
|
sockaddr addr; |
||||||
|
int addr_len = sizeof(addr); |
||||||
|
if (getsockname(socket_->socket(), &addr, &addr_len) < 0) { |
||||||
|
gpr_log(GPR_ERROR, "Unrecoverable error: Failed to get local socket name."); |
||||||
|
abort(); |
||||||
|
} |
||||||
|
local_address_ = EventEngine::ResolvedAddress(&addr, addr_len); |
||||||
|
local_address_string_ = *ResolvedAddressToURI(local_address_); |
||||||
|
peer_address_string_ = *ResolvedAddressToURI(peer_address_); |
||||||
|
} |
||||||
|
|
||||||
|
WindowsEndpoint::~WindowsEndpoint() { |
||||||
|
socket_->MaybeShutdown(absl::OkStatus()); |
||||||
|
} |
||||||
|
|
||||||
|
void WindowsEndpoint::Read(absl::AnyInvocable<void(absl::Status)> on_read, |
||||||
|
SliceBuffer* buffer, const ReadArgs* args) { |
||||||
|
// TODO(hork): last_read_buffer from iomgr: Is it only garbage, or optimized?
|
||||||
|
GRPC_EVENT_ENGINE_TRACE("WindowsEndpoint::%p reading", this); |
||||||
|
// Prepare the WSABUF struct
|
||||||
|
WSABUF wsa_buffers[kMaxWSABUFCount]; |
||||||
|
int min_read_size = kDefaultTargetReadSize; |
||||||
|
if (args != nullptr && args->read_hint_bytes > 0) { |
||||||
|
min_read_size = args->read_hint_bytes; |
||||||
|
} |
||||||
|
if (buffer->Length() < min_read_size && buffer->Count() < kMaxWSABUFCount) { |
||||||
|
buffer->AppendIndexed(Slice(allocator_.MakeSlice(min_read_size))); |
||||||
|
} |
||||||
|
GPR_ASSERT(buffer->Count() <= kMaxWSABUFCount); |
||||||
|
for (int i = 0; i < buffer->Count(); i++) { |
||||||
|
Slice tmp = buffer->RefSlice(i); |
||||||
|
wsa_buffers[i].buf = (char*)tmp.begin(); |
||||||
|
wsa_buffers[i].len = tmp.size(); |
||||||
|
} |
||||||
|
DWORD bytes_read = 0; |
||||||
|
DWORD flags = 0; |
||||||
|
// First let's try a synchronous, non-blocking read.
|
||||||
|
int status = WSARecv(socket_->socket(), wsa_buffers, (DWORD)buffer->Count(), |
||||||
|
&bytes_read, &flags, nullptr, nullptr); |
||||||
|
int wsa_error = status == 0 ? 0 : WSAGetLastError(); |
||||||
|
// Did we get data immediately ? Yay.
|
||||||
|
if (wsa_error != WSAEWOULDBLOCK) { |
||||||
|
// prune slicebuffer
|
||||||
|
if (bytes_read != buffer->Length()) { |
||||||
|
buffer->RemoveLastNBytes(buffer->Length() - bytes_read); |
||||||
|
} |
||||||
|
executor_->Run([on_read = std::move(on_read)]() mutable { |
||||||
|
on_read(absl::OkStatus()); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
// Otherwise, let's retry, by queuing a read.
|
||||||
|
memset(socket_->read_info()->overlapped(), 0, sizeof(OVERLAPPED)); |
||||||
|
status = |
||||||
|
WSARecv(socket_->socket(), wsa_buffers, (DWORD)buffer->Count(), |
||||||
|
&bytes_read, &flags, socket_->read_info()->overlapped(), nullptr); |
||||||
|
wsa_error = status == 0 ? 0 : WSAGetLastError(); |
||||||
|
if (wsa_error != 0 && wsa_error != WSA_IO_PENDING) { |
||||||
|
// Async read returned immediately with an error
|
||||||
|
executor_->Run([this, on_read = std::move(on_read), wsa_error]() mutable { |
||||||
|
on_read(GRPC_WSA_ERROR( |
||||||
|
wsa_error, |
||||||
|
absl::StrFormat("WindowsEndpont::%p Read failed", this).c_str())); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
handle_read_event_.Prime(buffer, std::move(on_read)); |
||||||
|
socket_->NotifyOnRead(&handle_read_event_); |
||||||
|
} |
||||||
|
|
||||||
|
void WindowsEndpoint::Write(absl::AnyInvocable<void(absl::Status)> on_writable, |
||||||
|
SliceBuffer* data, const WriteArgs* /* args */) { |
||||||
|
if (grpc_event_engine_trace.enabled()) { |
||||||
|
for (int i = 0; i < data->Count(); i++) { |
||||||
|
auto str = data->RefSlice(i).as_string_view(); |
||||||
|
gpr_log(GPR_INFO, "WindowsEndpoint::%p WRITE (peer=%s): %.*s", this, |
||||||
|
peer_address_string_.c_str(), str.length(), str.data()); |
||||||
|
} |
||||||
|
} |
||||||
|
GPR_ASSERT(data->Count() <= UINT_MAX); |
||||||
|
absl::InlinedVector<WSABUF, kMaxWSABUFCount> buffers(data->Count()); |
||||||
|
for (int i = 0; i < data->Count(); i++) { |
||||||
|
auto slice = data->RefSlice(i); |
||||||
|
GPR_ASSERT(slice.size() <= ULONG_MAX); |
||||||
|
buffers[i].len = slice.size(); |
||||||
|
buffers[i].buf = (char*)slice.begin(); |
||||||
|
} |
||||||
|
// First, let's try a synchronous, non-blocking write.
|
||||||
|
DWORD bytes_sent; |
||||||
|
int status = WSASend(socket_->socket(), buffers.data(), (DWORD)buffers.size(), |
||||||
|
&bytes_sent, 0, nullptr, nullptr); |
||||||
|
size_t async_buffers_offset; |
||||||
|
if (status == 0) { |
||||||
|
if (bytes_sent == data->Length()) { |
||||||
|
// Write completed, exiting early
|
||||||
|
executor_->Run( |
||||||
|
[cb = std::move(on_writable)]() mutable { cb(absl::OkStatus()); }); |
||||||
|
return; |
||||||
|
} |
||||||
|
// The data was not completely delivered, we should send the rest of it by
|
||||||
|
// doing an async write operation.
|
||||||
|
for (int i = 0; i < data->Count(); i++) { |
||||||
|
if (buffers[i].len > bytes_sent) { |
||||||
|
buffers[i].buf += bytes_sent; |
||||||
|
buffers[i].len -= bytes_sent; |
||||||
|
break; |
||||||
|
} |
||||||
|
bytes_sent -= buffers[i].len; |
||||||
|
async_buffers_offset++; |
||||||
|
} |
||||||
|
} else { |
||||||
|
// We would kind of expect to get a WSAEWOULDBLOCK here, especially on a
|
||||||
|
// busy connection that has its send queue filled up. But if we don't,
|
||||||
|
// then we can avoid doing an async write operation at all.
|
||||||
|
int wsa_error = WSAGetLastError(); |
||||||
|
if (wsa_error != WSAEWOULDBLOCK) { |
||||||
|
executor_->Run([cb = std::move(on_writable), wsa_error]() mutable { |
||||||
|
cb(GRPC_WSA_ERROR(wsa_error, "WSASend")); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
auto write_info = socket_->write_info(); |
||||||
|
memset(write_info->overlapped(), 0, sizeof(OVERLAPPED)); |
||||||
|
status = WSASend(socket_->socket(), &buffers[async_buffers_offset], |
||||||
|
(DWORD)(data->Count() - async_buffers_offset), nullptr, 0, |
||||||
|
write_info->overlapped(), nullptr); |
||||||
|
|
||||||
|
if (status != 0) { |
||||||
|
int wsa_error = WSAGetLastError(); |
||||||
|
if (wsa_error != WSA_IO_PENDING) { |
||||||
|
executor_->Run([cb = std::move(on_writable), wsa_error]() mutable { |
||||||
|
cb(GRPC_WSA_ERROR(wsa_error, "WSASend")); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
// As all is now setup, we can now ask for the IOCP notification. It may
|
||||||
|
// trigger the callback immediately however, but no matter.
|
||||||
|
handle_write_event_.Prime(data, std::move(on_writable)); |
||||||
|
socket_->NotifyOnWrite(&handle_write_event_); |
||||||
|
} |
||||||
|
const EventEngine::ResolvedAddress& WindowsEndpoint::GetPeerAddress() const { |
||||||
|
return peer_address_; |
||||||
|
} |
||||||
|
const EventEngine::ResolvedAddress& WindowsEndpoint::GetLocalAddress() const { |
||||||
|
return local_address_; |
||||||
|
} |
||||||
|
|
||||||
|
// ---- Handle{Read|Write}Closure
|
||||||
|
|
||||||
|
WindowsEndpoint::BaseEventClosure::BaseEventClosure(WindowsEndpoint* endpoint) |
||||||
|
: endpoint_(endpoint), cb_(&AbortOnEvent) {} |
||||||
|
|
||||||
|
void WindowsEndpoint::HandleReadClosure::Run() { |
||||||
|
GRPC_EVENT_ENGINE_TRACE("WindowsEndpoint::%p Handling Read Event", endpoint_); |
||||||
|
absl::Status status; |
||||||
|
auto* read_info = endpoint_->socket_->read_info(); |
||||||
|
auto cb_cleanup = absl::MakeCleanup([this, &status]() { |
||||||
|
auto cb = std::move(cb_); |
||||||
|
cb_ = &AbortOnEvent; |
||||||
|
cb(status); |
||||||
|
}); |
||||||
|
if (read_info->wsa_error() != 0) { |
||||||
|
status = GRPC_WSA_ERROR(read_info->wsa_error(), "Async Read Error"); |
||||||
|
buffer_->Clear(); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (read_info->bytes_transferred() > 0) { |
||||||
|
GPR_ASSERT(read_info->bytes_transferred() <= buffer_->Length()); |
||||||
|
if (read_info->bytes_transferred() != buffer_->Length()) { |
||||||
|
buffer_->RemoveLastNBytes(buffer_->Length() - |
||||||
|
read_info->bytes_transferred()); |
||||||
|
} |
||||||
|
GPR_ASSERT(read_info->bytes_transferred() == buffer_->Length()); |
||||||
|
if (grpc_event_engine_trace.enabled()) { |
||||||
|
for (int i = 0; i < buffer_->Count(); i++) { |
||||||
|
auto str = buffer_->RefSlice(i).as_string_view(); |
||||||
|
gpr_log(GPR_INFO, "WindowsEndpoint::%p READ (peer=%s): %.*s", this, |
||||||
|
endpoint_->peer_address_string_.c_str(), str.length(), |
||||||
|
str.data()); |
||||||
|
} |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
// Either the endpoint is shut down or we've seen the end of the stream
|
||||||
|
buffer_->Clear(); |
||||||
|
// TODO(hork): different error message if shut down
|
||||||
|
status = absl::UnavailableError("End of TCP stream"); |
||||||
|
} |
||||||
|
|
||||||
|
void WindowsEndpoint::HandleWriteClosure::Run() { |
||||||
|
GRPC_EVENT_ENGINE_TRACE("WindowsEndpoint::%p Handling Write Event", |
||||||
|
endpoint_); |
||||||
|
auto* write_info = endpoint_->socket_->write_info(); |
||||||
|
auto cb = std::move(cb_); |
||||||
|
cb_ = &AbortOnEvent; |
||||||
|
absl::Status status; |
||||||
|
if (write_info->wsa_error() != 0) { |
||||||
|
status = GRPC_WSA_ERROR(write_info->wsa_error(), "WSASend"); |
||||||
|
} else { |
||||||
|
GPR_ASSERT(write_info->bytes_transferred() == buffer_->Length()); |
||||||
|
} |
||||||
|
cb(status); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace experimental
|
||||||
|
} // namespace grpc_event_engine
|
||||||
|
|
||||||
|
#endif // GPR_WINDOWS
|
@ -0,0 +1,94 @@ |
|||||||
|
// Copyright 2022 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.
|
||||||
|
#ifndef GRPC_CORE_LIB_EVENT_ENGINE_WINDOWS_WINDOWS_ENDPOINT_H |
||||||
|
#define GRPC_CORE_LIB_EVENT_ENGINE_WINDOWS_WINDOWS_ENDPOINT_H |
||||||
|
#include <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#ifdef GPR_WINDOWS |
||||||
|
|
||||||
|
#include <grpc/event_engine/event_engine.h> |
||||||
|
|
||||||
|
#include "src/core/lib/event_engine/windows/win_socket.h" |
||||||
|
|
||||||
|
namespace grpc_event_engine { |
||||||
|
namespace experimental { |
||||||
|
|
||||||
|
class WindowsEndpoint : public EventEngine::Endpoint { |
||||||
|
public: |
||||||
|
WindowsEndpoint(const EventEngine::ResolvedAddress& peer_address, |
||||||
|
std::unique_ptr<WinSocket> socket, |
||||||
|
MemoryAllocator&& allocator, const EndpointConfig& config, |
||||||
|
Executor* Executor); |
||||||
|
~WindowsEndpoint() override; |
||||||
|
void Read(absl::AnyInvocable<void(absl::Status)> on_read, SliceBuffer* buffer, |
||||||
|
const ReadArgs* args) override; |
||||||
|
void Write(absl::AnyInvocable<void(absl::Status)> on_writable, |
||||||
|
SliceBuffer* data, const WriteArgs* args) override; |
||||||
|
const EventEngine::ResolvedAddress& GetPeerAddress() const override; |
||||||
|
const EventEngine::ResolvedAddress& GetLocalAddress() const override; |
||||||
|
|
||||||
|
private: |
||||||
|
// Base class for the Read- and Write-specific event handler callbacks
|
||||||
|
class BaseEventClosure : public EventEngine::Closure { |
||||||
|
public: |
||||||
|
explicit BaseEventClosure(WindowsEndpoint* endpoint); |
||||||
|
// Calls the bound application callback, inline.
|
||||||
|
// If called through IOCP, this will be run from within an Executor.
|
||||||
|
virtual void Run() = 0; |
||||||
|
|
||||||
|
// Prepare the closure by setting the application callback and SliceBuffer
|
||||||
|
void Prime(SliceBuffer* buffer, absl::AnyInvocable<void(absl::Status)> cb) { |
||||||
|
cb_ = std::move(cb); |
||||||
|
buffer_ = buffer; |
||||||
|
} |
||||||
|
|
||||||
|
protected: |
||||||
|
absl::AnyInvocable<void(absl::Status)> cb_; |
||||||
|
SliceBuffer* buffer_; |
||||||
|
WindowsEndpoint* endpoint_; |
||||||
|
}; |
||||||
|
|
||||||
|
// Permanent closure type for Read callbacks
|
||||||
|
class HandleReadClosure : public BaseEventClosure { |
||||||
|
public: |
||||||
|
explicit HandleReadClosure(WindowsEndpoint* endpoint) |
||||||
|
: BaseEventClosure(endpoint) {} |
||||||
|
void Run() override; |
||||||
|
}; |
||||||
|
|
||||||
|
// Permanent closure type for Write callbacks
|
||||||
|
class HandleWriteClosure : public BaseEventClosure { |
||||||
|
public: |
||||||
|
explicit HandleWriteClosure(WindowsEndpoint* endpoint) |
||||||
|
: BaseEventClosure(endpoint) {} |
||||||
|
void Run() override; |
||||||
|
}; |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress peer_address_; |
||||||
|
std::string peer_address_string_; |
||||||
|
EventEngine::ResolvedAddress local_address_; |
||||||
|
std::string local_address_string_; |
||||||
|
std::unique_ptr<WinSocket> socket_; |
||||||
|
MemoryAllocator allocator_; |
||||||
|
HandleReadClosure handle_read_event_; |
||||||
|
HandleWriteClosure handle_write_event_; |
||||||
|
Executor* executor_; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace experimental
|
||||||
|
} // namespace grpc_event_engine
|
||||||
|
|
||||||
|
#endif |
||||||
|
|
||||||
|
#endif // GRPC_CORE_LIB_EVENT_ENGINE_WINDOWS_WINDOWS_ENDPOINT_H
|
@ -0,0 +1,283 @@ |
|||||||
|
// Copyright 2022 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/event_engine/tcp_socket_utils.h" |
||||||
|
|
||||||
|
#include <errno.h> |
||||||
|
#include <stdint.h> |
||||||
|
#include <string.h> |
||||||
|
|
||||||
|
// IWYU pragma: no_include <arpa/inet.h>
|
||||||
|
|
||||||
|
#include <string> |
||||||
|
|
||||||
|
#include "src/core/lib/iomgr/port.h" |
||||||
|
|
||||||
|
#ifdef GRPC_HAVE_UNIX_SOCKET |
||||||
|
#include <sys/un.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
#include "absl/status/status.h" |
||||||
|
#include "absl/status/statusor.h" |
||||||
|
#include "gtest/gtest.h" |
||||||
|
|
||||||
|
#include <grpc/event_engine/event_engine.h> |
||||||
|
#include <grpc/support/log.h> |
||||||
|
|
||||||
|
#include "src/core/lib/iomgr/sockaddr.h" |
||||||
|
|
||||||
|
namespace grpc_event_engine { |
||||||
|
namespace experimental { |
||||||
|
|
||||||
|
namespace { |
||||||
|
using ::grpc_event_engine::experimental::EventEngine; |
||||||
|
|
||||||
|
const uint8_t kMapped[] = {0, 0, 0, 0, 0, 0, 0, 0, |
||||||
|
0, 0, 0xff, 0xff, 192, 0, 2, 1}; |
||||||
|
const uint8_t kNotQuiteMapped[] = {0, 0, 0, 0, 0, 0, 0, 0, |
||||||
|
0, 0, 0xff, 0xfe, 192, 0, 2, 99}; |
||||||
|
const uint8_t kIPv4[] = {192, 0, 2, 1}; |
||||||
|
const uint8_t kIPv6[] = {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, |
||||||
|
0, 0, 0, 0, 0, 0, 0, 1}; |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress MakeAddr4(const uint8_t* data, size_t data_len) { |
||||||
|
EventEngine::ResolvedAddress resolved_addr4; |
||||||
|
sockaddr_in* addr4 = reinterpret_cast<sockaddr_in*>( |
||||||
|
const_cast<sockaddr*>(resolved_addr4.address())); |
||||||
|
memset(&resolved_addr4, 0, sizeof(resolved_addr4)); |
||||||
|
addr4->sin_family = AF_INET; |
||||||
|
GPR_ASSERT(data_len == sizeof(addr4->sin_addr.s_addr)); |
||||||
|
memcpy(&addr4->sin_addr.s_addr, data, data_len); |
||||||
|
addr4->sin_port = htons(12345); |
||||||
|
return EventEngine::ResolvedAddress( |
||||||
|
reinterpret_cast<sockaddr*>(addr4), |
||||||
|
static_cast<socklen_t>(sizeof(sockaddr_in))); |
||||||
|
} |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress MakeAddr6(const uint8_t* data, size_t data_len) { |
||||||
|
EventEngine::ResolvedAddress resolved_addr6; |
||||||
|
sockaddr_in6* addr6 = reinterpret_cast<sockaddr_in6*>( |
||||||
|
const_cast<sockaddr*>(resolved_addr6.address())); |
||||||
|
memset(&resolved_addr6, 0, sizeof(resolved_addr6)); |
||||||
|
addr6->sin6_family = AF_INET6; |
||||||
|
GPR_ASSERT(data_len == sizeof(addr6->sin6_addr.s6_addr)); |
||||||
|
memcpy(&addr6->sin6_addr.s6_addr, data, data_len); |
||||||
|
addr6->sin6_port = htons(12345); |
||||||
|
return EventEngine::ResolvedAddress( |
||||||
|
reinterpret_cast<sockaddr*>(addr6), |
||||||
|
static_cast<socklen_t>(sizeof(sockaddr_in6))); |
||||||
|
} |
||||||
|
|
||||||
|
void SetIPv6ScopeId(EventEngine::ResolvedAddress& addr, uint32_t scope_id) { |
||||||
|
sockaddr_in6* addr6 = |
||||||
|
reinterpret_cast<sockaddr_in6*>(const_cast<sockaddr*>(addr.address())); |
||||||
|
ASSERT_EQ(addr6->sin6_family, AF_INET6); |
||||||
|
addr6->sin6_scope_id = scope_id; |
||||||
|
} |
||||||
|
|
||||||
|
#ifdef GRPC_HAVE_UNIX_SOCKET |
||||||
|
absl::StatusOr<EventEngine::ResolvedAddress> UnixSockaddrPopulate( |
||||||
|
absl::string_view path) { |
||||||
|
EventEngine::ResolvedAddress resolved_addr; |
||||||
|
memset(const_cast<sockaddr*>(resolved_addr.address()), 0, |
||||||
|
resolved_addr.size()); |
||||||
|
struct sockaddr_un* un = reinterpret_cast<struct sockaddr_un*>( |
||||||
|
const_cast<sockaddr*>(resolved_addr.address())); |
||||||
|
const size_t maxlen = sizeof(un->sun_path) - 1; |
||||||
|
if (path.size() > maxlen) { |
||||||
|
return absl::InternalError(absl::StrCat( |
||||||
|
"Path name should not have more than ", maxlen, " characters")); |
||||||
|
} |
||||||
|
un->sun_family = AF_UNIX; |
||||||
|
path.copy(un->sun_path, path.size()); |
||||||
|
un->sun_path[path.size()] = '\0'; |
||||||
|
return EventEngine::ResolvedAddress(reinterpret_cast<sockaddr*>(un), |
||||||
|
static_cast<socklen_t>(sizeof(*un))); |
||||||
|
} |
||||||
|
|
||||||
|
absl::StatusOr<EventEngine::ResolvedAddress> UnixAbstractSockaddrPopulate( |
||||||
|
absl::string_view path) { |
||||||
|
EventEngine::ResolvedAddress resolved_addr; |
||||||
|
memset(const_cast<sockaddr*>(resolved_addr.address()), 0, |
||||||
|
resolved_addr.size()); |
||||||
|
struct sockaddr* addr = const_cast<sockaddr*>(resolved_addr.address()); |
||||||
|
struct sockaddr_un* un = reinterpret_cast<struct sockaddr_un*>(addr); |
||||||
|
const size_t maxlen = sizeof(un->sun_path) - 1; |
||||||
|
if (path.size() > maxlen) { |
||||||
|
return absl::InternalError(absl::StrCat( |
||||||
|
"Path name should not have more than ", maxlen, " characters")); |
||||||
|
} |
||||||
|
un->sun_family = AF_UNIX; |
||||||
|
un->sun_path[0] = '\0'; |
||||||
|
path.copy(un->sun_path + 1, path.size()); |
||||||
|
#ifdef GPR_APPLE |
||||||
|
return EventEngine::ResolvedAddress( |
||||||
|
addr, static_cast<socklen_t>(sizeof(un->sun_len) + |
||||||
|
sizeof(un->sun_family) + path.size() + 1)); |
||||||
|
#else |
||||||
|
return EventEngine::ResolvedAddress( |
||||||
|
addr, static_cast<socklen_t>(sizeof(un->sun_family) + path.size() + 1)); |
||||||
|
#endif |
||||||
|
} |
||||||
|
#endif // GRPC_HAVE_UNIX_SOCKET
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(TcpSocketUtilsTest, ResolvedAddressIsV4MappedTest) { |
||||||
|
// v4mapped input should succeed.
|
||||||
|
EventEngine::ResolvedAddress input6 = MakeAddr6(kMapped, sizeof(kMapped)); |
||||||
|
ASSERT_TRUE(ResolvedAddressIsV4Mapped(input6, nullptr)); |
||||||
|
EventEngine::ResolvedAddress output4; |
||||||
|
ASSERT_TRUE(ResolvedAddressIsV4Mapped(input6, &output4)); |
||||||
|
EventEngine::ResolvedAddress expect4 = MakeAddr4(kIPv4, sizeof(kIPv4)); |
||||||
|
ASSERT_EQ(memcmp(expect4.address(), output4.address(), expect4.size()), 0); |
||||||
|
|
||||||
|
// Non-v4mapped input should fail.
|
||||||
|
input6 = MakeAddr6(kNotQuiteMapped, sizeof(kNotQuiteMapped)); |
||||||
|
ASSERT_FALSE(ResolvedAddressIsV4Mapped(input6, nullptr)); |
||||||
|
ASSERT_FALSE(ResolvedAddressIsV4Mapped(input6, &output4)); |
||||||
|
// Output is unchanged.
|
||||||
|
ASSERT_EQ(memcmp(expect4.address(), output4.address(), expect4.size()), 0); |
||||||
|
|
||||||
|
// Plain IPv4 input should also fail.
|
||||||
|
EventEngine::ResolvedAddress input4 = MakeAddr4(kIPv4, sizeof(kIPv4)); |
||||||
|
ASSERT_FALSE(ResolvedAddressIsV4Mapped(input4, nullptr)); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(TcpSocketUtilsTest, ResolvedAddressToV4MappedTest) { |
||||||
|
// IPv4 input should succeed.
|
||||||
|
EventEngine::ResolvedAddress input4 = MakeAddr4(kIPv4, sizeof(kIPv4)); |
||||||
|
EventEngine::ResolvedAddress output6; |
||||||
|
ASSERT_TRUE(ResolvedAddressToV4Mapped(input4, &output6)); |
||||||
|
EventEngine::ResolvedAddress expect6 = MakeAddr6(kMapped, sizeof(kMapped)); |
||||||
|
ASSERT_EQ(memcmp(expect6.address(), output6.address(), output6.size()), 0); |
||||||
|
|
||||||
|
// IPv6 input should fail.
|
||||||
|
EventEngine::ResolvedAddress input6 = MakeAddr6(kIPv6, sizeof(kIPv6)); |
||||||
|
ASSERT_TRUE(!ResolvedAddressToV4Mapped(input6, &output6)); |
||||||
|
// Output is unchanged.
|
||||||
|
ASSERT_EQ(memcmp(expect6.address(), output6.address(), output6.size()), 0); |
||||||
|
|
||||||
|
// Already-v4mapped input should also fail.
|
||||||
|
input6 = MakeAddr6(kMapped, sizeof(kMapped)); |
||||||
|
ASSERT_TRUE(!ResolvedAddressToV4Mapped(input6, &output6)); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(TcpSocketUtilsTest, ResolvedAddressToStringTest) { |
||||||
|
errno = 0x7EADBEEF; |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress input4 = MakeAddr4(kIPv4, sizeof(kIPv4)); |
||||||
|
EXPECT_EQ(ResolvedAddressToString(input4).value(), "192.0.2.1:12345"); |
||||||
|
EventEngine::ResolvedAddress input6 = MakeAddr6(kIPv6, sizeof(kIPv6)); |
||||||
|
EXPECT_EQ(ResolvedAddressToString(input6).value(), "[2001:db8::1]:12345"); |
||||||
|
SetIPv6ScopeId(input6, 2); |
||||||
|
EXPECT_EQ(ResolvedAddressToString(input6).value(), "[2001:db8::1%2]:12345"); |
||||||
|
SetIPv6ScopeId(input6, 101); |
||||||
|
EXPECT_EQ(ResolvedAddressToString(input6).value(), "[2001:db8::1%101]:12345"); |
||||||
|
EventEngine::ResolvedAddress input6x = MakeAddr6(kMapped, sizeof(kMapped)); |
||||||
|
EXPECT_EQ(ResolvedAddressToString(input6x).value(), |
||||||
|
"[::ffff:192.0.2.1]:12345"); |
||||||
|
EventEngine::ResolvedAddress input6y = |
||||||
|
MakeAddr6(kNotQuiteMapped, sizeof(kNotQuiteMapped)); |
||||||
|
EXPECT_EQ(ResolvedAddressToString(input6y).value(), |
||||||
|
"[::fffe:c000:263]:12345"); |
||||||
|
EventEngine::ResolvedAddress phony; |
||||||
|
memset(const_cast<sockaddr*>(phony.address()), 0, phony.size()); |
||||||
|
sockaddr* phony_addr = const_cast<sockaddr*>(phony.address()); |
||||||
|
phony_addr->sa_family = 123; |
||||||
|
EXPECT_EQ(ResolvedAddressToString(phony).status(), |
||||||
|
absl::InvalidArgumentError("Unknown sockaddr family: 123")); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(TcpSocketUtilsTest, ResolvedAddressToNormalizedStringTest) { |
||||||
|
errno = 0x7EADBEEF; |
||||||
|
|
||||||
|
EventEngine::ResolvedAddress input4 = MakeAddr4(kIPv4, sizeof(kIPv4)); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(input4).value(), |
||||||
|
"192.0.2.1:12345"); |
||||||
|
EventEngine::ResolvedAddress input6 = MakeAddr6(kIPv6, sizeof(kIPv6)); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(input6).value(), |
||||||
|
"[2001:db8::1]:12345"); |
||||||
|
SetIPv6ScopeId(input6, 2); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(input6).value(), |
||||||
|
"[2001:db8::1%2]:12345"); |
||||||
|
SetIPv6ScopeId(input6, 101); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(input6).value(), |
||||||
|
"[2001:db8::1%101]:12345"); |
||||||
|
EventEngine::ResolvedAddress input6x = MakeAddr6(kMapped, sizeof(kMapped)); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(input6x).value(), |
||||||
|
"192.0.2.1:12345"); |
||||||
|
EventEngine::ResolvedAddress input6y = |
||||||
|
MakeAddr6(kNotQuiteMapped, sizeof(kNotQuiteMapped)); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(input6y).value(), |
||||||
|
"[::fffe:c000:263]:12345"); |
||||||
|
EventEngine::ResolvedAddress phony; |
||||||
|
memset(const_cast<sockaddr*>(phony.address()), 0, phony.size()); |
||||||
|
sockaddr* phony_addr = const_cast<sockaddr*>(phony.address()); |
||||||
|
phony_addr->sa_family = 123; |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(phony).status(), |
||||||
|
absl::InvalidArgumentError("Unknown sockaddr family: 123")); |
||||||
|
|
||||||
|
#ifdef GRPC_HAVE_UNIX_SOCKET |
||||||
|
EventEngine::ResolvedAddress inputun = |
||||||
|
*UnixSockaddrPopulate("/some/unix/path"); |
||||||
|
struct sockaddr_un* sock_un = reinterpret_cast<struct sockaddr_un*>( |
||||||
|
const_cast<sockaddr*>(inputun.address())); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(inputun).value(), |
||||||
|
"/some/unix/path"); |
||||||
|
std::string max_filepath(sizeof(sock_un->sun_path) - 1, 'x'); |
||||||
|
inputun = *UnixSockaddrPopulate(max_filepath); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(inputun).value(), max_filepath); |
||||||
|
inputun = *UnixSockaddrPopulate(max_filepath); |
||||||
|
sock_un->sun_path[sizeof(sockaddr_un::sun_path) - 1] = 'x'; |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(inputun).status(), |
||||||
|
absl::InvalidArgumentError("UDS path is not null-terminated")); |
||||||
|
EventEngine::ResolvedAddress inputun2 = |
||||||
|
*UnixAbstractSockaddrPopulate("some_unix_path"); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(inputun2).value(), |
||||||
|
absl::StrCat(std::string(1, '\0'), "some_unix_path")); |
||||||
|
std::string max_abspath(sizeof(sock_un->sun_path) - 1, '\0'); |
||||||
|
EventEngine::ResolvedAddress inputun3 = |
||||||
|
*UnixAbstractSockaddrPopulate(max_abspath); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(inputun3).value(), |
||||||
|
absl::StrCat(std::string(1, '\0'), max_abspath)); |
||||||
|
#endif |
||||||
|
} |
||||||
|
|
||||||
|
TEST(TcpSocketUtilsTest, SockAddrPortTest) { |
||||||
|
EventEngine::ResolvedAddress wild6 = ResolvedAddressMakeWild6(20); |
||||||
|
EventEngine::ResolvedAddress wild4 = ResolvedAddressMakeWild4(20); |
||||||
|
// Verify the string description matches the expected wildcard address with
|
||||||
|
// correct port number.
|
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(wild6).value(), "[::]:20"); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(wild4).value(), "0.0.0.0:20"); |
||||||
|
// Update the port values.
|
||||||
|
ResolvedAddressSetPort(wild4, 21); |
||||||
|
ResolvedAddressSetPort(wild6, 22); |
||||||
|
// Read back the port values.
|
||||||
|
EXPECT_EQ(ResolvedAddressGetPort(wild4), 21); |
||||||
|
EXPECT_EQ(ResolvedAddressGetPort(wild6), 22); |
||||||
|
// Ensure the string description reflects the updated port values.
|
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(wild4).value(), "0.0.0.0:21"); |
||||||
|
EXPECT_EQ(ResolvedAddressToNormalizedString(wild6).value(), "[::]:22"); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace experimental
|
||||||
|
} // namespace grpc_event_engine
|
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
::testing::InitGoogleTest(&argc, argv); |
||||||
|
return RUN_ALL_TESTS(); |
||||||
|
} |
@ -0,0 +1,170 @@ |
|||||||
|
// Copyright 2022 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> |
||||||
|
|
||||||
|
#ifdef GPR_WINDOWS |
||||||
|
|
||||||
|
#include <gtest/gtest.h> |
||||||
|
|
||||||
|
#include "absl/status/status.h" |
||||||
|
|
||||||
|
#include <grpc/event_engine/event_engine.h> |
||||||
|
#include <grpc/grpc.h> |
||||||
|
|
||||||
|
#include "src/core/lib/event_engine/channel_args_endpoint_config.h" |
||||||
|
#include "src/core/lib/event_engine/thread_pool.h" |
||||||
|
#include "src/core/lib/event_engine/windows/iocp.h" |
||||||
|
#include "src/core/lib/event_engine/windows/windows_endpoint.h" |
||||||
|
#include "src/core/lib/gprpp/notification.h" |
||||||
|
#include "src/core/lib/resource_quota/memory_quota.h" |
||||||
|
#include "test/core/event_engine/windows/create_sockpair.h" |
||||||
|
|
||||||
|
namespace grpc_event_engine { |
||||||
|
namespace experimental { |
||||||
|
|
||||||
|
using namespace std::chrono_literals; |
||||||
|
|
||||||
|
class WindowsEndpointTest : public testing::Test {}; |
||||||
|
|
||||||
|
TEST_F(WindowsEndpointTest, BasicCommunication) { |
||||||
|
// TODO(hork): deduplicate against winsocket and iocp tests
|
||||||
|
// Setup
|
||||||
|
ThreadPool executor; |
||||||
|
IOCP iocp(&executor); |
||||||
|
grpc_core::MemoryQuota quota("endpoint_test"); |
||||||
|
SOCKET sockpair[2]; |
||||||
|
CreateSockpair(sockpair, IOCP::GetDefaultSocketFlags()); |
||||||
|
auto wrapped_client_socket = iocp.Watch(sockpair[0]); |
||||||
|
auto wrapped_server_socket = iocp.Watch(sockpair[1]); |
||||||
|
sockaddr_in loopback_addr = GetSomeIpv4LoopbackAddress(); |
||||||
|
EventEngine::ResolvedAddress addr((sockaddr*)&loopback_addr, |
||||||
|
sizeof(loopback_addr)); |
||||||
|
WindowsEndpoint client(addr, std::move(wrapped_client_socket), |
||||||
|
quota.CreateMemoryAllocator("client"), |
||||||
|
ChannelArgsEndpointConfig(), &executor); |
||||||
|
WindowsEndpoint server(addr, std::move(wrapped_server_socket), |
||||||
|
quota.CreateMemoryAllocator("server"), |
||||||
|
ChannelArgsEndpointConfig(), &executor); |
||||||
|
// Test
|
||||||
|
std::string message = "0xDEADBEEF"; |
||||||
|
grpc_core::Notification read_done; |
||||||
|
SliceBuffer read_buffer; |
||||||
|
server.Read( |
||||||
|
[&read_done, &message, &read_buffer](absl::Status status) { |
||||||
|
ASSERT_EQ(read_buffer.Count(), 1); |
||||||
|
auto slice = read_buffer.TakeFirst(); |
||||||
|
EXPECT_EQ(slice.as_string_view(), message); |
||||||
|
read_done.Notify(); |
||||||
|
}, |
||||||
|
&read_buffer, nullptr); |
||||||
|
grpc_core::Notification write_done; |
||||||
|
SliceBuffer write_buffer; |
||||||
|
write_buffer.Append(Slice::FromCopiedString(message)); |
||||||
|
client.Write([&write_done](absl::Status status) { write_done.Notify(); }, |
||||||
|
&write_buffer, nullptr); |
||||||
|
iocp.Work(5s, []() {}); |
||||||
|
// Cleanup
|
||||||
|
write_done.WaitForNotification(); |
||||||
|
read_done.WaitForNotification(); |
||||||
|
executor.Quiesce(); |
||||||
|
} |
||||||
|
|
||||||
|
TEST_F(WindowsEndpointTest, Conversation) { |
||||||
|
// Setup
|
||||||
|
ThreadPool executor; |
||||||
|
IOCP iocp(&executor); |
||||||
|
grpc_core::MemoryQuota quota("endpoint_test"); |
||||||
|
SOCKET sockpair[2]; |
||||||
|
CreateSockpair(sockpair, IOCP::GetDefaultSocketFlags()); |
||||||
|
sockaddr_in loopback_addr = GetSomeIpv4LoopbackAddress(); |
||||||
|
EventEngine::ResolvedAddress addr((sockaddr*)&loopback_addr, |
||||||
|
sizeof(loopback_addr)); |
||||||
|
// Test
|
||||||
|
struct AppState { |
||||||
|
AppState(const EventEngine::ResolvedAddress& addr, |
||||||
|
std::unique_ptr<WinSocket> client, |
||||||
|
std::unique_ptr<WinSocket> server, grpc_core::MemoryQuota& quota, |
||||||
|
Executor& executor) |
||||||
|
: client(addr, std::move(client), quota.CreateMemoryAllocator("client"), |
||||||
|
ChannelArgsEndpointConfig(), &executor), |
||||||
|
server(addr, std::move(server), quota.CreateMemoryAllocator("server"), |
||||||
|
ChannelArgsEndpointConfig(), &executor) {} |
||||||
|
grpc_core::Notification done; |
||||||
|
WindowsEndpoint client; |
||||||
|
WindowsEndpoint server; |
||||||
|
SliceBuffer read_buffer; |
||||||
|
SliceBuffer write_buffer; |
||||||
|
const std::vector<std::string> messages{ |
||||||
|
"Java is to Javascript what car is to carpet. -Heilmann", |
||||||
|
"Make it work, make it right, make it fast. -Beck", |
||||||
|
"First, solve the problem. Then write the code. -Johnson", |
||||||
|
"It works on my machine."}; |
||||||
|
// incremented after a corresponding read of a previous write
|
||||||
|
// if exchange%2 == 0, client -> server
|
||||||
|
// if exchange%2 == 1, server -> client
|
||||||
|
// if exchange == messages.length, done
|
||||||
|
std::atomic<int> exchange{0}; |
||||||
|
|
||||||
|
// Initiates a Write and corresponding Read on two endpoints.
|
||||||
|
void WriteAndQueueReader(WindowsEndpoint* writer, WindowsEndpoint* reader) { |
||||||
|
write_buffer.Clear(); |
||||||
|
write_buffer.Append(Slice::FromCopiedString(messages[exchange])); |
||||||
|
writer->Write([](absl::Status) {}, &write_buffer, /*args=*/nullptr); |
||||||
|
auto cb = [this](absl::Status status) { ReadCB(status); }; |
||||||
|
read_buffer.Clear(); |
||||||
|
reader->Read(cb, &read_buffer, /*args=*/nullptr); |
||||||
|
} |
||||||
|
|
||||||
|
// Asserts that the received string matches, then queues the next Write/Read
|
||||||
|
// pair
|
||||||
|
void ReadCB(absl::Status status) { |
||||||
|
ASSERT_EQ(read_buffer.Count(), 1); |
||||||
|
ASSERT_EQ(read_buffer.TakeFirst().as_string_view(), messages[exchange]); |
||||||
|
if (++exchange == messages.size()) { |
||||||
|
done.Notify(); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (exchange % 2 == 0) { |
||||||
|
WriteAndQueueReader(/*writer=*/&client, /*reader=*/&server); |
||||||
|
} else { |
||||||
|
WriteAndQueueReader(/*writer=*/&server, /*reader=*/&client); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
AppState state(addr, /*client=*/iocp.Watch(sockpair[0]), |
||||||
|
/*server=*/iocp.Watch(sockpair[1]), quota, executor); |
||||||
|
state.WriteAndQueueReader(/*writer=*/&state.client, /*reader=*/&state.server); |
||||||
|
while (iocp.Work(100ms, []() {}) == Poller::WorkResult::kOk || |
||||||
|
!state.done.HasBeenNotified()) { |
||||||
|
} |
||||||
|
// Cleanup
|
||||||
|
state.done.WaitForNotification(); |
||||||
|
executor.Quiesce(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace experimental
|
||||||
|
} // namespace grpc_event_engine
|
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
::testing::InitGoogleTest(&argc, argv); |
||||||
|
grpc_init(); |
||||||
|
int status = RUN_ALL_TESTS(); |
||||||
|
grpc_shutdown(); |
||||||
|
return status; |
||||||
|
} |
||||||
|
|
||||||
|
#else // not GPR_WINDOWS
|
||||||
|
int main(int /* argc */, char** /* argv */) { return 0; } |
||||||
|
#endif |
Loading…
Reference in new issue