blob: b1de0dc3c682052c519f1747030e634e56d3fe3b [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/fdio/io.h>
#include <lib/fdio/spawn.h>
#include <lib/zx/process.h>
#include <lib/zx/time.h>
#include <zircon/processargs.h>
#include <map>
#include <string>
#include <vector>
#include <fbl/unique_fd.h>
#include <linenoise/linenoise.h>
#include <zxtest/zxtest.h>
namespace {
constexpr char kRunChildFlag[] = "--run-child-main";
void AddPipe(fdio_spawn_action_t* action, int target_fd, int* fd_out) {
zx_handle_t handle = ZX_HANDLE_INVALID;
zx_status_t status = fdio_pipe_half(fd_out, &handle);
ASSERT_OK(status);
action->action = FDIO_SPAWN_ACTION_ADD_HANDLE;
action->h.id = PA_HND(PA_FD, target_fd);
action->h.handle = handle;
}
struct MultiprocessInfo {
fbl::unique_fd stdin_write;
fbl::unique_fd stdout_read;
zx::process child;
};
std::unique_ptr<MultiprocessInfo> RunChild(const char* child_main) {
std::unique_ptr<MultiprocessInfo> ret(new MultiprocessInfo);
constexpr size_t kActionCount = 3;
fdio_spawn_action_t actions[kActionCount];
int stdin_parent_side = -1;
AddPipe(&actions[0], STDIN_FILENO, &stdin_parent_side);
ret->stdin_write.reset(stdin_parent_side);
int stdout_parent_side = -1;
AddPipe(&actions[1], STDOUT_FILENO, &stdout_parent_side);
ret->stdout_read.reset(stdout_parent_side);
actions[2].action = FDIO_SPAWN_ACTION_CLONE_FD;
actions[2].fd.local_fd = STDERR_FILENO;
actions[2].fd.target_fd = STDERR_FILENO;
// Pass the filesystem namespace, parent environment, and default job to the child, but don't
// include any other file handles, preferring to set them up explicitly.
uint32_t flags = FDIO_SPAWN_CLONE_ALL & ~FDIO_SPAWN_CLONE_STDIO;
const char* root_dir = getenv("TEST_ROOT_DIR");
if (root_dir == nullptr) {
root_dir = "";
}
std::string command = std::string(root_dir) + "/test/linenoise-test";
std::vector<const char*> argv;
argv.push_back(command.c_str());
argv.push_back(kRunChildFlag);
argv.push_back(child_main);
argv.push_back(nullptr);
char error_message[FDIO_SPAWN_ERR_MSG_MAX_LENGTH];
zx::process child;
zx_status_t status =
fdio_spawn_etc(ZX_HANDLE_INVALID, flags, command.c_str(), argv.data(), nullptr, kActionCount,
actions, child.reset_and_get_address(), error_message);
ZX_ASSERT(status == ZX_OK);
ret->child = std::move(child);
return ret;
}
int64_t JoinChild(std::unique_ptr<MultiprocessInfo> info) {
zx_status_t status = info->child.wait_one(ZX_TASK_TERMINATED, zx::time::infinite(), nullptr);
ZX_ASSERT(status == ZX_OK);
zx_info_process_t proc_info{};
status = info->child.get_info(ZX_INFO_PROCESS, &proc_info, sizeof(proc_info), nullptr, nullptr);
ZX_ASSERT(status == ZX_OK);
return proc_info.return_code;
}
std::map<std::string, int (*)()>& GetChildMainFunctionMap() {
static auto* map = new std::map<std::string, int (*)()>();
return *map;
}
class AppendChildMain {
public:
AppendChildMain(const std::string& test_name, int (*main_function_pointer)()) {
ZX_ASSERT(GetChildMainFunctionMap().find(test_name) == GetChildMainFunctionMap().end());
GetChildMainFunctionMap()[test_name] = main_function_pointer;
}
};
#define CHILD_MAIN(test_main) \
int test_main(); \
namespace { \
AppendChildMain AppendChildMain##_##test_main(#test_main, (test_main)); \
} /* namespace */ \
int test_main()
std::string ReadString(MultiprocessInfo* child) {
char buf[4096];
ssize_t bytes_read = read(child->stdout_read.get(), buf, sizeof(buf));
ZX_ASSERT(bytes_read > 0);
ZX_ASSERT(bytes_read < static_cast<ssize_t>(std::size(buf) - 1));
buf[bytes_read] = 0;
return std::string(buf);
}
// Emulate typing something in.
void Send(MultiprocessInfo* child, const std::string& input) {
for (char c : input) {
write(child->stdin_write.get(), &c, 1);
usleep(100);
}
}
CHILD_MAIN(TestWritingToStdout) {
printf("hello");
return 0;
}
TEST(Linenoise, TestMultiprocessHelper) {
auto child = RunChild("TestWritingToStdout");
ASSERT_EQ(ReadString(child.get()), "hello");
ASSERT_EQ(JoinChild(std::move(child)), 0);
}
CHILD_MAIN(RunLinenoiseWithLongPrompt) {
linenoiseHistorySetMaxLen(10);
std::string long_str(1000, 'X');
char* line = linenoise(long_str.c_str());
linenoiseFree(line);
return 0;
}
// Test for reproduction in fxbug.dev/33554 where a long prompt caused a crash.
TEST(Linenoise, CrashLongPrompt) {
auto child = RunChild("RunLinenoiseWithLongPrompt");
// linenoise requests information terminal information here, we have to stub out responses (always
// returning a cursor position of 10, 100 when it asks).
// Handle initial request for console position.
char buf[32];
ASSERT_EQ(read(child->stdout_read.get(), buf, 4), 4); // \x1b[6n
sprintf(buf, "\x1b[%d;%dR", 10, 100);
write(child->stdin_write.get(), buf, strlen(buf));
// Handle moving followed by request.
ASSERT_EQ(read(child->stdout_read.get(), buf, 6), 6); // \x1b[999C
ASSERT_EQ(read(child->stdout_read.get(), buf, 4), 4); // \x1b[6n
sprintf(buf, "\x1b[%d;%dR", 10, 100);
write(child->stdin_write.get(), buf, strlen(buf));
// Send some input with the long prompt (set by the child process).
Send(child.get(), "l\n");
// And ensure that the process didn't crash (for example, with an exit code of
// ZX_TASK_RETCODE_EXCEPTION_KILL).
ASSERT_EQ(JoinChild(std::move(child)), 0);
}
} // namespace
extern "C" {
// Encourage linenoise to go through the same path as normal at-a-console interactions.
int isatty(int fd) { return 1; }
}
int main(int argc, char** argv) {
if (argc == 3 && strcmp(argv[1], kRunChildFlag) == 0) {
// Thunk back into a pseudo-main for a child process if a special flag is specified.
ZX_ASSERT(GetChildMainFunctionMap().find(argv[2]) != GetChildMainFunctionMap().end());
return GetChildMainFunctionMap().find(argv[2])->second();
}
return RUN_ALL_TESTS(argc, argv);
}