| /** |
| * 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 |