blob: ba3f7a9fff1ef0e430245e844e2ce0bc1d78370b [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 "core/conf.h"
#include "core/htrace.h"
#include "test/mini_htraced.h"
#include "test/temp_dir.h"
#include "test/test_config.h"
#include "test/test.h"
#include "util/log.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <json_object.h>
#include <json_tokener.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
/**
* The maximum size of the notification data sent from the htraced daemon on
* startup.
*/
#define MAX_NDATA 65536
/**
* The separator to use in between paths. TODO: portability
*/
#define PATH_LIST_SEP ':'
/**
* The maximum number of arguments when launching an external process.
*/
#define MAX_LAUNCH_ARGS 32
/**
* Retry an operation that may get EINTR.
* The operation must return a non-negative value on success.
*/
#define RETRY_ON_EINTR(ret, expr) do { \
ret = expr; \
if (ret >= 0) \
break; \
} while (errno == EINTR);
#define MINI_HTRACED_LAUNCH_REDIRECT_FDS 0x1
static void mini_htraced_open_snsock(struct mini_htraced *ht, char *err,
size_t err_len);
static void mini_htraced_write_conf_file(struct mini_htraced *ht,
char *err, size_t err_len);
static int mini_htraced_write_conf_key(FILE *fp, const char *key,
const char *fmt, ...)
__attribute__((format(printf, 3, 4)));
static void mini_htraced_launch_daemon(struct mini_htraced *ht,
char *err, size_t err_len);
/**
* Launch an external process.
*
* @param ht The mini htraced object.
* @param path The binary to launch
* @param err (out param) the error, or empty string on success.
* @param err_len The length of the error buffer.
* @param flags The flags.
* MINI_HTRACED_LAUNCH_REDIRECT_FDS: redirect
* stderr, stdout, stdin to null.
* finished successfully.
* @param ... Additional arguments to pass to the process.
* NULL_terminated. The first argument will
* always be the path to the binary.
*
* @return The new process ID, on success. -1 on failure.
* The error string will always be set on failure.
*/
pid_t mini_htraced_launch(const struct mini_htraced *ht, const char *path,
char *err, size_t err_len, int flags, ...)
__attribute__((sentinel));
static void mini_htraced_read_startup_notification(struct mini_htraced *ht,
char *err, size_t err_len);
static void parse_startup_notification(struct mini_htraced *ht,
char *ndata, size_t ndata_len,
char *err, size_t err_len);
void mini_htraced_build(const struct mini_htraced_params *params,
struct mini_htraced **hret,
char *err, size_t err_len)
{
struct mini_htraced *ht = NULL;
int i, ret;
err[0] = '\0';
ht = calloc(1, sizeof(*ht));
if (!ht) {
snprintf(err, err_len, "out of memory allocating mini_htraced object");
goto done;
}
ht->snsock = -1;
ht->root_dir = create_tempdir(params->name, 0777, err, err_len);
if (err[0]) {
goto done;
}
ret = register_tempdir_for_cleanup(ht->root_dir);
if (ret) {
snprintf(err, err_len, "register_tempdir_for_cleanup(%s) "
"failed: %s", ht->root_dir, terror(ret));
goto done;
}
for (i = 0; i < NUM_DATA_DIRS; i++) {
if (asprintf(ht->data_dir + i, "%s/dir%d", ht->root_dir, i) < 0) {
ht->data_dir[i] = NULL;
snprintf(err, err_len, "failed to create path to data dir %d", i);
goto done;
}
}
if (asprintf(&ht->htraced_log_path, "%s/htraced.log", ht->root_dir) < 0) {
ht->htraced_log_path = NULL;
snprintf(err, err_len, "failed to create path to htraced.log");
goto done;
}
if (asprintf(&ht->htraced_conf_path, "%s/htraced-conf.xml",
ht->root_dir) < 0) {
ht->htraced_conf_path = NULL;
snprintf(err, err_len, "failed to create path to htraced-conf.xml");
goto done;
}
mini_htraced_open_snsock(ht, err, err_len);
if (err[0]) {
goto done;
}
mini_htraced_write_conf_file(ht, err, err_len);
if (err[0]) {
goto done;
}
mini_htraced_launch_daemon(ht, err, err_len);
if (err[0]) {
goto done;
}
mini_htraced_read_startup_notification(ht, err, err_len);
if (err[0]) {
goto done;
}
if (asprintf(&ht->client_conf_defaults, "%s=%s",
HTRACED_ADDRESS_KEY, ht->htraced_http_addr) < 0) {
ht->client_conf_defaults = NULL;
snprintf(err, err_len, "failed to allocate client conf defaults.");
goto done;
}
*hret = ht;
err[0] = '\0';
done:
if (err[0]) {
mini_htraced_free(ht);
}
}
static int do_waitpid(pid_t pid, char *err, size_t err_len)
{
err[0] = '\0';
while (1) {
int status, res = waitpid(pid, &status, 0);
if (res < 0) {
if (errno == EINTR) {
continue;
}
snprintf(err, err_len, "waitpid(%lld) error: %s",
(long long)pid, terror(res));
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return -1; // signal or other exit
}
}
void mini_htraced_stop(struct mini_htraced *ht)
{
char err[512];
size_t err_len = sizeof(err);
if (!ht->htraced_pid_valid) {
return;
}
kill(ht->htraced_pid, SIGTERM);
ht->htraced_pid_valid = 0;
do_waitpid(ht->htraced_pid, err, err_len);
if (err[0]) {
fprintf(stderr, "%s\n", err);
}
}
void mini_htraced_free(struct mini_htraced *ht)
{
int i;
if (!ht) {
return;
}
mini_htraced_stop(ht);
if (ht->root_dir) {
unregister_tempdir_for_cleanup(ht->root_dir);
if (!getenv("SKIP_CLEANUP")) {
recursive_unlink(ht->root_dir);
}
}
free(ht->root_dir);
for (i = 0; i < NUM_DATA_DIRS; i++) {
free(ht->data_dir[i]);
}
free(ht->htraced_log_path);
free(ht->htraced_conf_path);
free(ht->client_conf_defaults);
if (ht->snsock >= 0) {
close(ht->snsock);
ht->snsock = -1;
}
free(ht->htraced_http_addr);
free(ht);
}
static void mini_htraced_open_snsock(struct mini_htraced *ht, char *err,
size_t err_len)
{
struct sockaddr_in snaddr;
socklen_t len = sizeof(snaddr);
struct timeval tv;
err[0] = '\0';
memset(&snaddr, 0, sizeof(snaddr));
snaddr.sin_family = AF_INET;
snaddr.sin_addr.s_addr = htonl(INADDR_ANY);
ht->snsock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ht->snsock < 0) {
int res = errno;
snprintf(err, err_len, "Failed to create new socket: %s\n",
terror(res));
return;
}
if (bind(ht->snsock, (struct sockaddr *) &snaddr, sizeof(snaddr)) < 0) {
int res = errno;
snprintf(err, err_len, "bind failed: %s\n", terror(res));
return;
}
if (getsockname(ht->snsock, (struct sockaddr *)&snaddr, &len) < 0) {
int res = errno;
snprintf(err, err_len, "getsockname failed: %s\n", terror(res));
return;
}
ht->snport = ntohs(snaddr.sin_port);
if (listen(ht->snsock, 32) < 0) {
int res = errno;
snprintf(err, err_len, "listen failed: %s\n", terror(res));
return;
}
// On Linux, at least, this makes accept() time out after 30 seconds. I'm
// too lazy to use select() here.
tv.tv_sec = 30;
tv.tv_usec = 0;
setsockopt(ht->snsock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
}
static void mini_htraced_write_conf_file(struct mini_htraced *ht,
char *err, size_t err_len)
{
FILE *fp;
int res;
err[0] = '\0';
fp = fopen(ht->htraced_conf_path, "w");
if (!fp) {
res = errno;
snprintf(err, err_len, "fopen(%s) failed: %s",
ht->htraced_conf_path, terror(res));
goto error;
}
if (fprintf(fp, "\
<?xml version=\"1.0\"?>\n\
<?xml-stylesheet type=\"text/xsl\" href=\"configuration.xsl\"?>\n\
<configuration>\n") < 0) {
goto ioerror;
}
if (mini_htraced_write_conf_key(fp, "log.path", "%s",
ht->htraced_log_path)) {
goto ioerror;
}
if (mini_htraced_write_conf_key(fp, "web.address", "127.0.0.1:0")) {
goto ioerror;
}
if (mini_htraced_write_conf_key(fp, "hrpc.address", "127.0.0.1:0")) {
goto ioerror;
}
if (mini_htraced_write_conf_key(fp, "data.store.directories",
"%s%c%s", ht->data_dir[0], PATH_LIST_SEP, ht->data_dir[1])) {
goto ioerror;
}
if (mini_htraced_write_conf_key(fp, "startup.notification.address",
"localhost:%d", ht->snport)) {
goto ioerror;
}
if (mini_htraced_write_conf_key(fp, "log.level", "%s", "TRACE")) {
goto ioerror;
}
if (fprintf(fp, "</configuration>\n") < 0) {
goto ioerror;
}
res = fclose(fp);
if (res) {
snprintf(err, err_len, "fclose(%s) failed: %s",
ht->htraced_conf_path, terror(res));
}
return;
ioerror:
snprintf(err, err_len, "fprintf(%s) error",
ht->htraced_conf_path);
error:
if (fp) {
fclose(fp);
}
}
static int mini_htraced_write_conf_key(FILE *fp, const char *key,
const char *fmt, ...)
{
va_list ap;
if (fprintf(fp, " <property>\n <name>%s</name>\n <value>",
key) < 0) {
return 1;
}
va_start(ap, fmt);
if (vfprintf(fp, fmt, ap) < 0) {
va_end(ap);
return 1;
}
va_end(ap);
if (fprintf(fp, "</value>\n </property>\n") < 0) {
return 1;
}
return 0;
}
pid_t mini_htraced_launch(const struct mini_htraced *ht, const char *path,
char *err, size_t err_len, int flags, ...)
{
pid_t pid;
int res, num_args = 0;
va_list ap;
const char *args[MAX_LAUNCH_ARGS + 1];
const char * const env[] = {
"HTRACED_CONF_DIR=.", "HTRACED_WEB_DIR=", NULL
};
err[0] = '\0';
if (access(path, X_OK) < 0) {
snprintf(err, err_len, "The %s binary is not accessible and "
"executable.", path);
return -1;
}
va_start(ap, flags);
args[num_args++] = path;
while (1) {
const char *arg = va_arg(ap, char*);
if (!arg) {
break;
}
if (num_args >= MAX_LAUNCH_ARGS) {
va_end(ap);
snprintf(err, err_len, "Too many arguments to launch! The "
"maximum number of arguments is %d.\n", MAX_LAUNCH_ARGS);
return -1;
}
args[num_args++] = arg;
}
va_end(ap);
args[num_args++] = NULL;
pid = fork();
if (pid == -1) {
// Fork failed.
res = errno;
snprintf(err, err_len, "fork() failed for %s: %s\n",
path, terror(res));
return -1;
} else if (pid == 0) {
// Child process.
// We don't want to delete the temporary directory when this child
// process exists. The parent process is responsible for that, if it
// is to be done at all.
unregister_tempdir_for_cleanup(ht->root_dir);
// Make things nicer by exiting when the parent process exits.
prctl(PR_SET_PDEATHSIG, SIGHUP);
if (flags & MINI_HTRACED_LAUNCH_REDIRECT_FDS) {
int null_fd;
RETRY_ON_EINTR(null_fd, open("/dev/null", O_WRONLY));
if (null_fd < 0) {
_exit(127);
}
RETRY_ON_EINTR(res, dup2(null_fd, STDOUT_FILENO));
if (res < 0) {
_exit(127);
}
RETRY_ON_EINTR(res, dup2(null_fd, STDERR_FILENO));
if (res < 0) {
_exit(127);
}
}
if (chdir(ht->root_dir) < 0) {
_exit(127);
}
execve(path, (char *const*)args, (char * const*)env);
_exit(127);
}
// Parent process.
return pid;
}
static void mini_htraced_launch_daemon(struct mini_htraced *ht,
char *err, size_t err_len)
{
int flags = 0;
pid_t pid;
if (!getenv("SKIP_CLEANUP")) {
flags |= MINI_HTRACED_LAUNCH_REDIRECT_FDS;
}
pid = mini_htraced_launch(ht, HTRACED_ABSPATH, err, err_len, flags, NULL);
if (err[0]) {
return;
}
ht->htraced_pid_valid = 1;
ht->htraced_pid = pid;
}
void mini_htraced_dump_spans(struct mini_htraced *ht,
char *err, size_t err_len,
const char *path)
{
pid_t pid;
int ret;
char *addr = NULL, *log_path = NULL;
err[0] = '\0';
if (asprintf(&addr, "--addr=%s", ht->htraced_http_addr) < 0) {
addr = NULL;
snprintf(err, err_len, "OOM while allocating the addr string");
return;
}
if (asprintf(&log_path, "--Dlog.path=%s/htrace.%05"PRId64".log",
ht->root_dir, ht->num_htrace_commands_run) < 0) {
log_path = NULL;
snprintf(err, err_len, "OOM while allocating the addr string");
free(addr);
return;
}
ht->num_htrace_commands_run++;
pid = mini_htraced_launch(ht, HTRACED_TOOL_ABSPATH, err, err_len, 0,
addr, log_path, "dumpAll", path, NULL);
free(addr);
free(log_path);
if (err[0]) {
return;
}
ret = do_waitpid(pid, err, err_len);
if (err[0]) {
return;
}
if (ret != EXIT_SUCCESS) {
snprintf(err, err_len, "%s returned non-zero exit status %d\n",
HTRACED_TOOL_ABSPATH, ret);
return;
}
}
static void mini_htraced_read_startup_notification(struct mini_htraced *ht,
char *err, size_t err_len)
{
char *ndata = NULL;
int res, sock = -1;
size_t ndata_len = 0;
err[0] = '\0';
ndata = malloc(MAX_NDATA);
if (!ndata) {
snprintf(err, err_len, "failed to allocate %d byte buffer for "
"notification data.", MAX_NDATA);
goto done;
}
RETRY_ON_EINTR(sock, accept(ht->snsock, NULL, NULL));
if (sock < 0) {
int e = errno;
snprintf(err, err_len, "accept failed: %s", terror(e));
goto done;
}
while (ndata_len < MAX_NDATA) {
res = recv(sock, ndata + ndata_len, MAX_NDATA - ndata_len, 0);
if (res == 0) {
break;
}
if (res < 0) {
int e = errno;
if (e == EINTR) {
continue;
}
snprintf(err, err_len, "recv error: %s", terror(e));
goto done;
}
ndata_len += res;
}
parse_startup_notification(ht, ndata, ndata_len, err, err_len);
if (err[0]) {
goto done;
}
done:
if (sock >= 0) {
close(sock);
}
free(ndata);
}
static void parse_startup_notification(struct mini_htraced *ht,
char *ndata, size_t ndata_len,
char *err, size_t err_len)
{
struct json_tokener *tok = NULL;
struct json_object *root = NULL, *http_addr, *process_id, *hrpc_addr;
int32_t pid;
err[0] = '\0';
tok = json_tokener_new();
if (!tok) {
snprintf(err, err_len, "json_tokener_new failed.");
goto done;
}
root = json_tokener_parse_ex(tok, ndata, ndata_len);
if (!root) {
enum json_tokener_error jerr = json_tokener_get_error(tok);
snprintf(err, err_len, "Failed to parse startup notification: %s.",
json_tokener_error_desc(jerr));
goto done;
}
// Find the http address, in the form of hostname:port, which the htraced
// is listening on.
if (!json_object_object_get_ex(root, "HttpAddr", &http_addr)) {
snprintf(err, err_len, "Failed to find HttpAddr in the startup "
"notification.");
goto done;
}
ht->htraced_http_addr = strdup(json_object_get_string(http_addr));
if (!ht->htraced_http_addr) {
snprintf(err, err_len, "OOM");
goto done;
}
// Find the HRPC address, in the form of hostname:port, which the htraced
// is listening on.
if (!json_object_object_get_ex(root, "HrpcAddr", &hrpc_addr)) {
snprintf(err, err_len, "Failed to find HrpcAddr in the startup "
"notification.");
goto done;
}
ht->htraced_hrpc_addr = strdup(json_object_get_string(hrpc_addr));
if (!ht->htraced_hrpc_addr) {
snprintf(err, err_len, "OOM");
goto done;
}
// Check that the process ID from the startup notification matches the
// process ID from the fork.
if (!json_object_object_get_ex(root, "ProcessId", &process_id)) {
snprintf(err, err_len, "Failed to find ProcessId in the startup "
"notification.");
goto done;
}
pid = json_object_get_int(process_id);
if (pid != ht->htraced_pid) {
snprintf(err, err_len, "Startup notification pid was %lld, but the "
"htraced process id was %lld.",
(long long)pid, (long long)ht->htraced_pid);
goto done;
}
done:
json_tokener_free(tok);
if (root) {
json_object_put(root);
}
}
// vim: ts=4:sw=4:tw=79:et