blob: 02c58a3a935d0633e5aa0e6a4cbdb755a39f441a [file]
// Copyright 2024 Google Inc. All Rights Reserved.
//
// 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 "ipc_service.h"
#include <stdio.h>
#include <string.h>
#include "ipc_utils.h"
#include "test.h"
#ifndef _WIN32
#include <pthread.h>
#endif
#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#endif
namespace {
// Alternatives to ASSERT_{STREQ,STRNE}("", str) that
// works correctly if the str argument contains an initial \0.
#define ASSERT_STRING_EMPTY(str) ASSERT_EQ(std::string(), str)
#define ASSERT_STRING_NOT_EMPTY(str) ASSERT_NE(std::string(), str)
#define IPC_ERROR_CONTEXT(prefix) \
prefix " did_timeout=" << did_timeout << ", error: " << error_message
ScopedHandle::NativeType CreateTestNativeHandle() {
#ifdef _WIN32
return CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, 0, NULL);
#else
return open("/dev/null", O_RDWR);
#endif
}
ScopedHandle CreateTestHandle() {
return { CreateTestNativeHandle() };
}
// Create a pipe for testing. On success return true and
// set |*read| and |*write| to be the read and write ends
// of the pipe. Note that the pipe is unidirectional due
// to portability with Windows.
bool CreateTestPipe(ScopedHandle* read, ScopedHandle* write) {
std::string error;
if (!ScopedHandle::CreatePipe(read, write, &error)) {
fprintf(stderr, "ERROR: when creating pipe: %s\n", error.c_str());
return false;
}
return true;
}
// A background thread that will try to connect to a given IpcService
// instance for a given timeout, then report the result. Usage is:
//
// - Create instance, passing service name and timeout in milliseconds.
// - Call Start() to start the thread in the background.
// - Call Join() to wait for its completion.
// - Access |handle|, |did_timeout| and |error_message| members directly
// to get the result of the operation.
//
// This is necessary because on Windows, a ConnectTo() will always timeout
// or fail if there was no prior AcceptPeer() from another thread. This
// does not happen on Posix though.
struct TestIpcServiceConnectingThread {
TestIpcServiceConnectingThread(StringPiece service_name, int64_t timeout_ms)
: service_name_(service_name.AsString()), timeout_ms_(timeout_ms) {}
void Start() {
#ifdef _WIN32
thread_ = CreateThread(nullptr, 65536, MainRoutine, this, 0, nullptr);
ASSERT_NE(thread_, INVALID_HANDLE_VALUE) << GetLastErrorString();
#else // !_WIN32
int ret = pthread_create(&thread_, nullptr, MainRoutine, this);
ASSERT_EQ(ret, 0) << strerror(ret);
#endif // !_WIN32
}
void Join() {
#ifdef _WIN32
(void)WaitForSingleObject(thread_, INFINITE);
#else // !_WIN32
void* ignored = nullptr;
pthread_join(thread_, &ignored);
#endif // !_WIN32
}
private:
const std::string service_name_;
const int64_t timeout_ms_;
#ifdef _WIN32
HANDLE thread_ = INVALID_HANDLE_VALUE;
static DWORD WINAPI MainRoutine(void* arg) {
reinterpret_cast<TestIpcServiceConnectingThread*>(arg)->RunInBackground();
return 0;
}
#else // !_WIN32
pthread_t thread_;
static void* MainRoutine(void* arg) {
reinterpret_cast<TestIpcServiceConnectingThread*>(arg)->RunInBackground();
return nullptr;
}
#endif // !_WIN32
void RunInBackground() {
handle = IpcService::ConnectTo(service_name_, timeout_ms_, &did_timeout,
&error_message);
}
public:
ScopedHandle handle;
bool did_timeout = false;
std::string error_message;
};
} // namespace
TEST(IpcService, ServerHandleIsNotInheritable) {
std::string service_name = "test_service";
std::string error_message;
ASSERT_FALSE(IpcService::HasServer(service_name));
// Start server
error_message.clear();
IpcService service = IpcService::StartServer(service_name, &error_message);
ASSERT_TRUE(service) << "StartServer(): " << error_message;
ASSERT_FALSE(service.GetServerHandleForTest().IsInheritable());
ASSERT_STRING_EMPTY(error_message);
ASSERT_TRUE(service.IsValid());
}
TEST(IpcService, StartConnectAndAccept) {
std::string service_name = "test_service";
std::string error_message;
// Tring to connect to a service without a server should fail.
bool did_timeout = false;
ScopedHandle no_client =
IpcService::ConnectTo(service_name, -1, &did_timeout, &error_message);
ASSERT_FALSE(no_client) << IPC_ERROR_CONTEXT("ConnectTo");
ASSERT_FALSE(did_timeout);
ASSERT_STRING_NOT_EMPTY(error_message);
error_message.clear();
ASSERT_FALSE(IpcService::HasServer(service_name));
// Start server
error_message.clear();
IpcService service = IpcService::StartServer(service_name, &error_message);
ASSERT_TRUE(service) << "StartServer(): " << error_message;
ASSERT_STRING_EMPTY(error_message);
ASSERT_TRUE(service.IsValid());
ASSERT_TRUE(IpcService::HasServer(service_name));
// Trying to create a secondary server for the same service should fail.
IpcService no_service = IpcService::StartServer(service_name, &error_message);
ASSERT_FALSE(no_service);
ASSERT_STRING_NOT_EMPTY(error_message);
error_message.clear();
ASSERT_TRUE(IpcService::HasServer(service_name));
// Start a connecting thread in the background with a large timeout.
TestIpcServiceConnectingThread client(service_name, 1000);
client.Start();
// Accept new client connection on the server side, this should succeed
// immediately, except if the background thread has not been scheduled
// yet, so use a minimal timeout of 1 millisecond to ensure this happens.
did_timeout = false;
ScopedHandle peer = service.AcceptPeer(1, &did_timeout, &error_message);
ASSERT_TRUE(peer) << IPC_ERROR_CONTEXT("AcceptPeer");
ASSERT_FALSE(did_timeout);
ASSERT_STRING_EMPTY(error_message);
// Connect to the server, and receive a client handle.
// Note that it will not be possible to write to it until the server
// has called AcceptPeer() on its service instance.
client.Join();
ASSERT_TRUE(client.handle) << "client did_timeout=" << client.did_timeout
<< ", error: " << client.error_message;
ASSERT_FALSE(client.did_timeout);
ASSERT_STRING_EMPTY(client.error_message);
// Send data from the client to the peer.
const char kInput[] = "sending data";
const size_t kInputSize = sizeof(kInput);
ASSERT_TRUE(client.handle.WriteFull(kInput, kInputSize, &error_message));
ASSERT_STRING_EMPTY(error_message);
char output[kInputSize] = {};
ASSERT_TRUE(peer.ReadFull(output, sizeof(output), &error_message));
ASSERT_STRING_EMPTY(error_message);
ASSERT_TRUE(!memcmp(output, kInput, kInputSize));
// Close all handles and verify that IsBound() returns false.
// Note that on Unix, only closing the server handle is necessary for this,
// but the way Win32 named pipes work require all client handles closed too.
peer.Close();
client.handle.Close();
service.Stop();
ASSERT_FALSE(IpcService::HasServer(service_name));
}
TEST(IpcService, StartAndAcceptWithTimeout) {
std::string error_message;
StringPiece service_name = "test_service";
// Create server
IpcService server = IpcService::StartServer(service_name, &error_message);
ASSERT_TRUE(server) << error_message;
ASSERT_STRING_EMPTY(error_message);
// Accepting should time out since no client is trying to connect.
bool did_timeout = false;
ScopedHandle connection = server.AcceptPeer(10, &did_timeout, &error_message);
ASSERT_FALSE(connection) << IPC_ERROR_CONTEXT("AcceptPeer");
ASSERT_TRUE(did_timeout);
ASSERT_STRING_NOT_EMPTY(error_message);
error_message.clear();
did_timeout = false;
// Start a connecting thread in the background with a large timeout.
TestIpcServiceConnectingThread client(service_name, 1000);
client.Start();
// Accepting should now work.
ScopedHandle peer = server.AcceptPeer(1, &did_timeout, &error_message);
ASSERT_TRUE(peer) << IPC_ERROR_CONTEXT("AcceptPeer");
ASSERT_FALSE(did_timeout);
ASSERT_STRING_EMPTY(error_message);
// Connect to the server, and receive a peer handle.
client.Join();
ASSERT_TRUE(client.handle) << "client did_timeout=" << client.did_timeout
<< ", error: " << client.error_message;
ASSERT_FALSE(client.did_timeout);
ASSERT_STRING_EMPTY(client.error_message);
}
TEST(IpcService, StartAndSendLoop) {
std::string service_name = "test_service";
std::string error_message;
// Start server.
IpcService server = IpcService::StartServer(service_name, &error_message);
ASSERT_TRUE(server) << "StartServer: " << error_message;
ASSERT_STRING_EMPTY(error_message);
ASSERT_TRUE(server.IsValid());
ASSERT_TRUE(IpcService::HasServer(service_name));
for (size_t count = 0; count < 10; ++count) {
std::string loop_info = std::string("#") + std::to_string(count) + ": ";
// Start a first client in a background thread.
TestIpcServiceConnectingThread client1(service_name, 1000);
client1.Start();
// Accept it as first peer. Give 10ms timeout to allow thread
// to be scheduled.
bool did_timeout = false;
ScopedHandle peer1 = server.AcceptPeer(10, &did_timeout, &error_message);
ASSERT_TRUE(peer1) << loop_info << IPC_ERROR_CONTEXT("AcceptPeer #1");
ASSERT_FALSE(did_timeout);
ASSERT_STRING_EMPTY(error_message);
client1.Join();
ASSERT_TRUE(client1.handle) << loop_info;
// Send data through the first pipe from peer to client, then close the
// peer handle, but do not close the client handle. This should maintain
// the pipe buffer in kernel memory.
const char kMessage[] = "One, Two, Three, Four";
ASSERT_TRUE(peer1.WriteFull(kMessage, sizeof(kMessage), &error_message))
<< loop_info << error_message;
peer1.Close();
// Create second client in background thread.
TestIpcServiceConnectingThread client2(service_name, 1000);
client2.Start();
// Accept it as second peer.
ScopedHandle peer2 = server.AcceptPeer(10, &did_timeout, &error_message);
ASSERT_TRUE(peer2) << loop_info << IPC_ERROR_CONTEXT("AcceptPeer #2");
ASSERT_FALSE(did_timeout);
ASSERT_STRING_EMPTY(error_message);
client2.Join();
ASSERT_TRUE(client2.handle);
// Receive data from the first pipe.
char buffer1[sizeof(kMessage)] = {};
ASSERT_TRUE(
client1.handle.ReadFull(buffer1, sizeof(kMessage), &error_message))
<< loop_info << error_message;
ASSERT_STREQ(kMessage, buffer1) << loop_info;
const char kMessage2[] = "Uno, Dos, Tres, Quatro";
ASSERT_TRUE(peer2.WriteFull(kMessage2, sizeof(kMessage2), &error_message))
<< loop_info << error_message;
peer2.Close();
char buffer2[sizeof(kMessage2)] = {};
ASSERT_TRUE(
client2.handle.ReadFull(buffer2, sizeof(kMessage2), &error_message))
<< loop_info << error_message;
ASSERT_STREQ(kMessage2, buffer2) << loop_info;
}
}
TEST(IpcService, SendAndReceiveNativeHandle) {
ScopedHandle pipe1_read;
ScopedHandle pipe1_write;
ASSERT_TRUE(CreateTestPipe(&pipe1_read, &pipe1_write));
std::string error_message;
StringPiece service_name = "test_service";
IpcService server = IpcService::StartServer(service_name, &error_message);
ASSERT_TRUE(server) << error_message;
ASSERT_STRING_EMPTY(error_message);
// Start a connecting thread in the background with a large timeout.
TestIpcServiceConnectingThread client(service_name, 1000);
client.Start();
bool did_timeout = false;
ScopedHandle peer = server.AcceptPeer(-1, &did_timeout, &error_message);
ASSERT_TRUE(peer) << error_message;
ASSERT_FALSE(did_timeout);
ASSERT_STRING_EMPTY(error_message);
client.Join();
ASSERT_TRUE(client.handle) << "client did_timeout=" << client.did_timeout
<< ", error: " << client.error_message;
ASSERT_FALSE(client.did_timeout);
ASSERT_STRING_EMPTY(client.error_message);
// Send pipe1_write.get() from client to peer.
ASSERT_TRUE(
RemoteSendNativeHandle(client.handle, pipe1_write.get(), &error_message))
<< error_message;
ASSERT_STRING_EMPTY(error_message);
ScopedHandle received;
ASSERT_TRUE(RemoteReceiveNativeHandle(peer, &received, &error_message))
<< error_message;
ASSERT_STRING_EMPTY(error_message);
// Write to |received|, this should send data to the first pipe.
const char kInputData[] = "Bonjour monde!";
const size_t kInputDataSize = sizeof(kInputData);
ASSERT_TRUE(received.WriteFull(kInputData, kInputDataSize, &error_message));
ASSERT_STRING_EMPTY(error_message);
char data[kInputDataSize] = {};
ssize_t count = pipe1_read.Read(data, sizeof(data), &error_message);
ASSERT_EQ(static_cast<ssize_t>(sizeof(data)), count);
ASSERT_STRING_EMPTY(error_message);
// Verify content.
ASSERT_TRUE(!memcmp(data, kInputData, kInputDataSize));
}