diff options
Diffstat (limited to 'src/main/cpp/blaze.cc')
-rw-r--r-- | src/main/cpp/blaze.cc | 1679 |
1 files changed, 1679 insertions, 0 deletions
diff --git a/src/main/cpp/blaze.cc b/src/main/cpp/blaze.cc new file mode 100644 index 0000000000..d51ab787f1 --- /dev/null +++ b/src/main/cpp/blaze.cc @@ -0,0 +1,1679 @@ +// Copyright 2014 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. +// +// blaze.cc: bootstrap and client code for Blaze server. +// +// Responsible for: +// - extracting the Python, C++ and Java components. +// - starting the server or finding the existing one. +// - client options parsing. +// - passing the argv array, and printing the out/err streams. +// - signal handling. +// - exiting with the right error/WTERMSIG code. +// - debugger + profiler support. +// - mutual exclusion between batch invocations. + +#include <assert.h> +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <poll.h> +#include <sched.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/resource.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/time.h> +#include <sys/un.h> +#include <time.h> +#include <unistd.h> +#include <utime.h> +#include <algorithm> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "blaze_exit_code.h" +#include "blaze_startup_options.h" +#include "blaze_util.h" +#include "blaze_util_platform.h" +#include "option_processor.h" +#include "util/file.h" +#include "util/md5.h" +#include "util/numbers.h" +#include "util/port.h" +#include "util/strings.h" +#include "archive.h" +#include "archive_entry.h" + +using std::set; +using std::vector; + +// This should already be defined in sched.h, but it's not. +#ifndef SCHED_BATCH +#define SCHED_BATCH 3 +#endif + +namespace blaze { + +// Enable messages mostly of interest to developers. +static const bool SPAM = getenv("VERBOSE_BLAZE_CLIENT") != NULL; + +// Blaze is being run by a test. +static const bool TESTING = getenv("TEST_TMPDIR") != NULL; + +extern char **environ; + +//////////////////////////////////////////////////////////////////////// +// Global Variables + +// The reason for a blaze server restart. +// Keep in sync with logging.proto +enum RestartReason { + NO_RESTART = 0, + NO_DAEMON, + NEW_VERSION, + NEW_OPTIONS +}; + +struct GlobalVariables { + // Used to make concurrent invocations of this program safe. + string lockfile; // = <output_base>/lock + int lockfd; + + string jvm_log_file; // = <output_base>/server/jvm.out + + string cwd; + + // The nearest enclosing workspace directory, starting from cwd. + // If not under a workspace directory, this is equal to cwd. + string workspace; + + // Option processor responsible for parsing RC files and converting them into + // the argument list passed on to the server. + OptionProcessor option_processor; + + pid_t server_pid; + + volatile sig_atomic_t sigint_count; + + // The number of the last received signal that should cause the client + // to shutdown. This is saved so that the client's WTERMSIG can be set + // correctly. (Currently only SIGPIPE uses this mechanism.) + volatile sig_atomic_t received_signal; + + // Contains the relative paths of all the files in the attached zip, and is + // populated during GetInstallDir(). + vector<string> extracted_binaries; + + // Parsed startup options + BlazeStartupOptions options; + + // The time in ms the launcher spends before sending the request to the Blaze + uint64 startup_time; + + // The time spent on extracting the new blaze version + // This is part of startup_time + uint64 extract_data_time; + + // The time in ms if a command had to wait on a busy Blaze server process + // This is part of startup_time + uint64 command_wait_time; + + RestartReason restart_reason; + + // Absolute path of the blaze binary + string binary_path; +}; + +static GlobalVariables *globals; + +void InitGlobals() { + globals = new GlobalVariables; + globals->sigint_count = 0; + globals->startup_time = 0; + globals->extract_data_time = 0; + globals->command_wait_time = 0; + globals->restart_reason = NO_RESTART; +} + +//////////////////////////////////////////////////////////////////////// +// Logic + + +// Returns the canonical form of the base dir given a root and a hashable +// string. The resulting dir is composed of the root + md5(hashable) +static string GetHashedBaseDir(const string &root, + const string &hashable) { + unsigned char buf[17]; + blaze_util::Md5Digest digest; + digest.Update(hashable.data(), hashable.size()); + digest.Finish(buf); + return root + "/" + digest.String(); +} + +// Returns the install base (the root concatenated with the contents of the file +// 'install_base_key' contained as a ZIP entry in the Blaze binary); as a side +// effect, it also populates the extracted_binaries global variable. +static string GetInstallBase(const string &root, const string &self_path) { + string key_file = "install_base_key"; + struct archive *blaze_zip = archive_read_new(); + archive_read_support_format_zip(blaze_zip); + int retval = archive_read_open_filename(blaze_zip, self_path.c_str(), 10240); + if (retval != ARCHIVE_OK) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to open blaze as a zip file: (%d) %s", + archive_errno(blaze_zip), archive_error_string(blaze_zip)); + } + + struct archive_entry *entry; + string install_base_key; + while (archive_read_next_header(blaze_zip, &entry) == ARCHIVE_OK) { + string pathname = archive_entry_pathname(entry); + globals->extracted_binaries.push_back(pathname); + + if (key_file == pathname) { + const int size = 32; + char buf[size]; + int bytesRead = archive_read_data(blaze_zip, &buf, size); + if (bytesRead < 0) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to extract install_base_key: (%d) %s", + archive_errno(blaze_zip), archive_error_string(blaze_zip)); + } + if (bytesRead < 32) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to extract install_base_key: file too short"); + } + install_base_key = string(buf, bytesRead); + } + } + retval = archive_read_free(blaze_zip); + if (retval != ARCHIVE_OK) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to close install_base_key's containing zip file"); + } + + return root + "/" + install_base_key; +} + +// Escapes colons by replacing them with '_C' and underscores by replacing them +// with '_U'. E.g. "name:foo_bar" becomes "name_Cfoo_Ubar" +static string EscapeForOptionSource(const string& input) { + string result = input; + blaze_util::Replace("_", "_U", &result); + blaze_util::Replace(":", "_C", &result); + return result; +} + +// Returns the JVM command argument array. +static vector<string> GetArgumentArray() { + vector<string> result; + + // e.g. A Blaze server process running in ~/src/build_root (where there's a + // ~/src/build_root/WORKSPACE file) will appear in ps(1) as "blaze(src)". + string workspace = + blaze_util::Basename(blaze_util::Dirname(globals->workspace)); + result.push_back("blaze(" + workspace + ")"); + if (globals->options.batch) { + result.push_back("-client"); + result.push_back("-Xms256m"); + result.push_back("-XX:NewRatio=4"); + } else { + result.push_back("-server"); + } + + result.push_back("-XX:+HeapDumpOnOutOfMemoryError"); + string heap_crash_path = globals->options.output_base; + result.push_back("-XX:HeapDumpPath=" + heap_crash_path); + + result.push_back("-Xverify:none"); + + // Add JVM arguments particular to building blaze64 and particular JVM + // versions. + string error; + blaze_exit_code::ExitCode jvm_args_exit_code = + globals->options.AddJVMArguments(globals->options.GetHostJavabase(), + &result, &error); + if (jvm_args_exit_code != blaze_exit_code::SUCCESS) { + die(jvm_args_exit_code, "%s", error.c_str()); + } + + // We put all directories on the java.library.path that contain .so files. + string java_library_path = "-Djava.library.path="; + string real_install_dir = blaze_util::JoinPath(globals->options.install_base, + "_embedded_binaries"); + bool first = true; + for (const auto& it : globals->extracted_binaries) { + if (blaze::IsSharedLibrary(it)) { + if (!first) { + java_library_path += ":"; + } + first = false; + java_library_path += blaze_util::JoinPath(real_install_dir, + blaze_util::Dirname(it)); + } + } + result.push_back(java_library_path); + + // Force use of latin1 for file names. + result.push_back("-Dfile.encoding=ISO-8859-1"); + + if (globals->options.host_jvm_debug) { + fprintf(stderr, + "Running host JVM under debugger (listening on TCP port 5005).\n"); + // Start JVM so that it listens for a connection from a + // JDWP-compliant debugger: + result.push_back("-Xdebug"); + result.push_back("-Xrunjdwp:transport=dt_socket,server=y,address=5005"); + } + + blaze_util::SplitQuotedStringUsing(globals->options.host_jvm_args, ' ', + &result); + + result.push_back("-jar"); + result.push_back(blaze_util::JoinPath(real_install_dir, + globals->extracted_binaries[0])); + + if (!globals->options.batch) { + result.push_back("--max_idle_secs"); + result.push_back(std::to_string(globals->options.max_idle_secs)); + } else { + result.push_back("--batch"); + } + result.push_back("--install_base=" + globals->options.install_base); + result.push_back("--output_base=" + globals->options.output_base); + result.push_back("--workspace_directory=" + globals->workspace); + if (!globals->options.skyframe.empty()) { + result.push_back("--skyframe=" + globals->options.skyframe); + } + if (globals->options.allow_configurable_attributes) { + result.push_back("--allow_configurable_attributes"); + } + if (globals->options.watchfs) { + result.push_back("--watchfs"); + } + if (globals->options.fatal_event_bus_exceptions) { + result.push_back("--fatal_event_bus_exceptions"); + } else { + result.push_back("--nofatal_event_bus_exceptions"); + } + if (globals->options.webstatus_port) { + result.push_back("--use_webstatusserver=" + \ + std::to_string(globals->options.webstatus_port)); + } + + // This is only for Blaze reporting purposes; the real interpretation of the + // jvm flags occurs when we set up the java command line. + if (globals->options.host_jvm_debug) { + result.push_back("--host_jvm_debug"); + } + if (!globals->options.host_jvm_profile.empty()) { + result.push_back("--host_jvm_profile=" + globals->options.host_jvm_profile); + } + if (!globals->options.host_jvm_args.empty()) { + result.push_back("--host_jvm_args=" + globals->options.host_jvm_args); + } + globals->options.AddExtraOptions(&result); + + // The option sources are transmitted in the following format: + // --option_sources=option1:source1:option2:source2:... + string option_sources = "--option_sources="; + first = true; + for (const auto& it : globals->options.option_sources) { + if (!first) { + option_sources += ":"; + } + + first = false; + option_sources += EscapeForOptionSource(it.first) + ":" + + EscapeForOptionSource(it.second); + } + + result.push_back(option_sources); + return result; +} + +// Add commom command options for logging to the given argument array. +static void AddLoggingArgs(vector<string>* args) { + args->push_back("--startup_time=" + std::to_string(globals->startup_time)); + if (globals->command_wait_time != 0) { + args->push_back("--command_wait_time=" + + std::to_string(globals->command_wait_time)); + } + if (globals->extract_data_time != 0) { + args->push_back("--extract_data_time=" + + std::to_string(globals->extract_data_time)); + } + if (globals->restart_reason != NO_RESTART) { + const char *reasons[] = { + "no_restart", "no_daemon", "new_version", "new_options" + }; + args->push_back( + string("--restart_reason=") + reasons[globals->restart_reason]); + } + args->push_back( + string("--binary_path=") + globals->binary_path); +} + + +// Join the elements of the specified array with NUL's (\0's), akin to the +// format of /proc/$PID/cmdline. +string GetArgumentString(const vector<string>& argument_array) { + string result; + blaze_util::JoinStrings(argument_array, '\0', &result); + return result; +} + +// Causes the current process to become a daemon (i.e. a child of +// init, detached from the terminal, in its own session.) We don't +// change cwd, though. +static void Daemonize(int socket) { + // Don't call die() or exit() in this function; we're already in a + // child process so it won't work as expected. Just don't do + // anything that can possibly fail. :) + + signal(SIGHUP, SIG_IGN); + if (fork() > 0) { + // This second fork is required iff there's any chance cmd will + // open an specific tty explicitly, e.g., open("/dev/tty23"). If + // not, this fork can be removed. + _exit(blaze_exit_code::SUCCESS); + } + + setsid(); + + close(0); + close(1); + close(2); + close(socket); + + open("/dev/null", O_RDONLY); // stdin + // stdout: + if (open(globals->jvm_log_file.c_str(), + O_WRONLY | O_CREAT | O_TRUNC, 0666) == -1) { + // In a daemon, no-one can hear you scream. + open("/dev/null", O_WRONLY); + } + dup(STDOUT_FILENO); // stderr (2>&1) + + // Keep server from inheriting a useless fd. + // The file lock was already lost at fork(). + close(globals->lockfd); +} + +// Do a chdir into the workspace, and die if it fails. +static void GoToWorkspace() { + if (BlazeStartupOptions::InWorkspace(globals->workspace) && + chdir(globals->workspace.c_str()) != 0) { + pdie(blaze_exit_code::INTERNAL_ERROR, + "chdir() into %s failed", globals->workspace.c_str()); + } +} + +// Check the java version if a java version specification is bundled. On +// success, +// return the executable path of the java command. +static string VerifyJavaVersionAndGetJvm() { + string exe = globals->options.GetJvm(); + + string version_spec_file = blaze_util::JoinPath( + blaze_util::JoinPath(globals->options.install_base, "_embedded_binaries"), + "java.version"); + string version_spec = ""; + if (ReadFile(version_spec_file, &version_spec)) { + blaze_util::StripWhitespace(&version_spec); + // A version specification is given, get version of java. + string jvm_version = GetJvmVersion(exe); + + // Compare that jvm_version is found and at least the one specified. + if (jvm_version.size() == 0) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Java version not detected while at least %s is needed.\n" + "Please set JAVA_HOME.", version_spec.c_str()); + } else if (!CheckJavaVersionIsAtLeast(jvm_version, version_spec)) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Java version is %s while at least %s is needed.\n" + "Please set JAVA_HOME.", + jvm_version.c_str(), version_spec.c_str()); + } + } + + return exe; +} + +// Starts the Blaze server. Returns a readable fd connected to the server. +// This is currently used only to detect liveness. +static int StartServer(int socket) { + vector<string> jvm_args_vector = GetArgumentArray(); + string argument_string = GetArgumentString(jvm_args_vector); + + // Write the cmdline argument string to the server dir. If we get to this + // point, there is no server running, so we don't overwrite the cmdline file + // for the existing server. If might be that the server dies and the cmdline + // file stays there, but that is not a problem, since we always check the + // server, too. + WriteFile(argument_string, globals->options.output_base + "/server/cmdline"); + + // unless we restarted for a new-version, mark this as initial start + if (globals->restart_reason == NO_RESTART) { + globals->restart_reason = NO_DAEMON; + } + + // Computing this path may report a fatal error, so do it before forking. + string exe = VerifyJavaVersionAndGetJvm(); + + // Go to the workspace before we daemonize, so + // we can still print errors to the terminal. + GoToWorkspace(); + + int fds[2]; + if (pipe(fds)) { + pdie(blaze_exit_code::INTERNAL_ERROR, "pipe creation failed"); + } + int child = fork(); + if (child == -1) { + pdie(blaze_exit_code::INTERNAL_ERROR, "fork() failed"); + } else if (child > 0) { // we're the parent + close(fds[1]); // parent keeps only the reading side + return fds[0]; + } else { + close(fds[0]); // child keeps only the writing side + } + + Daemonize(socket); + ExecuteProgram(exe, jvm_args_vector); + pdie(blaze_exit_code::INTERNAL_ERROR, "execv of '%s' failed", exe.c_str()); +} + +static bool KillRunningServerIfAny(); + +// Replace this process with blaze in standalone/batch mode. +// The batch mode blaze process handles the command and exits. +// +// This function passes the commands array to the blaze process. +// This array should start with a command ("build", "info", etc.). +static void StartStandalone() { + KillRunningServerIfAny(); + + // Wall clock time since process startup. + globals->startup_time = ProcessClock() / 1000000LL; + + if (VerboseLogging()) { + fprintf(stderr, "Starting blaze in batch mode.\n"); + } + string command = globals->option_processor.GetCommand(); + vector<string> command_arguments; + globals->option_processor.GetCommandArguments(&command_arguments); + + if (!command_arguments.empty() && command == "shutdown") { + fprintf(stderr, + "WARNING: Running command \"shutdown\" in batch mode. Batch mode " + "is triggered\nwhen not running blaze within a workspace. If you " + "intend to shutdown an\nexisting blaze server, run \"blaze " + "shutdown\" from the directory where\nit was started.\n"); + } + vector<string> jvm_args_vector = GetArgumentArray(); + if (command != "") { + jvm_args_vector.push_back(command); + AddLoggingArgs(&jvm_args_vector); + } + + jvm_args_vector.insert(jvm_args_vector.end(), + command_arguments.begin(), + command_arguments.end()); + + GoToWorkspace(); + + string exe = VerifyJavaVersionAndGetJvm(); + ExecuteProgram(exe, jvm_args_vector); + pdie(blaze_exit_code::INTERNAL_ERROR, "execv of '%s' failed", exe.c_str()); +} + +// Like connect(2), but uses the AF_UNIX address denoted by socket_file, +// resolving symbolic links. (The server may make "socket_file" a +// symlink, to avoid ENAMETOOLONG, in which case the client must +// resolve it in userspace before connecting.) +static int Connect(int socket, const string &socket_file) { + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + + char *resolved_path = realpath(socket_file.c_str(), NULL); + if (resolved_path != NULL) { + strncpy(addr.sun_path, resolved_path, sizeof addr.sun_path); + addr.sun_path[sizeof addr.sun_path - 1] = '\0'; + free(resolved_path); + sockaddr *paddr = reinterpret_cast<sockaddr *>(&addr); + return connect(socket, paddr, sizeof addr); + } else if (errno == ENOENT) { // No socket means no server to connect to + errno = ECONNREFUSED; + return -1; + } else { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "realpath('%s') failed", socket_file.c_str()); + } +} + +// Write the contents of file_name to stream. +static void WriteFileToStreamOrDie(FILE *stream, const char *file_name) { + FILE *fp = fopen(file_name, "r"); + if (fp == NULL) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "opening %s failed", file_name); + } + char buffer[255]; + int num_read; + while ((num_read = fread(buffer, 1, sizeof buffer, fp)) > 0) { + if (ferror(fp)) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "failed to read from '%s'", file_name); + } + fwrite(buffer, 1, num_read, stream); + } + fclose(fp); +} + +// Connects to the Blaze server, returning the socket, or -1 if no +// server is running and !start. If start, attempts to start a new +// server, and exits on failure. +static int ConnectToServer(bool start) { + int s = socket(PF_UNIX, SOCK_STREAM, 0); + if (s == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "can't create AF_UNIX socket"); + } + + string server_dir = globals->options.output_base + "/server"; + + // The server dir has the socket, so we don't allow access by other + // users. + if (MakeDirectories(server_dir, 0700) == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "server directory '%s' could not be created", server_dir.c_str()); + } + + string socket_file = server_dir + "/server.socket"; + + if (Connect(s, socket_file) == 0) { + return s; + } + if (!start) { + return -1; + } else { + SetScheduling( + globals->options.batch_cpu_scheduling, + globals->options.io_nice_level); + + int fd = StartServer(s); + if (fcntl(fd, F_SETFL, O_NONBLOCK | fcntl(fd, F_GETFL))) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Failed: fcntl to enable O_NONBLOCK on pipe"); + } + // Give the server one minute to start up. + for (int ii = 0; ii < 600; ++ii) { // 60s; enough time to connect + // with debugger + if (Connect(s, socket_file) == 0) { + if (ii) { + fputc('\n', stderr); + fflush(stderr); + } + return s; + } + fputc('.', stderr); + fflush(stderr); + poll(NULL, 0, 100); // sleep 100ms. (usleep(3) is obsolete.) + char c; + if (read(fd, &c, 1) != -1 || errno != EAGAIN) { + fprintf(stderr, "\nunexpected pipe read status: %s\n" + "Server presumed dead. Now printing '%s':\n", + strerror(errno), globals->jvm_log_file.c_str()); + WriteFileToStreamOrDie(stderr, globals->jvm_log_file.c_str()); + exit(blaze_exit_code::INTERNAL_ERROR); + } + } + die(blaze_exit_code::INTERNAL_ERROR, + "\nError: couldn't connect to server at '%s' after 60 seconds.", + socket_file.c_str()); + } +} + + +// Kills the specified running Blaze server. +static void KillRunningServer(pid_t server_pid) { + fprintf(stderr, "Sending SIGTERM to previous Blaze server (pid=%d)... ", + server_pid); + fflush(stderr); + for (int ii = 0; ii < 100; ++ii) { // wait up to 10s + if (kill(server_pid, SIGTERM) == -1) { + fprintf(stderr, "done.\n"); + return; // Ding! Dong! The witch is dead! + } + poll(NULL, 0, 100); // sleep 100ms. (usleep(3) is obsolete.) + } + + // If the previous attempt did not suceeded, kill the whole group. + fprintf(stderr, + "Sending SIGKILL to previous Blaze server process group (pid=%d)... ", + server_pid); + fflush(stderr); + killpg(server_pid, SIGKILL); + if (kill(server_pid, 0) == -1) { // (probe) + fprintf(stderr, "could not be killed.\n"); // task state 'Z' or 'D'? + exit(1); // TODO(bazel-team): confirm whether this is an internal error. + } else { + fprintf(stderr, "killed.\n"); + } +} + + +// Kills the running Blaze server, if any. Finds the pid from the socket. +static bool KillRunningServerIfAny() { + int socket = ConnectToServer(false); + if (socket != -1) { + KillRunningServer(GetPeerProcessId(socket)); + return true; + } + return false; +} + + +// Calls fsync() on the file (or directory) specified in 'file_path'. +// pdie()'s if syncing fails. +static void SyncFile(const char *file_path) { + // fsync always fails on Cygwin with "Permission denied" for some reason. +#ifndef __CYGWIN__ + int fd = open(file_path, O_RDONLY); + if (fd < 0) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "failed to open '%s' for syncing", file_path); + } + if (fsync(fd) < 0) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "failed to sync '%s'", file_path); + } + close(fd); +#endif +} + +// Walks the temporary directory recursively and collects full file paths. +static void CollectExtractedFiles(const string &dir_path, vector<string> &files) { + DIR *dir; + struct dirent *ent; + + if ((dir = opendir(dir_path.c_str())) == NULL) { + die(blaze_exit_code::INTERNAL_ERROR, "opendir failed"); + } + + while ((ent = readdir(dir)) != NULL) { + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) { + continue; + } + + string filename(blaze_util::JoinPath(dir_path, ent->d_name)); + bool is_directory; + if (ent->d_type == DT_UNKNOWN) { + struct stat buf; + if (lstat(filename.c_str(), &buf) == -1) { + die(blaze_exit_code::INTERNAL_ERROR, "stat failed"); + } + is_directory = S_ISDIR(buf.st_mode); + } else { + is_directory = (ent->d_type == DT_DIR); + } + + if (is_directory) { + CollectExtractedFiles(filename, files); + } else { + files.push_back(filename); + } + } + + closedir(dir); +} + +// Actually extracts the embedded data files into the tree whose root +// is 'embedded_binaries'. +static void ActuallyExtractData(const string &argv0, + const string &embedded_binaries) { + if (MakeDirectories(embedded_binaries, 0777) == -1) { + pdie(blaze_exit_code::INTERNAL_ERROR, + "couldn't create '%s'", embedded_binaries.c_str()); + } + + fprintf(stderr, "Extracting Blaze installation...\n"); + + struct archive *blaze_zip = archive_read_new(); + archive_read_support_format_zip(blaze_zip); + int retval = archive_read_open_filename(blaze_zip, argv0.c_str(), 10240); + if (retval != ARCHIVE_OK) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to open blaze as a zip file"); + } + + struct archive_entry *entry; + string install_base_key; + while (archive_read_next_header(blaze_zip, &entry) == ARCHIVE_OK) { + string path = blaze_util::JoinPath( + embedded_binaries, archive_entry_pathname(entry)); + if (MakeDirectories(blaze_util::Dirname(path), 0777) == -1) { + pdie(blaze_exit_code::INTERNAL_ERROR, + "couldn't create '%s'", path.c_str()); + } + int fd = open(path.c_str(), O_CREAT | O_WRONLY, archive_entry_perm(entry)); + if (fd < 0) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to open extraction file: %s", strerror(errno)); + } + + const void *buf; + size_t size; + off_t offset; + while (true) { + retval = archive_read_data_block(blaze_zip, &buf, &size, &offset); + if (retval == ARCHIVE_EOF) { + break; + } else if (retval != ARCHIVE_OK) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to extract data from blaze zip: (%d) %s", + archive_errno(blaze_zip), archive_error_string(blaze_zip)); + } + if (write(fd, buf, size) != size) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nError writing zipped file to %s", path.c_str()); + } + } + if (close(fd) != 0) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nCould not close file %s", path.c_str()); + } + } + retval = archive_read_free(blaze_zip); + if (retval != ARCHIVE_OK) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "\nFailed to close blaze zip"); + } + + const time_t TEN_YEARS_IN_SEC = 3600 * 24 * 365 * 10; + time_t future_time = time(NULL) + TEN_YEARS_IN_SEC; + + // Set the timestamps of the extracted files to the future and make sure (or + // at least as sure as we can...) that the files we have written are actually + // on the disk. + + vector<string> extracted_files; + CollectExtractedFiles(embedded_binaries, extracted_files); + + set<string> synced_directories; + for (vector<string>::iterator it = extracted_files.begin(); it != extracted_files.end(); it++) { + + const char *extracted_path = it->c_str(); + + // Set the time to a distantly futuristic value so we can observe tampering. + // Note that keeping the default timestamp set by unzip (1970-01-01) and using + // that to detect tampering is not enough, because we also need the timestamp + // to change between Blaze releases so that the metadata cache knows that + // the files may have changed. This is important for actions that use + // embedded binaries as artifacts. + struct utimbuf times = { future_time, future_time }; + if (utime(extracted_path, ×) == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "failed to set timestamp on '%s'", extracted_path); + } + + SyncFile(extracted_path); + + string directory = blaze_util::Dirname(extracted_path); + + // Now walk up until embedded_binaries and sync every directory in between. + // synced_directories is used to avoid syncing the same directory twice. + // The !directory.empty() and directory != "/" conditions are not strictly + // needed, but it makes this loop more robust, because otherwise, if due to + // some glitch, directory was not under embedded_binaries, it would get + // into an infinite loop. + while (directory != embedded_binaries && + synced_directories.count(directory) == 0 && + !directory.empty() && + directory != "/") { + SyncFile(directory.c_str()); + synced_directories.insert(directory); + directory = blaze_util::Dirname(directory); + } + } + + SyncFile(embedded_binaries.c_str()); +} + +// Installs Blaze by extracting the embedded data files, iff necessary. +// The MD5-named install_base directory on disk is trusted; we assume +// no-one has modified the extracted files beneath this directory once +// it is in place. Concurrency during extraction is handled by +// extracting in a tmp dir and then renaming it into place where it +// becomes visible automically at the new path. +// Populates globals->extracted_binaries with their extracted locations. +static void ExtractData(const string &self_path) { + // If the install dir doesn't exist, create it, if it does, we know it's good. + struct stat buf; + if (stat(globals->options.install_base.c_str(), &buf) == -1) { + uint64 st = MonotonicClock(); + // Work in a temp dir to avoid races. + string tmp_install = globals->options.install_base + ".tmp." + + std::to_string(getpid()); + string tmp_binaries = tmp_install + "/_embedded_binaries"; + ActuallyExtractData(self_path, tmp_binaries); + + uint64 et = MonotonicClock(); + globals->extract_data_time = (et - st) / 1000000LL; + + // Now rename the completed installation to its final name. If this + // fails due to an ENOTEMPTY then we assume another good + // installation snuck in before us. + if (rename(tmp_install.c_str(), globals->options.install_base.c_str()) == -1 + && errno != ENOTEMPTY) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "install base directory '%s' could not be renamed into place", + tmp_install.c_str()); + } + } else { + if (!S_ISDIR(buf.st_mode)) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Error: Install base directory '%s' could not be created. " + "It exists but is not a directory.", + globals->options.install_base.c_str()); + } + + const time_t time_now = time(NULL); + string real_install_dir = blaze_util::JoinPath( + globals->options.install_base, + "_embedded_binaries"); + for (const auto& it : globals->extracted_binaries) { + string path = blaze_util::JoinPath(real_install_dir, it); + // Check that the file exists and is readable. + if (stat(path.c_str(), &buf) == -1) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Error: corrupt installation: file '%s' missing." + " Please remove '%s' and try again.", + path.c_str(), globals->options.install_base.c_str()); + } + // Check that the timestamp is in the future. A past timestamp would indicate + // that the file has been tampered with. See ActuallyExtractData(). + if (buf.st_mtime <= time_now) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Error: corrupt installation: file '%s' " + "modified. Please remove '%s' and try again.", + path.c_str(), globals->options.install_base.c_str()); + } + } + } +} + +// Returns true if the server needs to be restarted to accommodate changes +// between the two argument lists. +static bool ServerNeedsToBeKilled(const vector<string>& args1, + const vector<string>& args2) { + // We need not worry about one side missing an argument and the other side + // having the default value, since this command line is already the + // canonicalized one that always contains every switch (with default values + // if it was not present on the real command line). Same applies for argument + // ordering. + if (args1.size() != args2.size()) { + return true; + } + + for (int i = 0; i < args1.size(); i++) { + string option_sources = "--option_sources="; + if (args1[i].substr(0, option_sources.size()) == option_sources && + args2[i].substr(0, option_sources.size()) == option_sources) { + continue; + } + + if (args1[i] !=args2[i]) { + return true; + } + + if (args1[i] == "--max_idle_secs") { + // Skip the argument of --max_idle_secs. + i++; + } + } + + return false; +} + +// Kills the running Blaze server, if any, if the startup options do not match. +static void KillRunningServerIfDifferentStartupOptions() { + int socket = ConnectToServer(false); + + if (socket == -1) { + return; + } + + pid_t server_pid = GetPeerProcessId(socket); + close(socket); + string cmdline_path = globals->options.output_base + "/server/cmdline"; + string joined_arguments; + + // No, /proc/$PID/cmdline does not work, because it is limited to 4K. Even + // worse, its behavior differs slightly between kernels (in some, when longer + // command lines are truncated, the last 4 bytes are replaced with + // "..." + NUL. + ReadFile(cmdline_path, &joined_arguments); + vector<string> arguments = blaze_util::Split(joined_arguments, '\0'); + + // These strings contain null-separated command line arguments. If they are + // the same, the server can stay alive, otherwise, it needs shuffle off this + // mortal coil. + if (ServerNeedsToBeKilled(arguments, GetArgumentArray())) { + globals->restart_reason = NEW_OPTIONS; + fprintf(stderr, + "WARNING: Running Blaze server needs to be killed, because the " + "startup options are different.\n"); + KillRunningServer(server_pid); + } +} + + +// Kills the old running server if it is not the same version as us, +// dealing with various combinations of installation scheme +// (installation symlink and older MD5_MANIFEST contents). +// This function requires that the installation be complete, and the +// server lock acquired. +static void EnsureCorrectRunningVersion() { + // Read the previous installation's semaphore symlink in output_base. If the + // target dirs don't match, or if the symlink was not present, then kill any + // running servers. Lastly, symlink to our installation so others know which + // installation is running. + string installation_path = globals->options.output_base + "/install"; + char prev_installation[PATH_MAX + 1] = ""; // NULs the whole array + if (readlink(installation_path.c_str(), + prev_installation, PATH_MAX) == -1 || + prev_installation != globals->options.install_base) { + if (KillRunningServerIfAny()) { + globals->restart_reason = NEW_VERSION; + } + unlink(installation_path.c_str()); + if (symlink(globals->options.install_base.c_str(), + installation_path.c_str())) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "failed to create installation symlink '%s'", + installation_path.c_str()); + } + const time_t time_now = time(NULL); + struct utimbuf times = { time_now, time_now }; + if (utime(globals->options.install_base.c_str(), ×) == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "failed to set timestamp on '%s'", + globals->options.install_base.c_str()); + } + } +} + + +// A signal-safe version of fprintf(stderr, ...). +// +// WARNING: any output from the blaze client may be interleaved +// with output from the blaze server. In --curses mode, +// the Blaze server often erases the previous line of output. +// So, be sure to end each such message with TWO newlines, +// otherwise it may be erased by the next message from the +// Blaze server. +// Also, it's a good idea to start each message with a newline, +// in case the Blaze server has written a partial line. +static void sigprintf(const char *format, ...) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int r = vsnprintf(buf, sizeof buf, format, ap); + va_end(ap); + write(STDERR_FILENO, buf, r); +} + + +// Signal handler. +static void handler(int signum) { + // A defensive measure: + if (kill(globals->server_pid, 0) == -1 && errno == ESRCH) { + sigprintf("\nBlaze server has died; client exiting.\n\n"); + _exit(1); + } + + switch (signum) { + case SIGINT: + if (++globals->sigint_count >= 3) { + sigprintf("\nBlaze caught third interrupt signal; killed.\n\n"); + kill(globals->server_pid, SIGKILL); + _exit(1); + } + sigprintf("\nBlaze caught interrupt signal; shutting down.\n\n"); + + kill(globals->server_pid, SIGINT); + break; + case SIGTERM: + sigprintf("\nBlaze caught terminate signal; shutting down.\n\n"); + kill(globals->server_pid, SIGINT); + break; + case SIGPIPE: + // Don't bother the user with a message in this case; they're + // probably using head(1) or more(1). + kill(globals->server_pid, SIGINT); + signal(SIGPIPE, SIG_IGN); // ignore subsequent SIGPIPE signals + globals->received_signal = SIGPIPE; + break; + case SIGQUIT: + sigprintf("\nSending SIGQUIT to JVM process %d (see %s).\n\n", + globals->server_pid, + globals->jvm_log_file.c_str()); + kill(globals->server_pid, SIGQUIT); + break; + } +} + + +// Reads a single char from the specified stream. +static char read_server_char(FILE *fp) { + int c = getc(fp); + if (c == EOF) { + // e.g. external SIGKILL of server, misplaced System.exit() in the server, + // or a JVM crash. Print out the jvm.out file in case there's something + // useful. + fprintf(stderr, "Error: unexpected EOF from Blaze server.\n" + "Contents of '%s':\n", globals->jvm_log_file.c_str()); + WriteFileToStreamOrDie(stderr, globals->jvm_log_file.c_str()); + exit(blaze_exit_code::INTERNAL_ERROR); + } + return static_cast<char>(c); +} + +// Constructs the command line for a server request, +static string BuildServerRequest() { + vector<string> arg_vector; + string command = globals->option_processor.GetCommand(); + if (command != "") { + arg_vector.push_back(command); + AddLoggingArgs(&arg_vector); + } + + globals->option_processor.GetCommandArguments(&arg_vector); + + string request("blaze"); + for (vector<string>::iterator it = arg_vector.begin(); + it != arg_vector.end(); it++) { + request.push_back('\0'); + request.append(*it); + } + return request; +} + +// Performs all I/O for a single client request to the server, and +// shuts down the client (by exit or signal). +static void SendServerRequest(void) ATTRIBUTE_NORETURN; +static void SendServerRequest(void) { + int socket = -1; + while (true) { + socket = ConnectToServer(true); + globals->server_pid = GetPeerProcessId(socket); + + // Check for deleted server cwd: + string server_cwd = GetProcessCWD(globals->server_pid); + if (server_cwd.empty() || // GetProcessCWD failed + server_cwd != globals->workspace || // changed + server_cwd.find(" (deleted)") != string::npos) { // deleted. + // There's a distant possibility that the two paths look the same yet are + // actually different because the two processes have different mount + // tables. + if (VerboseLogging()) { + fprintf(stderr, "Server's cwd moved or deleted (%s).\n", + server_cwd.c_str()); + } + close(socket); + KillRunningServer(globals->server_pid); + } else { + break; + } + } + + FILE *fp = fdopen(socket, "r"); // use buffering for reads--it's faster + + if (VerboseLogging()) { + fprintf(stderr, "Connected (server pid=%d).\n", globals->server_pid); + } + + // Wall clock time since process startup. + globals->startup_time = ProcessClock() / 1000000LL; + const string request = BuildServerRequest(); + + // Unblock all signals. + sigset_t sigset; + sigemptyset(&sigset); + sigprocmask(SIG_SETMASK, &sigset, NULL); + + signal(SIGINT, handler); + signal(SIGTERM, handler); + signal(SIGPIPE, handler); + signal(SIGQUIT, handler); + + // Send request and shutdown the write half of the connection: + // (Request is written in a single chunk.) + if (write(socket, request.data(), request.size()) != request.size()) { + pdie(blaze_exit_code::INTERNAL_ERROR, "write() to server failed"); + } + // In this (totally bizarre) protocol, this is the + // client's way of saying "um, that's the end of the request". + if (shutdown(socket, SHUT_WR) == -1) { + pdie(blaze_exit_code::INTERNAL_ERROR, "shutdown(WR) failed"); + } + + // Wait until we receive some response from the server. + // (We do this by calling select() with a timeout.) + // If we don't receive a response within 3 seconds, print a message, + // so that the user has some idea what is going on. + while (true) { + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(socket, &fdset); + struct timeval timeout; + timeout.tv_sec = 3; + timeout.tv_usec = 0; + int result = select(socket + 1, &fdset, NULL, &fdset, &timeout); + if (result > 0) { + // Data is ready on socket. Go ahead and read it. + break; + } else if (result == 0) { + // Timeout. Print a message, then go ahead and read from + // the socket (the read will usually block). + fprintf(stderr, + "INFO: Waiting for response from blaze server (pid %d)...\n", + globals->server_pid); + break; + } else { // result < 0 + // Error. For EINTR we try again, all other errors are fatal. + if (errno != EINTR) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "select() on server socket failed"); + } + } + } + + // Read and demux the response. This protocol is awful. + for (;;) { + // Read one line: + char at = read_server_char(fp); + assert(at == '@'); + (void) at; // avoid warning about unused variable + char tag = read_server_char(fp); + assert(tag == '1' || tag == '2' || tag == '3'); + char at_or_newline = read_server_char(fp); + bool second_at = at_or_newline == '@'; + if (second_at) { + at_or_newline = read_server_char(fp); + } + assert(at_or_newline == '\n'); + + if (tag == '3') { + // In this (totally bizarre) protocol, this is the + // server's way of saying "um, that's the end of the response". + break; + } + FILE *stream = tag == '1' ? stdout : stderr; + for (;;) { + char c = read_server_char(fp); + if (c == '\n') { + if (!second_at) fputc(c, stream); + fflush(stream); + break; + } else { + fputc(c, stream); + } + } + } + + char line[255]; + if (fgets(line, sizeof line, fp) == NULL || + !isdigit(line[0])) { + die(blaze_exit_code::INTERNAL_ERROR, + "Error: can't read exit code from server."); + } + int exit_code; + blaze_util::safe_strto32(line, &exit_code); + + close(socket); // might fail EINTR, just ignore. + + if (globals->received_signal) { // Kill ourselves with the same signal, so + // that callers see the right WTERMSIG value. + signal(globals->received_signal, SIG_DFL); + raise(globals->received_signal); + exit(1); // (in case raise didn't kill us for some reason) + } + + exit(exit_code); +} + +// Parse the options, storing parsed values in globals. +// Returns the index of the first non-option argument. +static void ParseOptions(int argc, const char *argv[]) { + string error; + blaze_exit_code::ExitCode parse_exit_code = + globals->option_processor.ParseOptions(argc, argv, globals->workspace, + globals->cwd, &error); + if (parse_exit_code != blaze_exit_code::SUCCESS) { + die(parse_exit_code, "%s", error.c_str()); + } + globals->options = globals->option_processor.GetParsedStartupOptions(); +} + +// Returns the canonical form of a path. +static string MakeCanonical(const char *path) { + char *resolved_path = realpath(path, NULL); + if (resolved_path == NULL) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "realpath('%s') failed", path); + } + + string ret = resolved_path; + free(resolved_path); + return ret; +} + +// Compute the globals globals->cwd and globals->workspace. +static void ComputeWorkspace() { + char cwdbuf[PATH_MAX]; + if (getcwd(cwdbuf, sizeof cwdbuf) == NULL) { + pdie(blaze_exit_code::INTERNAL_ERROR, "getcwd() failed"); + } + globals->cwd = MakeCanonical(cwdbuf); + globals->workspace = BlazeStartupOptions::GetWorkspace(globals->cwd); +} + +// Figure out the base directories based on embedded data, username, cwd, etc. +// Sets globals->options.install_base, globals->options.output_base, +// globals->lock_file, globals->jvm_log_file. +static void ComputeBaseDirectories(const string self_path) { + // Only start a server when in a workspace because otherwise we won't do more + // than emit a help message. + if (!BlazeStartupOptions::InWorkspace(globals->workspace)) { + globals->options.batch = true; + } + + // The default install_base is <output_user_root>/install/<md5(blaze)> + // but if an install_base is specified on the command line, we use that as + // the base instead. + if (globals->options.install_base.empty()) { + string install_user_root = globals->options.output_user_root + "/install"; + globals->options.install_base = + GetInstallBase(install_user_root, self_path); + } else { + // We call GetInstallBase anyway to populate extracted_binaries. + GetInstallBase("", self_path); + } + + if (globals->options.output_base.empty()) { + globals->options.output_base = GetHashedBaseDir( + globals->options.output_user_root, globals->workspace); + } + + struct stat buf; + if (stat(globals->options.output_base.c_str(), &buf) == -1) { + if (MakeDirectories(globals->options.output_base, 0777) == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Output base directory '%s' could not be created", + globals->options.output_base.c_str()); + } + } else { + if (!S_ISDIR(buf.st_mode)) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Error: Output base directory '%s' could not be created. " + "It exists but is not a directory.", + globals->options.output_base.c_str()); + } + } + if (access(globals->options.output_base.c_str(), R_OK | W_OK | X_OK) != 0) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "Error: Output base directory '%s' must be readable and writable.", + globals->options.output_base.c_str()); + } + + globals->options.output_base = + MakeCanonical(globals->options.output_base.c_str()); + globals->lockfile = globals->options.output_base + "/lock"; + globals->jvm_log_file = globals->options.output_base + "/server/jvm.out"; +} + +static void CheckEnvironment() { + char pthread_impl[512]; +#ifndef _CS_GNU_LIBPTHREAD_VERSION +#define _CS_GNU_LIBPTHREAD_VERSION 3 +#endif + if (confstr(_CS_GNU_LIBPTHREAD_VERSION, pthread_impl, sizeof pthread_impl) && + strprefix(pthread_impl, "linuxthreads")) { + fprintf(stderr, "Warning: LinuxThreads detected. NPTL is preferred.\n" + " (Perhaps unset LD_ASSUME_KERNEL or LD_LIBRARY_PATH.)\n"); + } + + if (getenv("LD_ASSUME_KERNEL") != NULL) { + // Fix for bug: if ulimit -s and LD_ASSUME_KERNEL are both + // specified, the JVM fails to create threads. See thread_stack_regtest. + // This is also provoked by LD_LIBRARY_PATH=/usr/lib/debug, + // or anything else that causes the JVM to use LinuxThreads. + fprintf(stderr, "Warning: ignoring LD_ASSUME_KERNEL in environment.\n"); + unsetenv("LD_ASSUME_KERNEL"); + } + + if (getenv("LD_PRELOAD") != NULL) { + fprintf(stderr, "Warning: ignoring LD_PRELOAD in environment.\n"); + unsetenv("LD_PRELOAD"); + } + + if (getenv("_JAVA_OPTIONS") != NULL) { + // This would override --host_jvm_args + fprintf(stderr, "Warning: ignoring _JAVA_OPTIONS in environment.\n"); + unsetenv("_JAVA_OPTIONS"); + } + + if (TESTING) { + fprintf(stderr, "INFO: $TEST_TMPDIR defined: output root default is " + "'%s'.\n", globals->options.output_root.c_str()); + } + + // TODO(bazel-team): We've also seen a failure during loading (creating + // threads?) when ulimit -Hs 8192. Characterize that and check for it here. + + // Make the JVM use ISO-8859-1 for parsing its command line because "blaze + // run" doesn't handle non-ASCII command line arguments. This is apparently + // the most reliable way to select the platform default encoding. + setenv("LANG", "en_US.ISO-8859-1", 1); + setenv("LANGUAGE", "en_US.ISO-8859-1", 1); + setenv("LC_ALL", "en_US.ISO-8859-1", 1); + setenv("LC_CTYPE", "en_US.ISO-8859-1", 1); +} + +// Create the lockfile and take an exclusive lock on a region within it. This +// lock is inherited with the file descriptor across execve(), but not fork(). +// So in the batch case, the JVM holds the lock until exit; otherwise, this +// program holds it until exit. +static void AcquireLock() { + globals->lockfd = open(globals->lockfile.c_str(), O_CREAT|O_RDWR, 0644); + if (globals->lockfd < 0) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "cannot open lockfile '%s' for writing", globals->lockfile.c_str()); + } + + struct flock lock; + lock.l_type = F_WRLCK; + lock.l_whence = SEEK_SET; + lock.l_start = 0; + // This doesn't really matter now, but allows us to subdivide the lock + // later if that becomes meaningful. (Ranges beyond EOF can be locked.) + lock.l_len = 4096; + + // Try to take the lock, without blocking. + if (fcntl(globals->lockfd, F_SETLK, &lock) == -1) { + if (errno != EACCES && errno != EAGAIN) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "unexpected result from F_SETLK"); + } + + // We didn't get the lock. Find out who has it. + struct flock probe = lock; + probe.l_pid = 0; + if (fcntl(globals->lockfd, F_GETLK, &probe) == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "unexpected result from F_GETLK"); + } + if (!globals->options.block_for_lock) { + die(blaze_exit_code::BAD_ARGV, + "Another Blaze command is running (pid=%d). Exiting immediately.", + probe.l_pid); + } + fprintf(stderr, "Another Blaze command is running (pid = %d). " + "Waiting for it to complete...", probe.l_pid); + fflush(stderr); + + // Take a clock sample for that start of the waiting time + uint64 st = MonotonicClock(); + // Try to take the lock again (blocking). + int r; + do { + r = fcntl(globals->lockfd, F_SETLKW, &lock); + } while (r == -1 && errno == EINTR); + fprintf(stderr, "\n"); + if (r == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "couldn't acquire file lock"); + } + // Take another clock sample, calculate elapsed + uint64 et = MonotonicClock(); + globals->command_wait_time = (et - st) / 1000000LL; + } + + // Identify ourselves in the lockfile. + ftruncate(globals->lockfd, 0); + const char *tty = ttyname(STDIN_FILENO); // NOLINT (single-threaded) + string msg = "owner=blaze launcher\npid=" + std::to_string(getpid()) + + "\ntty=" + (tty ? tty : "") + "\n"; + // Don't bother checking for error, since it's unlikely and unimportant. + // The contents are currently meant only for debugging. + write(globals->lockfd, msg.data(), msg.size()); +} + +// Returns the mountpoint containing the specified directory, which +// must exist. Fails if any parent path could not be statted or +// canonicalised. +static string GetMountpoint(string dir) { + dev_t initial_device = -1; + ino_t prev_inode = -1; + string prev_dir = dir; + for (;;) { + struct stat buf; + if (stat(dir.c_str(), &buf) == -1) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "stat('%s') failed", dir.c_str()); + } else if (initial_device == -1 && prev_inode == -1) { // first time + initial_device = buf.st_dev; + } else if (initial_device != buf.st_dev) { // we crossed file systems + char *resolved_path = realpath(prev_dir.c_str(), NULL); + if (resolved_path == NULL) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "realpath('%s') failed", prev_dir.c_str()); + } + dir = resolved_path; + free(resolved_path); + return dir; + } else if (prev_inode == buf.st_ino) { // ".." had no effect => root. + return "/"; + } + + prev_inode = buf.st_ino; + prev_dir = dir; + dir += "/.."; + } + + return "/"; +} + +// Issue a warning if disk has less than 10% free blocks or inodes. +static void WarnIfFullDisk() { + struct statvfs buf; + if (statvfs(globals->options.output_base.c_str(), &buf) < 0) { + fprintf(stderr, "WARNING: couldn't get file system information for '%s': " + "%s\n", globals->options.output_base.c_str(), strerror(errno)); + return; + } + + if (10LL * buf.f_favail < buf.f_files) { + fprintf(stderr, + "WARNING: build volume %s is nearly full " + "(%llu inodes remain).\n", + GetMountpoint(globals->options.output_base).c_str(), + static_cast<int64>(buf.f_favail)); + } + if (10LL * buf.f_bavail < buf.f_blocks) { + fprintf(stderr, + "WARNING: build volume %s is nearly full " + "(%.1fGB remain).\n", + GetMountpoint(globals->options.output_base).c_str(), + (1.0 * buf.f_bavail) * buf.f_frsize / 1E9); + } +} + +void SetupStreams() { + // Line-buffer stderr, since we always flush at the end of a server + // message. This saves lots of single-char calls to write(2). + // This doesn't work if any writes to stderr have already occurred! + setlinebuf(stderr); + + // Ensure we have three open fds. Otherwise we can end up with + // bizarre things like stdout going to the lock file, etc. + if (fcntl(0, F_GETFL) == -1) open("/dev/null", O_RDONLY); + if (fcntl(1, F_GETFL) == -1) open("/dev/null", O_WRONLY); + if (fcntl(2, F_GETFL) == -1) open("/dev/null", O_WRONLY); +} + +// Set an 8MB stack for Blaze. When the stack max is unbounded, it changes the +// layout in the JVM's address space, and we are unable to instantiate the +// default 3000MB heap. +static void EnsureFiniteStackLimit() { + struct rlimit limit; + const int default_stack = 8 * 1024 * 1024; // 8MB. + if (getrlimit(RLIMIT_STACK, &limit)) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "getrlimit() failed"); + } + + if (default_stack < limit.rlim_cur) { + limit.rlim_cur = default_stack; + if (setrlimit(RLIMIT_STACK, &limit)) { + perror("setrlimit() failed: If the stack limit is too high, " + "this can cause the JVM to be unable to allocate enough " + "contiguous address space for its heap"); + } + } +} + +static void CheckBinaryPath(const string& argv0) { + if (argv0[0] == '/') { + globals->binary_path = argv0; + } else { + string abs_path = globals->cwd + '/' + argv0; + char *resolved_path = realpath(abs_path.c_str(), NULL); + if (resolved_path) { + globals->binary_path = resolved_path; + free(resolved_path); + } else { + // This happens during our integration tests, but thats okay, as we won't + // log the invocation anyway. + globals->binary_path = abs_path; + } + } +} + +// Create the user's directory where we keep state, installations etc. +// Typically, this happens inside a temp directory, so we have to be +// careful about symlink attacks. +static void CreateSecureOutputRoot() { + const char* root = globals->options.output_user_root.c_str(); + struct stat fileinfo = {}; + + if (mkdir(root, 0755) == 0) { + return; // mkdir succeeded, no need to verify ownership/mode. + } + if (errno != EEXIST) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "mkdir('%s')", root); + } + + // The path already exists. + // Check ownership and mode, and verify that it is a directory. + + if (lstat(root, &fileinfo) < 0) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "lstat('%s')", root); + } + + if (fileinfo.st_uid != geteuid()) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "'%s' is not owned by me", + root); + } + + if ((fileinfo.st_mode & 022) != 0) { + int new_mode = fileinfo.st_mode & (~022); + if (chmod(root, new_mode) < 0) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, + "'%s' has mode %o, chmod to %o failed", root, + fileinfo.st_mode & 07777, new_mode); + } + } + + if (stat(root, &fileinfo) < 0) { + pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "stat('%s')", root); + } + + if (!S_ISDIR(fileinfo.st_mode)) { + die(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "'%s' is not a directory", + root); + } +} + +// TODO(bazel-team): Execute the server as a child process and write its exit +// code to a file. In case the server becomes unresonsive or terminates +// unexpectedly (in a way that isn't already handled), we can observe the file, +// if it exists. (If it doesn't, then we know something went horribly wrong.) +int main(int argc, const char *argv[]) { + InitGlobals(); + SetupStreams(); + + // Must be done before command line parsing. + ComputeWorkspace(); + CheckBinaryPath(argv[0]); + ParseOptions(argc, argv); + string error; + blaze_exit_code::ExitCode reexec_options_exit_code = + globals->options.CheckForReExecuteOptions(argc, argv, &error); + if (reexec_options_exit_code != blaze_exit_code::SUCCESS) { + die(reexec_options_exit_code, "%s", error.c_str()); + } + CheckEnvironment(); + CreateSecureOutputRoot(); + + const string self_path = GetSelfPath(); + ComputeBaseDirectories(self_path); + + AcquireLock(); + + WarnIfFullDisk(); + WarnFilesystemType(globals->options.output_base); + EnsureFiniteStackLimit(); + + ExtractData(self_path); + EnsureCorrectRunningVersion(); + KillRunningServerIfDifferentStartupOptions(); + + if (globals->options.batch) { + SetScheduling(globals->options.batch_cpu_scheduling, + globals->options.io_nice_level); + StartStandalone(); + } else { + SendServerRequest(); + } + return 0; +} +} // namespace blaze + +int main(int argc, const char *argv[]) { + return blaze::main(argc, argv); +} |