/* -*- c-file-style: "java"; indent-tabs-mode: nil; fill-column: 78 -*- * * distcc -- A simple distributed compiler system * * Copyright (C) 2003 by Apple Computer, Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ /** * @file * * Server functions for file indirection feature. **/ #if defined(DARWIN) #include #include #include #include #include #include #include #include #include #include #include #include #include "bulk.h" #include "config.h" #include "exitcode.h" #include "indirect_server.h" #include "indirect_util.h" #include "io.h" #include "rpc.h" #include "tempfile.h" #include "trace.h" static const char message_terminator = '\n'; static int childWrite[2]; static pthread_t indirectionThread = { 0 }; static int parentWrite[2]; // from lock.c int sys_lock(int fd, int block); /** * Receive messages from a child process (typically GCC 3.3 and later). * These messages provide information from the child regarding file * indirection. * The message includes all bytes on the pipe up to the first message * terminator (newline) encountered. * Converts the message terminator into a string terminator. * Stores the message in buffer. * * Return 1 on success and 0 on failure. * Fails if the message is longer than size **/ static int read_from_child(char *buffer, const int size) { int idx = 0; int result; if ( size <= 0 ) { return 0; } do { result = read(childWrite[0], &buffer[idx], 1); if ( result <= 0 || idx >= size ) { return 0; } else { idx += result; } } while ( buffer[idx - 1] != message_terminator ); // Straighten out the string termination. buffer[idx - 1] = '\0'; return 1; } /** * Write message to a child process (typically GCC 3.3 and later). * These messages provide information to the child regarding file * indirection. * The message includes all bytes up to the string terminator. * Converts the string terminator into a message terminator (newline). * Writes a single message terminator alone if message is * NULL. * * Return 1 on success and 0 on failure. **/ static int write_to_child(const char *message) { int result; if ( message ) { const int length = strlen(message); int idx = 0; while ( idx < length ) { result = write(parentWrite[1], &message[idx], length - idx); if ( result < 0 ) { return 0; } else { idx += result; } } } result = write(parentWrite[1], &message_terminator, 1); if ( result < 0 ) { return 0; } return 1; } /** * Create path_to_dir, including any intervening directories. * Behaves similarly to mkdir -p. * Permissions are set to ug=rwx,o-rwx. * * path_to_dir must not be NULL **/ static int mkdir_p(const char *path_to_dir) { char *path = strdup(path_to_dir); char current_path[MAXPATHLEN]; char *latest_path_element; current_path[0] = '\0'; latest_path_element = strtok(path, "/"); while ( strlen(current_path) < MAXPATHLEN - MAXNAMLEN - 1 ) { if ( latest_path_element == NULL ) { break; } else { strcat(current_path, "/"); strcat(current_path, latest_path_element); // 504 == \770 == ug=rwx,o-rwx if ( mkdir(current_path, 504) == 0 ) { rs_trace("Created directory %s", current_path); } else { if ( errno == EEXIST ) { rs_trace("Directory exists: %s", current_path); } else { rs_log_error("Unable to create directory %s: %s", current_path, strerror(errno)); return 0; } } latest_path_element = strtok(NULL, "/"); } } free(path); if ( latest_path_element == NULL ) { return 1; } else { return 0; } } /** * Creates a temporary file at tmp_path to store * checksum. **/ static char *dcc_create_checksum_tmpfile(const char *tmp_path, const char *checksum) { char *checksum_tmpfile = malloc(MAXPATHLEN); int fd; if ( checksum_tmpfile == NULL ) { rs_log_error("Unable to allocate space for checksum_tmpfile"); return NULL; } strncpy(checksum_tmpfile, tmp_path, MAXPATHLEN); strcat(checksum_tmpfile, checksum_suffix); // do not need to mkdir_p, since this should always happen after // the files represented by the checksum are laid down fd = open(checksum_tmpfile, O_WRONLY|O_CREAT, 0777); if ( fd < 0 ) { rs_log_error("Unable to create %s", checksum_tmpfile); return NULL; } else { size_t checksum_length = strlen(checksum); if ( write(fd, checksum, checksum_length) == (ssize_t)checksum_length ){ rs_trace("Wrote checksum to %s", checksum_tmpfile); close(fd); return checksum_tmpfile; } else { rs_log_error("Unable to write checksum to %s", checksum_tmpfile); close(fd); remove(checksum_tmpfile); return NULL; } } } /** * Pulls a checksum for the previously requested file over the socket * specified by netfd. Stores the checksum at * tmp_path. **/ static char *dcc_pull_checksum(int netfd, const char *tmp_path) { int checksum_length = 0; if ( dcc_r_token_int(netfd, checksum_length_token, &checksum_length) || checksum_length <= 0 ) { rs_log_error("No incoming checksum for path %s", tmp_path); return NULL; } else { char *checksum = malloc(checksum_length + 1); if ( dcc_readx(netfd, checksum, checksum_length) ) { rs_log_error("Unable to read checksum for path %s", tmp_path); free(checksum); return NULL; } else { char *tmp_checksum_filename = dcc_create_checksum_tmpfile(tmp_path, checksum); free(checksum); return tmp_checksum_filename; } } } /** * Pushes the existing checksum for the previously requested file * over the socket specified by netfd. * hostname used strictly to disambiguate logging. **/ static int dcc_push_checksum(int netfd, const char *hostname, const char *checksum) { size_t checksum_length = ( checksum == NULL ) ? 0 : strlen(checksum) + 1; if ( dcc_x_token_int(netfd, checksum_length_token, checksum_length) ) { rs_log_error("Unable to transmit checksum length %u to %s", checksum_length, hostname); return 0; } else if ( checksum_length > 0 ) { if ( dcc_writex(netfd, checksum, checksum_length) ) { rs_log_error("Unable to transmit checksum \"%s\" to %s", checksum, hostname); return 0; } } return 1; } /** * Reads the contents of cached_path_checksum. * cached_path_checksum_size is used as a hint for the size * of the buffer to return. The returned buffer must be freed by the caller. **/ static char *dcc_read_checksum(const char *cached_path_checksum, size_t cached_path_checksum_size) { int remaining_buffer = cached_path_checksum_size + 1; char *checksum = malloc(remaining_buffer); if ( checksum == NULL ) { rs_log_error("Unable to allocate space for checksum_tmpfile"); } else { int checksum_file = open(cached_path_checksum, O_RDONLY, 0777); if ( checksum_file <= 0 ) { rs_log_error("Unable to open %s", cached_path_checksum); free(checksum); checksum = NULL; } else { int num_read = -1; while ( remaining_buffer > 0 && ( num_read = read(checksum_file, checksum, remaining_buffer) ) > 0 ) { remaining_buffer -= num_read; } checksum[cached_path_checksum_size] = '\0'; if ( num_read < 0 || strlen(checksum) < cached_path_checksum_size ) { rs_log_error("Unable to read checksum from %s", cached_path_checksum); free(checksum); checksum = NULL; } close(checksum_file); } } return checksum; } /** * Pulls the previously requested directory over the socket specified by * netfd. Currently, this directory cannot contain any * subdirectories. tmp_path specifies the location on the * server where the directory will be stored. hostname is * used only to disambiguate logging. **/ static void dcc_pull_directory_file(int netfd, const char *hostname, const char *tmp_path) { int cwd = open(".", O_RDONLY, 0777); if ( chdir(tmp_path) ) { rs_log_error("Unable to chdir to %s", tmp_path); } else { int incomingBytes; if ( dcc_r_token_int(netfd, result_name_token, &incomingBytes) || incomingBytes <= 0 ) { rs_log_error("No incoming file from %s for directory %s", hostname, tmp_path); } else { char filename[incomingBytes]; if ( dcc_readx(netfd, filename, incomingBytes) ) { rs_log_error("Unable to read filename from %s for file in directory %s", hostname, tmp_path); } else { if (dcc_r_token_int(netfd, result_item_token, &incomingBytes)) { rs_log_error("Unable to read length from %s for file %s in %s", hostname, filename, tmp_path); } else { if ( dcc_r_file_timed(netfd, filename, incomingBytes) ) { rs_log_error("Failed to retrieve from %s incoming file %s/%s", hostname, tmp_path, filename); } else { rs_log_info("Stored incoming file from %s at %s in %s", hostname, filename, tmp_path); } } } } } fchdir(cwd); close(cwd); } /** * Remove path (not including intervening directories). * All files in path are removed, as well. * Does not remove subdirectories of path. **/ static int dcc_rmdir(const char *path) { int numFiles; char **filenames = dcc_filenames_in_directory(path, &numFiles); if ( filenames != NULL ) { int dir = open(path, O_RDONLY, 0777); if ( dir > 0 ) { int cwd = open(".", O_RDONLY, 0777); if ( fchdir(dir) == 0 ) { int i; for ( i = 0; i < numFiles && filenames[i] != NULL; i++ ) { if ( unlink(filenames[i]) ) { rs_log_error("Unable to remove %s from %s", filenames[i], path); } else { rs_trace("Removed %s from %s", filenames[i], path); } free(filenames[i]); } } fchdir(cwd); close(cwd); close(dir); } free(filenames); } return rmdir(path); } #if ! defined(HAVE_FLOCK) /** * Get a shared lock on a file using whatever method * is available on this system. * * @retval 0 if we got the lock * @retval -1 with errno set if the file is already locked. **/ static int sys_shlock(int fd, int block) { #if defined(F_SETLK) && ! defined(DARWIN) struct flock lockparam; lockparam.l_type = F_RDLCK; lockparam.l_whence = SEEK_SET; lockparam.l_start = 0; lockparam.l_len = 0; /* whole file */ return fcntl(fd, block ? F_SETLKW : F_SETLK, &lockparam); #elif defined(HAVE_FLOCK) return flock(fd, LOCK_SH | (block ? 0 : LOCK_NB)); #else # error "No supported lock method. Please port this code." #endif } #endif // ! defined(HAVE_FLOCK) /** * Performs a read lock on the file at checksum_path. * Returns the file descriptor for the result. **/ static int dcc_shared_lock_on_checksum_file(const char *checksum_path) { int fd; rs_trace("Taking read lock on %s", checksum_path); #if defined(HAVE_FLOCK) fd = open(checksum_path, O_RDONLY | O_SHLOCK, 0777); #else fd = open(checksum_path, O_RDONLY, 0777); sys_shlock(fd, 1); #endif rs_trace("Got read lock (released automatically on process exit) on %s", checksum_path); return fd; } /** * Performs a write lock on the file at checksum_path. * Returns the file descriptor for the result. **/ static int dcc_exclusive_lock_on_checksum_file(const char *checksum_path) { int fd = -1; // Use the checksum file as a lock file. rs_trace("Taking a write lock on %s", checksum_path); #if defined(HAVE_FLOCK) fd = open(checksum_path, O_WRONLY|O_CREAT|O_EXLOCK, 0777); #else fd = open(checksum_path, O_WRONLY|O_CREAT, 0777); sys_lock(fd, 1); #endif rs_trace("Got write lock on %s", checksum_path); return fd; } /** * Forcibly removes a lock on the file described by lock_fd. * Returns non-zero on error. **/ static int dcc_really_unlock(int lock_fd) { #if defined(F_SETLK) && ! defined(DARWIN) struct flock lockparam; lockparam.l_type = F_UNLCK; lockparam.l_whence = SEEK_SET; lockparam.l_start = 0; lockparam.l_len = 0; /* whole file */ if (fcntl(lock_fd, F_SETLK, &lockparam)) #elif defined(HAVE_FLOCK) if (flock(lock_fd, LOCK_UN)) #elif defined(HAVE_LOCKF) if (lockf(lock_fd, F_ULOCK, 0)) #else # error "No supported lock method. Please port this code." #endif { rs_log_error("unlock failed: %s", strerror(errno)); return EXIT_IO_ERROR; } rs_trace("really released lock"); return 0; } /** * Releases the lock held on fd. checksum_path * provided for logging only. Also closes fd. **/ static void dcc_unlock_checksum_file(int fd, const char *checksum_path) { dcc_really_unlock(fd); rs_trace("Released write lock on %s", checksum_path); if ( fd >= 0 ) { close(fd); } } /** * Handles a pull operation, as specified by the server's child (typically * gcc 3.3). netfd is the file descriptor for the socket * connection between the client and server. hostname specifies * the hostname of the client. Pulled files are stored in a per-host cache. **/ static void dcc_handle_pull(int netfd, const char *hostname) { char *actual_path = NULL; size_t hostname_length = strlen(hostname) + 1; char response[MAXPATHLEN]; const char *tempdir = dcc_get_tempdir(); size_t tempdir_length = strlen(tempdir) + 1; int fd; if ( read_from_child(response, MAXPATHLEN) ) { size_t response_length = strlen(response) + 1; size_t total_length = tempdir_length + hostname_length + response_length; char *cached_path = malloc(total_length); char *cached_path_checksum = malloc(total_length + checksum_suffix_length); int cached_path_exists; int cached_path_checksum_exists; size_t cached_path_checksum_size; char *checksum = NULL; struct stat sb; rs_trace("Attempting to pull %s", response); strcat(cached_path, tempdir); strcat(cached_path, "/"); strcat(cached_path, hostname); strcat(cached_path, "/"); strcat(cached_path, response); strncpy(cached_path_checksum, cached_path, total_length); strncat(cached_path_checksum, checksum_suffix, checksum_suffix_length); cached_path_exists = ( stat(cached_path, &sb) == 0 ); cached_path_checksum_exists = ( stat(cached_path_checksum, &sb) == 0 ); cached_path_checksum_size = sb.st_size; // Lock the file using a reader lock, so that multiple processes // may read the same file, but only one file at a time may write it. // The lock is released when the process exits. Do not explicitly // release it here, as the subordinate process will need to use // the file safely. fd = dcc_shared_lock_on_checksum_file(cached_path_checksum); if ( cached_path_exists && cached_path_checksum_exists && cached_path_checksum_size > 0 ) { checksum = dcc_read_checksum(cached_path_checksum, cached_path_checksum_size); } if ( dcc_x_token_int(netfd, indirection_request_token, indirection_request_pull) || dcc_x_token_int(netfd, operation_pull_token, response_length) || dcc_writex(netfd, response, response_length) ) { rs_log_error("Unable to transmit filename to %s", hostname); actual_path = response; } else { if ( ! dcc_push_checksum(netfd, hostname, checksum) ) { actual_path = response; } } dcc_unlock_checksum_file(fd, cached_path_checksum); if ( checksum != NULL ) { free(checksum); checksum = NULL; } if ( actual_path == NULL ) { int result_type = result_type_nothing; if ( dcc_r_token_int(netfd, result_type_token, &result_type) ) { rs_log_error("Unable to read result type from %s", hostname); actual_path = response; } else if ( result_type == result_type_nothing || result_type == result_type_checksum_only ) { // the client says that it has nothing or nothing newer // than what the server has if ( cached_path_exists && cached_path_checksum_exists ) { rs_log_info("Using cached version: %s", cached_path); actual_path = cached_path; } else { rs_log_error("Unable to find %s on %s", response, hostname); actual_path = response; } if ( result_type == result_type_checksum_only ) { char *tmpName = dcc_make_tmpnam(hostname, response); char *tmpChecksumName = NULL; char *lastSlash = strrchr(tmpName, '/'); lastSlash[0] = '\0'; if ( mkdir_p(tmpName) ) { lastSlash[0] = '/'; tmpChecksumName = dcc_pull_checksum(netfd, tmpName); if ( tmpChecksumName != NULL ) { int xfd = dcc_exclusive_lock_on_checksum_file(cached_path_checksum); if ( rename(tmpChecksumName, cached_path_checksum)){ rs_log_error("Failed to move %s to %s: %s", tmpChecksumName, cached_path_checksum, strerror(errno)); } else { rs_trace("Moved %s to %s", tmpChecksumName, cached_path_checksum); } dcc_unlock_checksum_file(xfd, cached_path_checksum); free(tmpChecksumName); } } else { rs_log_error("Unable to create directory %s", tmpName); } free(tmpName); free(cached_path_checksum); } } else if ( result_type == result_type_file ) { char *tmpChecksumName = NULL; char *tmpName = dcc_make_tmpnam(hostname, response); char *lastSlash = strrchr(tmpName, '/'); int incomingBytes; if ( dcc_r_token_int(netfd, result_item_token, &incomingBytes) || incomingBytes <= 0 ) { rs_log_error("No incoming file for path %s", response); actual_path = response; } else { lastSlash[0] = '\0'; if ( mkdir_p(tmpName) ) { lastSlash[0] = '/'; if ( dcc_r_file_timed(netfd, tmpName, incomingBytes) == 0 ) { rs_log_info("Stored incoming file at %s", tmpName); tmpChecksumName = dcc_pull_checksum(netfd, tmpName); } else { rs_log_error("Failed to retrieve incoming file %s", tmpName); actual_path = response; } } else { rs_log_error("Unable to create directory %s", tmpName); actual_path = response; } } if ( actual_path == NULL ) { // move the temp files into the cached location int xfd; lastSlash = strrchr(cached_path, '/'); lastSlash[0] = '\0'; if ( ! mkdir_p(cached_path) ) { rs_log_error("Unable to create directory %s", cached_path); } lastSlash[0] = '/'; xfd = dcc_exclusive_lock_on_checksum_file(cached_path_checksum); if ( rename(tmpName, cached_path) ) { rs_log_error("Failed to move %s to %s: %s", tmpName, cached_path, strerror(errno)); actual_path = response; } else { rs_trace("Moved %s to %s", tmpName, cached_path); actual_path = cached_path; if ( tmpChecksumName != NULL ) { if ( rename(tmpChecksumName, cached_path_checksum)){ rs_log_error("Failed to move %s to %s: %s", tmpChecksumName, cached_path_checksum, strerror(errno)); } else { rs_trace("Moved %s to %s", tmpChecksumName, cached_path_checksum); } } } dcc_unlock_checksum_file(xfd, cached_path_checksum); } // cleanup free(cached_path_checksum); if ( tmpChecksumName != NULL ) { free(tmpChecksumName); } free(tmpName); } else if ( result_type == result_type_dir ) { char *tmpChecksumName = NULL; char *tmpName = dcc_make_tmpnam(hostname, response); int fileCount; if ( dcc_r_token_int(netfd, result_count_token, &fileCount) || fileCount <= 0 ) { rs_log_error("No incoming files for path %s", response); actual_path = response; } else { if ( mkdir_p(tmpName) ) { int i; for ( i = 0; i < fileCount; i++ ) { dcc_pull_directory_file(netfd, hostname, tmpName); } tmpChecksumName = dcc_pull_checksum(netfd, tmpName); } else { rs_log_error("Unable to create directory %s", tmpName); actual_path = response; } } if ( actual_path == NULL ) { // move the temp files into the cached location int xfd; if ( dcc_rmdir(cached_path) ) { char *lastSlash = strrchr(cached_path, '/'); rs_log_error("Unable to remove %s", cached_path); lastSlash[0] = '\0'; if ( ! mkdir_p(cached_path) ) { rs_log_error("Unable to create directory %s", cached_path); actual_path = response; } lastSlash[0] = '/'; } xfd = dcc_exclusive_lock_on_checksum_file(cached_path_checksum); if ( rename(tmpName, cached_path) ) { rs_log_error("Failed to move %s to %s: %s", tmpName, cached_path, strerror(errno)); actual_path = response; } else { rs_trace("Moved %s to %s", tmpName, cached_path); actual_path = cached_path; if ( tmpChecksumName != NULL ) { if ( rename(tmpChecksumName, cached_path_checksum)){ rs_log_error("Failed to move %s to %s: %s", tmpChecksumName, cached_path_checksum, strerror(errno)); } else { rs_trace("Moved %s to %s", tmpChecksumName, cached_path_checksum); } } } dcc_unlock_checksum_file(xfd, cached_path_checksum); } // cleanup free(cached_path_checksum); if ( tmpChecksumName != NULL ) { free(tmpChecksumName); } free(tmpName); } } fd = dcc_shared_lock_on_checksum_file(cached_path_checksum); } else { rs_log_error("Unable to receive %s request", operation_pull_token); actual_path = (char *) "UNKNOWN"; } if ( write_to_child(actual_path) ) { rs_log_info("Using %s", actual_path); } else { rs_log_error("Unable to contact child for path %s", actual_path); } // cleanup if ( actual_path != response ) { free(actual_path); } } /** * Thread task for handling indirection requests from a child process * (typically gcc 3.3). Communication between the two processes occurs over * a parent-child pipe pair. Only "pull" operations are currently implemented. * Invokes dcc_handle_pull to do so. * ifd is a pointer to the file descriptor for the socket * that the client and server use to communicate. **/ static void *dcc_handle_indirection(void *ifd) { int netfd = *((int *)ifd); char *hostname = (char *) "unknown"; struct sockaddr_in sain; size_t sain_length = sizeof(sain); size_t actual_length = sain_length; // Grab the incoming hostname for use in creating a cache path on this // host for the desired files. if ( getpeername(netfd, (struct sockaddr *) &sain, (int *) &actual_length) == 0 && actual_length <= sain_length ) { hostname = inet_ntoa(sain.sin_addr); } else { rs_log_error("Unable to determine remote hostname; using \"unknown\""); } // Handle requests from the child process until this process terminates. while (1) { char response[10]; if ( read_from_child(response, 10) ) { if ( strcmp(operation_pull_token, response) == 0 ) { dcc_handle_pull(netfd, hostname); } else if ( strcmp(operation_push_token, response) == 0 ) { rs_log_error("Unsupported operation: %s", response); } else if ( strcmp(operation_both_token, response) == 0 ) { rs_log_error("Unsupported operation: %s", response); } else if ( strcmp(operation_version_token, response) == 0 ) { if ( read_from_child(response, 10) ) { if ( strcmp(indirection_protocol_version, response) == 0 ) { write_to_child("OK"); continue; } } write_to_child("NO"); } else { rs_log_error("Unsupported operation: %s", response); } } } } /** * Closes the child (typically gcc 3.3) end of the parent-child pipe pair used * to describe file indirection requests and results. **/ void dcc_close_pipe_end_child(void) { close(parentWrite[1]); close(childWrite[0]); } /** * Closes the parent end of the parent-child pipe pair used to describe file * indirection requests and results. **/ void dcc_close_pipe_end_parent(void) { close(parentWrite[0]); close(childWrite[1]); } /** * Creates a parent-child pipe pair to handle communication between the parent * (distccd) and the child (typically gcc 3.3). Creates a thread to handle * file indirection requests. This thread persists over the life of the parent * process. **/ void dcc_support_indirection(int *ifd) { char *fdString; int result = pipe(parentWrite); if ( result != 0 ) { rs_log_error("Unable to create file indirection pipe set #1: %s", strerror(errno)); return; } result = pipe(childWrite); if ( result != 0 ) { rs_log_error("Unable to create file indirection pipe set #2: %s", strerror(errno)); close(parentWrite[0]); close(parentWrite[1]); return; } result = asprintf(&fdString, "%d, %d", parentWrite[0], childWrite[1]); if ( result <= 0 ) { rs_log_error("Unable to create file indirection pipe description: %s", strerror(errno)); dcc_close_pipe_end_child(); dcc_close_pipe_end_parent(); return; } result = setenv("GCC_INDIRECT_FILES", fdString, 1); if ( result != 0 ) { rs_log_error("Unable to set indirection environment variable"); dcc_close_pipe_end_child(); dcc_close_pipe_end_parent(); free(fdString); return; } result = pthread_create(&indirectionThread, NULL, dcc_handle_indirection, ifd); if ( result == 0 ) { rs_log_info("Created thread to handle file indirection"); } else { rs_log_error("Unable to create thread to handle file indirection"); dcc_close_pipe_end_child(); dcc_close_pipe_end_parent(); free(fdString); unsetenv("GCC_INDIRECT_FILES"); return; } } #endif // DARWIN