Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion src/build.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,58 @@ BuildResult DryRunCommandRunner::WaitForCommand() {
return BuildResult::CommandCompleted{ edge, status };
}

/// Tracks the ninja lock file we have written so it can be removed at process
/// exit even on code paths that bypass C++ stack unwinding (e.g. Fatal()).
struct LockFileGuard {
std::string path;
int pid = 0;
bool atexit_registered = false;
};

LockFileGuard& GetLockFileGuard() {
static LockFileGuard guard;
return guard;
}

/// Removes the lock file at exit, but only if it still contains our PID.
/// This avoids removing a lock file that was overwritten by another concurrent
/// ninja invocation.
void RemoveLockFileAtExit() {
LockFileGuard& guard = GetLockFileGuard();
if (guard.path.empty() || guard.pid == 0) {
return;
}
FILE* fp = fopen(guard.path.c_str(), "rb");
if (!fp) {
return;
}
char buf[32];
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
buf[n] = '\0';
fclose(fp);
char* end = nullptr;
long pid = strtol(buf, &end, 10);
if (end != buf && pid > 0 && static_cast<int>(pid) == guard.pid) {
remove(guard.path.c_str());
}
}

void RegisterLockFileGuard(const std::string& path, int pid) {
LockFileGuard& guard = GetLockFileGuard();
guard.path = path;
guard.pid = pid;
if (!guard.atexit_registered) {
atexit(&RemoveLockFileAtExit);
guard.atexit_registered = true;
}
}

void ClearLockFileGuard() {
LockFileGuard& guard = GetLockFileGuard();
guard.path.clear();
guard.pid = 0;
}

} // namespace

Plan::Plan(Builder* builder)
Expand Down Expand Up @@ -612,6 +664,27 @@ Builder::Builder(State* state, const BuildConfig& config, BuildLog* build_log,
string build_dir = state_->bindings_.LookupVariable("builddir");
if (!build_dir.empty())
lock_file_path_ = build_dir + "/" + lock_file_path_;

// If a lock file from a previous (or concurrent) ninja invocation exists,
// see whether that process is still alive and warn the user.
std::string stat_err;
if (disk_interface_->Stat(lock_file_path_, &stat_err) > 0) {
std::string lock_contents;
std::string read_err;
if (disk_interface_->ReadFile(lock_file_path_, &lock_contents, &read_err) ==
FileReader::Okay) {
char* end = nullptr;
long pid = strtol(lock_contents.c_str(), &end, 10);
if (end != lock_contents.c_str() && pid > 0 &&
static_cast<int>(pid) != GetPid() &&
IsProcessRunning(static_cast<int>(pid))) {
Warning("another ninja process (pid %ld) seems to be running in this "
"build directory; if it is not, delete %s",
pid, lock_file_path_.c_str());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a Fatal() error instead. Two concurrent Ninjas are likely to break everything, from the .ninja_deps and .ninja_log files, and the content of output files when both launch the same command that want to write to the same output using a tool that doesn't support that well (e.g. without atomically renaming the output file).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately ninja calling ninja again on the same build dir is used in the wild. And it "works" right now, a fatal error would be a hard regression.

When thinking about it even the warning is too much. We need to correctly fix this (the second ninja process communicates to the first process which targets it wants to run).

}
}
}

status_->SetExplanations(explanations_.get());
}

Expand Down Expand Up @@ -652,6 +725,7 @@ void Builder::Cleanup() {
string err;
if (disk_interface_->Stat(lock_file_path_, &err) > 0)
disk_interface_->RemoveFile(lock_file_path_);
ClearLockFileGuard();
}

Node* Builder::AddTarget(const string& name, string* err) {
Expand Down Expand Up @@ -858,7 +932,10 @@ bool Builder::StartEdge(Edge* edge, string* err) {
if (!disk_interface_->MakeDirs((*o)->path()))
return false;
if (build_start == -1) {
disk_interface_->WriteFile(lock_file_path_, "", false);
char pid_str[32];
snprintf(pid_str, sizeof(pid_str), "%d", GetPid());
disk_interface_->WriteFile(lock_file_path_, pid_str, false);
RegisterLockFileGuard(lock_file_path_, GetPid());
build_start = disk_interface_->Stat(lock_file_path_, err);
if (build_start == -1)
build_start = 0;
Expand Down
32 changes: 32 additions & 0 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include <sys/types.h>

#ifndef _WIN32
#include <signal.h>
#include <unistd.h>
#include <sys/time.h>
#endif
Expand Down Expand Up @@ -1004,6 +1005,37 @@ std::string GetWorkingDirectory() {
return ret;
}

int GetPid() {
#ifdef _WIN32
return static_cast<int>(GetCurrentProcessId());
#else
return static_cast<int>(getpid());
#endif
}

bool IsProcessRunning(int pid) {
if (pid <= 0) {
return false;
}
#ifdef _WIN32
HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE,
static_cast<DWORD>(pid));
if (!handle) {
return false;
}
DWORD exit_code = 0;
BOOL ok = GetExitCodeProcess(handle, &exit_code);
CloseHandle(handle);
return ok && exit_code == STILL_ACTIVE;
#else
if (kill(static_cast<pid_t>(pid), 0) == 0) {
return true;
}
// EPERM means the process exists but we can't signal it.
return errno == EPERM;
#endif
}

bool Truncate(const string& path, size_t size, string* err) {
#ifdef _WIN32
int fh = _sopen(path.c_str(), _O_RDWR | _O_CREAT, _SH_DENYNO,
Expand Down
7 changes: 7 additions & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ double GetLoadAverage();
/// a wrapper for getcwd()
std::string GetWorkingDirectory();

/// @return the current process ID.
int GetPid();

/// @return true if a process with the given \a pid is currently running.
/// Returns false for invalid (non-positive) pids.
bool IsProcessRunning(int pid);

/// Truncates a file to the given size.
bool Truncate(const std::string& path, size_t size, std::string* err);

Expand Down
Loading