blob: 2db141ff3a1a468b9b80696cff963cd8ae6a5574 [file] [edit]
// Copyright 2014 The Bazel Authors. 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.
#include "src/main/native/unix_jni.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <jni.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <utime.h>
#include <iterator>
// Linting disabled for this line because for google code we could use
// absl::Mutex but we cannot yet because Bazel doesn't depend on absl.
#include <mutex> // NOLINT
#include <string>
#include <vector>
#include "src/main/cpp/util/logging.h"
#include "src/main/native/latin1_jni_path.h"
#define RESTARTABLE(_cmd, _result) \
do { \
do { \
_result = _cmd; \
} while ((_result == -1) && (errno == EINTR)); \
} while (0)
#define RESTARTABLE_PTR(_cmd, _result) \
do { \
do { \
_result = _cmd; \
} while ((_result == nullptr) && (errno == EINTR)); \
} while (0)
namespace blaze_jni {
#define FILE_BASENAME "unix_jni.cc"
namespace {
static jclass getClass(JNIEnv* env, const char* name) {
jclass lookup_result = env->FindClass(name);
BAZEL_CHECK_NE(lookup_result, nullptr);
return static_cast<jclass>(env->NewGlobalRef(lookup_result));
}
static jmethodID getConstructorID(JNIEnv* env, jclass clazz, const char* sig) {
jmethodID method = env->GetMethodID(clazz, "<init>", sig);
BAZEL_CHECK_NE(method, nullptr) << sig;
return method;
}
static jobject getStaticObjectField(JNIEnv* env, jclass clazz, const char* name,
const char* sig) {
jfieldID field = env->GetStaticFieldID(clazz, name, sig);
BAZEL_CHECK_NE(field, nullptr);
return static_cast<jobject>(
env->NewGlobalRef(env->GetStaticObjectField(clazz, field)));
}
} // namespace
struct DIROrError {
DIR *dir;
int error;
};
static void PostException(JNIEnv *env, const char *exception_classname,
const std::string &message) {
jclass exception_class = env->FindClass(exception_classname);
bool success = false;
if (exception_class != nullptr) {
success = env->ThrowNew(exception_class, message.c_str()) == 0;
}
if (!success) {
BAZEL_LOG(FATAL) << "Failed to throw Java exception from JNI: "
<< message.c_str();
}
}
static jobject GetPosixError(JNIEnv* env, int error_number) {
static const jclass error_class =
getClass(env,
"com/google/devtools/build/lib/unix/"
"NativePosixFilesException$PosixError");
static const char* field_sig =
"Lcom/google/devtools/build/lib/unix/"
"NativePosixFilesException$PosixError;";
static const jobject error_enoent =
getStaticObjectField(env, error_class, "ENOENT", field_sig);
static const jobject error_eacces =
getStaticObjectField(env, error_class, "EACCES", field_sig);
static const jobject error_eloop =
getStaticObjectField(env, error_class, "ELOOP", field_sig);
static const jobject error_etimedout =
getStaticObjectField(env, error_class, "ETIMEDOUT", field_sig);
static const jobject error_other =
getStaticObjectField(env, error_class, "OTHER", field_sig);
switch (error_number) {
case ETIMEDOUT: // Local socket timed out
return error_etimedout;
case ENOENT: // No such file or directory
return error_enoent;
case EACCES: // Permission denied
return error_eacces;
case ELOOP: // Too many symbolic links encountered
return error_eloop;
case EBADF: // Bad file number or descriptor already closed.
case ENAMETOOLONG: // File name too long
case ENODATA: // No data available
#if defined(EMULTIHOP)
case EMULTIHOP: // Multihop attempted
#endif
case EINVAL: // Invalid argument
case EINTR: // Interrupted system call
case ENOMEM: // Out of memory
case EPERM: // Operation not permitted
case ENOLINK: // Link has been severed
case EIO: // I/O error
case EAGAIN: // Try again
case EFBIG: // File too large
case EPIPE: // Broken pipe
case ENOSPC: // No space left on device
case EXDEV: // Cross-device link
case EROFS: // Read-only file system
case EEXIST: // File exists
case EMLINK: // Too many links
case EISDIR: // Is a directory
case ENOTDIR: // Not a directory
case ENOTEMPTY: // Directory not empty
case EBUSY: // Device or resource busy
case ENFILE: // File table overflow
case EMFILE: // Too many open files
default:
return error_other;
}
}
static void PostNativePosixFilesException(JNIEnv* env,
const std::string& message,
jobject posix_error) {
static const jclass exception_class = getClass(
env, "com/google/devtools/build/lib/unix/NativePosixFilesException");
static const jmethodID constructor =
getConstructorID(env, exception_class,
"(Ljava/lang/String;Lcom/google/devtools/build/lib/unix/"
"NativePosixFilesException$PosixError;)V");
jstring j_message = env->NewStringUTF(message.c_str());
bool success = false;
if (j_message != nullptr) {
jobject exception =
env->NewObject(exception_class, constructor, j_message, posix_error);
if (exception != nullptr) {
success = env->Throw(reinterpret_cast<jthrowable>(exception)) == 0;
}
}
if (!success) {
BAZEL_LOG(FATAL) << "Failed to throw NativePosixFilesException: "
<< message.c_str();
}
}
// See unix_jni.h.
void PostException(JNIEnv* env, int error_number, const std::string& message) {
// Select the most appropriate Java exception for a given UNIX error number.
const char* exception_classname = nullptr;
switch (error_number) {
case EFAULT: // Illegal pointer (unlikely; perhaps from or via FUSE?)
exception_classname = "java/lang/IllegalArgumentException";
break;
case ENOSYS: // Function not implemented
case ENOTSUP: // Operation not supported on transport endpoint
// (aka EOPNOTSUPP)
exception_classname = "java/lang/UnsupportedOperationException";
break;
}
std::string formatted_message =
message + " (" + ErrorMessage(error_number) + ")";
if (exception_classname != nullptr) {
PostException(env, exception_classname, formatted_message);
} else {
jobject posix_error = GetPosixError(env, error_number);
PostNativePosixFilesException(env, formatted_message, posix_error);
}
}
static void PostAssertionError(JNIEnv *env, const std::string& message) {
PostException(env, "java/lang/AssertionError", message);
}
// "TOSTRING" macro from: https://stackoverflow.com/a/240370
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define POST_EXCEPTION_FROM_ERRNO(env, error, message) \
PostException(env, error, \
std::string("[" FILE_BASENAME ":" TOSTRING(__LINE__) "] ") + \
std::string(message))
#define POST_ASSERTION_ERROR(env, message) \
PostAssertionError( \
env, std::string("[" FILE_BASENAME ":" TOSTRING(__LINE__) "] ") + \
std::string(message))
// Throws RuntimeExceptions for IO operations which fail unexpectedly.
// See package-info.html.
// Returns true iff an exception was thrown.
static bool PostRuntimeException(JNIEnv *env, int error_number,
const char *file_path) {
const char *exception_classname;
switch (error_number) {
case EFAULT: // Illegal pointer--not likely
case EBADF: // Bad file number
exception_classname = "java/lang/IllegalArgumentException";
break;
case ENOMEM: // Out of memory
exception_classname = "java/lang/OutOfMemoryError";
break;
case ENOTSUP: // Operation not supported on transport endpoint
// (aka EOPNOTSUPP)
exception_classname = "java/lang/UnsupportedOperationException";
break;
default:
exception_classname = nullptr;
}
if (exception_classname == nullptr) {
return false;
}
jclass exception_class = env->FindClass(exception_classname);
if (exception_class != nullptr) {
std::string message(file_path);
message += " (";
message += ErrorMessage(error_number);
message += ")";
env->ThrowNew(exception_class, message.c_str());
return true;
} else {
BAZEL_LOG(FATAL) << "Unable to find exception_class: "
<< exception_classname;
return false;
}
}
static JavaVM *GetJavaVM(JNIEnv *env) {
static JavaVM *java_vm = nullptr;
static std::mutex java_vm_mtx;
std::lock_guard<std::mutex> lock(java_vm_mtx);
if (env != nullptr) {
JavaVM *env_java_vm;
jint value = env->GetJavaVM(&env_java_vm);
if (value != 0) {
return nullptr;
}
if (java_vm == nullptr) {
java_vm = env_java_vm;
} else if (java_vm != env_java_vm) {
return nullptr;
}
}
return java_vm;
}
static void PerformIntegerValueCallback(jobject intConsumerObject, int value) {
JavaVM *java_vm = GetJavaVM(nullptr);
JNIEnv *java_env;
int status = java_vm->GetEnv((void **)&java_env, JNI_VERSION_1_8);
bool attach_current_thread = false;
if (status == JNI_EDETACHED) {
attach_current_thread = true;
} else {
BAZEL_CHECK_EQ(status, JNI_OK);
}
if (attach_current_thread) {
BAZEL_CHECK_EQ(java_vm->AttachCurrentThread((void **)&java_env, nullptr),
0);
}
jclass clazz = java_env->GetObjectClass(intConsumerObject);
BAZEL_CHECK_NE(clazz, nullptr);
// Java: IntConsumer#accept(int)
jmethodID method_id = java_env->GetMethodID(clazz, "accept", "(I)V");
BAZEL_CHECK_NE(method_id, nullptr);
java_env->CallVoidMethod(intConsumerObject, method_id, value);
if (attach_current_thread) {
BAZEL_CHECK_EQ(java_vm->DetachCurrentThread(), JNI_OK);
}
}
// TODO(bazel-team): split out all the FileSystem class's native methods
// into a separate source file, fsutils.cc.
extern "C" JNIEXPORT jstring JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_readlink(
JNIEnv* env, jobject instance, jstring path) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return nullptr;
}
char target[PATH_MAX + 1];
ssize_t len;
RESTARTABLE(readlink(path_chars, target, std::size(target) - 1), len);
if (len == -1) {
if (errno == EINVAL) {
// Not a symlink.
return nullptr;
}
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
return nullptr;
}
target[len] = '\0';
return NewStringLatin1(env, target);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_chmod(
JNIEnv* env, jobject instance, jstring path, jint mode) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return;
}
int err;
RESTARTABLE(chmod(path_chars, static_cast<int>(mode)), err);
if (err == -1) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
}
static void link_common(JNIEnv *env,
jstring oldpath,
jstring newpath,
int (*link_function)(const char *, const char *)) {
JStringLatin1Holder oldpath_chars(env, oldpath);
JStringLatin1Holder newpath_chars(env, newpath);
if (env->ExceptionOccurred()) {
return;
}
int err;
RESTARTABLE(link_function(oldpath_chars, newpath_chars), err);
if (err == -1) {
POST_EXCEPTION_FROM_ERRNO(env, errno, newpath_chars);
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_link(
JNIEnv* env, jobject instance, jstring oldpath, jstring newpath) {
link_common(env, oldpath, newpath, ::link);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_symlink(
JNIEnv* env, jobject instance, jstring oldpath, jstring newpath) {
link_common(env, oldpath, newpath, ::symlink);
}
namespace {
static jobject NewStat(JNIEnv* env, const portable_stat_struct& stat_ref) {
static const jclass stat_class = getClass(
env, "com/google/devtools/build/lib/unix/NativePosixFilesService$Stat");
static const jmethodID file_status_class_ctor =
getConstructorID(env, stat_class, "(IJJJJ)V");
return env->NewObject(
stat_class, file_status_class_ctor, static_cast<jint>(stat_ref.st_mode),
static_cast<jlong>(StatEpochMilliseconds(stat_ref, STAT_MTIME)),
static_cast<jlong>(StatEpochMilliseconds(stat_ref, STAT_CTIME)),
static_cast<jlong>(stat_ref.st_size),
static_cast<jlong>(stat_ref.st_ino));
}
} // namespace
namespace {
static jobject StatCommon(JNIEnv *env, jstring path,
int (*stat_function)(const char *,
portable_stat_struct *),
char error_handling) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return nullptr;
}
portable_stat_struct statbuf;
int err;
RESTARTABLE(stat_function(path_chars, &statbuf), err);
if (err == -1) {
// Save errno immediately, before we do any other syscalls.
int saved_errno = errno;
// Throw a RuntimeException if errno suggests a programming error.
if (PostRuntimeException(env, saved_errno, path_chars)) {
return nullptr;
}
// Throw an IOException if requested by the error handling mode.
if (error_handling == 'a' ||
(error_handling == 'f' && saved_errno != ENOENT &&
saved_errno != ENOTDIR)) {
POST_EXCEPTION_FROM_ERRNO(env, saved_errno, path_chars);
return nullptr;
}
// Otherwise, return null.
return nullptr;
}
return NewStat(env, statbuf);
}
} // namespace
extern "C" JNIEXPORT jobject JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_stat(
JNIEnv* env, jobject instance, jstring path, jchar error_handling) {
return StatCommon(env, path, portable_stat, error_handling);
}
extern "C" JNIEXPORT jobject JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_lstat(
JNIEnv* env, jobject instance, jstring path, jchar error_handling) {
return StatCommon(env, path, portable_lstat, error_handling);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_utimensat(
JNIEnv* env, jobject instance, jstring path, jboolean now, jlong millis) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return;
}
// If `now` is true use the current time, otherwise use `millis`.
// On Linux, if the current user has write permission but isn't the owner of
// the file, atime and mtime may be simultaneously set to UTIME_NOW, but any
// other combination is forbidden. For simplicity, always set both.
int64_t sec = millis / 1000;
int32_t nsec = (millis % 1000) * 1000000;
struct timespec times[2] = {
{sec, now ? UTIME_NOW : nsec},
{sec, now ? UTIME_NOW : nsec},
};
int err;
RESTARTABLE(utimensat(AT_FDCWD, path_chars, times, 0), err);
if (err == -1) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_mkdir(
JNIEnv* env, jobject instance, jstring path, jint mode) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return false;
}
jboolean result = true;
int err;
RESTARTABLE(mkdir(path_chars, mode), err);
if (err == -1) {
if (errno == EEXIST) {
result = false;
} else {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
}
return result;
}
namespace {
jobject NewDirent(JNIEnv* env, const struct dirent* e) {
static const jclass dirent_class = getClass(
env, "com/google/devtools/build/lib/unix/NativePosixFilesService$Dirent");
static const jmethodID dirent_ctor =
getConstructorID(env, dirent_class,
"(Ljava/lang/String;Lcom/google/devtools/build/lib/unix/"
"NativePosixFilesService$Dirent$Type;)V");
static const jclass type_class = getClass(
env,
"com/google/devtools/build/lib/unix/NativePosixFilesService$Dirent$Type");
static const char* field_sig =
"Lcom/google/devtools/build/lib/unix/"
"NativePosixFilesService$Dirent$Type;";
static const jobject type_file =
getStaticObjectField(env, type_class, "FILE", field_sig);
static const jobject type_directory =
getStaticObjectField(env, type_class, "DIRECTORY", field_sig);
static const jobject type_symlink =
getStaticObjectField(env, type_class, "SYMLINK", field_sig);
static const jobject type_char =
getStaticObjectField(env, type_class, "CHAR", field_sig);
static const jobject type_block =
getStaticObjectField(env, type_class, "BLOCK", field_sig);
static const jobject type_fifo =
getStaticObjectField(env, type_class, "FIFO", field_sig);
static const jobject type_socket =
getStaticObjectField(env, type_class, "SOCKET", field_sig);
static const jobject type_unknown =
getStaticObjectField(env, type_class, "UNKNOWN", field_sig);
jstring name = NewStringLatin1(env, e->d_name);
if (name == nullptr && env->ExceptionOccurred()) {
return nullptr;
}
jobject type;
switch (e->d_type) {
case DT_REG:
type = type_file;
break;
case DT_DIR:
type = type_directory;
break;
case DT_LNK:
type = type_symlink;
break;
case DT_CHR:
type = type_char;
break;
case DT_BLK:
type = type_block;
break;
case DT_FIFO:
type = type_fifo;
break;
case DT_SOCK:
type = type_socket;
break;
default:
type = type_unknown;
break;
}
return env->NewObject(dirent_class, dirent_ctor, name, type);
}
} // namespace
extern "C" JNIEXPORT jobject JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_readdir(
JNIEnv* env, jobject instance, jstring path) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return nullptr;
}
DIR *dirh;
RESTARTABLE_PTR(opendir(path_chars), dirh);
if (dirh == nullptr) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
if (dirh == nullptr) {
return nullptr;
}
std::vector<jobject> dirents;
for (;;) {
// Clear errno beforehand. Because readdir() is not required to clear it at
// EOF, this is the only way to reliably distinguish EOF from error.
errno = 0;
struct dirent* entry;
RESTARTABLE_PTR(readdir(dirh), entry);
if (entry == nullptr) {
if (errno == 0) break; // EOF
// It is unclear whether an error can also skip some records.
// That does not appear to happen with glibc, at least.
if (errno == EIO) continue; // glibc returns this on transient errors
// Otherwise, this is a real error we should report.
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
closedir(dirh);
return nullptr;
}
// Omit . and .. from results.
if (entry->d_name[0] == '.') {
if (entry->d_name[1] == '\0') continue;
if (entry->d_name[1] == '.' && entry->d_name[2] == '\0') continue;
}
jobject dirent = NewDirent(env, entry);
if (dirent == nullptr && env->ExceptionOccurred()) {
return nullptr;
}
dirents.push_back(dirent);
}
// Do not reattempt closedir() on EINTR to avoid a double-close.
if (closedir(dirh) < 0 && errno != EINTR) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
return nullptr;
}
static const jclass dirent_class = getClass(
env, "com/google/devtools/build/lib/unix/NativePosixFilesService$Dirent");
jobjectArray dirent_array =
env->NewObjectArray(dirents.size(), dirent_class, nullptr);
if (dirent_array == nullptr && env->ExceptionOccurred()) {
return nullptr;
}
for (size_t i = 0; i < dirents.size(); i++) {
env->SetObjectArrayElement(dirent_array, i, dirents[i]);
}
return dirent_array;
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_rename(
JNIEnv* env, jobject instance, jstring oldpath, jstring newpath) {
JStringLatin1Holder oldpath_chars(env, oldpath);
JStringLatin1Holder newpath_chars(env, newpath);
if (env->ExceptionOccurred()) {
return;
}
int err;
RESTARTABLE(rename(oldpath_chars, newpath_chars), err);
if (err == -1) {
std::string message(std::string(oldpath_chars) + " -> " +
std::string(newpath_chars));
POST_EXCEPTION_FROM_ERRNO(env, errno, message);
}
}
extern "C" JNIEXPORT bool JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_remove(
JNIEnv* env, jobject instance, jstring path) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return false;
}
int err;
RESTARTABLE(remove(path_chars), err);
if (err == -1) {
if (errno != ENOENT && errno != ENOTDIR) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
}
return err == 0;
}
/*
* Class: com.google.devtools.build.lib.unix.NativePosixFiles
* Method: mkfifo
* Signature: (Ljava/lang/String;I)V
* Throws: java.io.IOException
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_mkfifo(
JNIEnv* env, jobject instance, jstring path, jint mode) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return;
}
int err;
RESTARTABLE(mkfifo(path_chars, mode), err);
if (err == -1) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
}
namespace {
// Posts an exception generated by the DeleteTreesBelow algorithm and its helper
// functions.
//
// This is just a convenience wrapper over PostException to format the
// path that caused an error only when necessary, as we keep that path tokenized
// throughout the deletion process.
//
// env is the JNI environment in which to post the exception. error and function
// capture the errno value and the name of the system function that triggered
// it. The faulty path is specified by all the components of dir_path and the
// optional entry subcomponent, which may be NULL.
static void PostDeleteTreesBelowException(
JNIEnv *env, int error, const char *function,
const std::vector<std::string> &dir_path, const char *entry,
const char *filename_and_line_prefix) {
std::vector<std::string>::const_iterator iter = dir_path.begin();
std::string path;
if (iter != dir_path.end()) {
path = *iter;
while (++iter != dir_path.end()) {
path += "/";
path += *iter;
}
if (entry != nullptr) {
path += "/";
path += entry;
}
} else {
// When scanning the top-level directory given to DeleteTreesBelow, the
// dir_path buffer is still empty but we have the full path in entry.
path = entry;
}
BAZEL_CHECK(!env->ExceptionOccurred());
PostException(
env, error,
std::string(filename_and_line_prefix) + function + " (" + path + ")");
}
#define POST_DELETE_TREES_BELOW_EXCEPTION(env, error, function, dir_path, \
entry) \
PostDeleteTreesBelowException(env, error, function, dir_path, entry, \
"[" FILE_BASENAME ":" TOSTRING(__LINE__) "] ")
// Tries to open a directory and, if the first attempt fails, retries after
// granting extra permissions to the directory.
//
// The directory to open is identified by the open descriptor of the parent
// directory (dir_fd) and the subpath to resolve within that directory (entry).
// dir_path contains the path components that were used when opening dir_fd and
// is only used for error reporting purposes.
//
// Returns a directory handle on success or an errno on error. If the error is
// other than ENOENT, posts an exception before returning.
static DIROrError ForceOpendir(JNIEnv *env,
const std::vector<std::string> &dir_path,
const int dir_fd, const char *entry) {
static constexpr int flags = O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC;
int fd;
RESTARTABLE(openat(dir_fd, entry, flags), fd);
if (fd == -1) {
if (errno == ENOENT) {
return {nullptr, errno};
}
// If dir_fd is a readable but non-executable directory containing entry, we
// could have obtained entry by readdir()-ing, but any attempt to open or
// stat the entry would fail with EACCESS. In this case, we need to fix the
// permissions on dir_fd (which we can do only if it's a "real" file
// descriptor, not AT_FDCWD used as the starting point of DeleteTreesBelow
// recursion).
if (errno == EACCES && dir_fd != AT_FDCWD) {
int err;
RESTARTABLE(fchmod(dir_fd, 0700), err);
if (err == -1) {
if (errno != ENOENT) {
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "fchmod", dir_path,
nullptr);
}
return {nullptr, errno};
}
}
int err;
RESTARTABLE(fchmodat(dir_fd, entry, 0700, 0), err);
if (err == -1) {
if (errno != ENOENT) {
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "fchmodat", dir_path,
entry);
}
return {nullptr, errno};
}
RESTARTABLE(openat(dir_fd, entry, flags), fd);
if (fd == -1) {
if (errno != ENOENT) {
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "opendir", dir_path,
entry);
}
return {nullptr, errno};
}
}
DIR* dir;
RESTARTABLE_PTR(fdopendir(fd), dir);
if (dir == nullptr) {
if (errno != ENOENT) {
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "fdopendir", dir_path,
entry);
}
// Do not reattempt close() on EINTR to avoid a double-close.
close(fd);
return {nullptr, errno != EINTR ? errno : 0};
}
return {dir, 0};
}
// Tries to delete a file within a directory and, if the first attempt fails,
// retries after granting extra write permissions to the directory.
//
// The file to delete is identified by the open descriptor of the parent
// directory (dir_fd) and the subpath to resolve within that directory (entry).
// dir_path contains the path components that were used when opening dir_fd and
// is only used for error reporting purposes.
//
// is_dir indicates whether the entry to delete is a directory or not.
//
// Returns 0 when the file doesn't exist or is successfully deleted. Otherwise,
// returns -1 and posts an exception.
static int ForceDelete(JNIEnv* env, const std::vector<std::string>& dir_path,
const int dir_fd, const char* entry,
const bool is_dir) {
const int flags = is_dir ? AT_REMOVEDIR : 0;
int err;
RESTARTABLE(unlinkat(dir_fd, entry, flags), err);
if (err == -1) {
if (errno == ENOENT) {
return 0;
}
RESTARTABLE(fchmod(dir_fd, 0700), err);
if (err == -1) {
if (errno == ENOENT) {
return 0;
}
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "fchmod", dir_path,
nullptr);
return -1;
}
RESTARTABLE(unlinkat(dir_fd, entry, flags), err);
if (err == -1) {
if (errno == ENOENT) {
return 0;
}
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "unlinkat", dir_path,
entry);
return -1;
}
}
return 0;
}
// Returns true if the given directory entry represents a subdirectory of dir.
//
// The file to check is identified by the open descriptor of the parent
// directory (dir_fd) and the directory entry within that directory (de).
// dir_path contains the path components that were used when opening dir_fd and
// is only used for error reporting purposes.
//
// This function prefers to extract the type information from the directory
// entry itself if available. If not available, issues a stat starting from
// dir_fd.
//
// Returns 0 on success and updates is_dir accordingly. Returns -1 on error and
// posts an exception.
static int IsSubdir(JNIEnv* env, const std::vector<std::string>& dir_path,
const int dir_fd, const struct dirent* de, bool* is_dir) {
switch (de->d_type) {
case DT_DIR:
*is_dir = true;
return 0;
case DT_UNKNOWN: {
struct stat st;
int err;
RESTARTABLE(fstatat(dir_fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW), err);
if (err == -1) {
if (errno == ENOENT) {
*is_dir = false;
return 0;
}
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "fstatat", dir_path,
de->d_name);
return -1;
}
*is_dir = st.st_mode & S_IFDIR;
return 0;
}
default:
*is_dir = false;
return 0;
}
}
// Recursively deletes all trees under the given path.
//
// The directory to delete is identified by the open descriptor of the parent
// directory (dir_fd) and the subpath to resolve within that directory (entry).
// dir_path contains the path components that were used when opening dir_fd and
// is only used for error reporting purposes.
//
// dir_path is an in/out parameter updated with the path to the directory being
// processed. This avoids the need to construct unnecessary intermediate paths,
// as this algorithm works purely on file descriptors: the paths are only used
// for error reporting purposes, and therefore are only formatted at that
// point.
//
// Returns 0 on success. Returns -1 on error and posts an exception.
static int DeleteTreesBelow(JNIEnv* env, std::vector<std::string>* dir_path,
const int dir_fd, const char* entry) {
DIROrError dir_or_error = ForceOpendir(env, *dir_path, dir_fd, entry);
DIR *dir = dir_or_error.dir;
if (dir == nullptr) {
if (dir_or_error.error == ENOENT) {
return 0;
}
BAZEL_CHECK_NE(env->ExceptionOccurred(), nullptr);
return -1;
}
dir_path->push_back(entry);
// On macOS and some other non-Linux OSes, on some filesystems, readdir(dir)
// may return NULL after an entry in dir is deleted even if not all files have
// been read yet - see
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html;
// "If a file is removed from or added to the directory after the most recent
// call to opendir() or rewinddir(), whether a subsequent call to readdir()
// returns an entry for that file is unspecified." We thus read all the names
// of dir's entries before deleting. We don't want to simply use fts(3)
// because we want to be able to chmod at any point in the directory hierarchy
// to retry a filesystem operation after hitting an EACCES.
// If in the future we hit any problems here due to the unspecified behavior
// of readdir() when a file has been deleted by a different thread we can use
// some form of locking to make sure the threads don't try to clean up the
// same directory at the same time; or doing it in a loop until the directory
// is really empty.
std::vector<std::string> dir_files, dir_subdirs;
for (;;) {
errno = 0;
struct dirent* entry;
RESTARTABLE_PTR(readdir(dir), entry);
if (entry == nullptr) {
if (errno != 0 && errno != ENOENT) {
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "readdir", *dir_path,
nullptr);
}
break;
}
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
bool is_dir;
if (IsSubdir(env, *dir_path, dirfd(dir), entry, &is_dir) == -1) {
BAZEL_CHECK_NE(env->ExceptionOccurred(), nullptr);
break;
}
if (is_dir) {
dir_subdirs.push_back(entry->d_name);
} else {
dir_files.push_back(entry->d_name);
}
}
if (env->ExceptionOccurred() == nullptr) {
for (const auto &file : dir_files) {
if (ForceDelete(env, *dir_path, dirfd(dir), file.c_str(), false) == -1) {
BAZEL_CHECK_NE(env->ExceptionOccurred(), nullptr);
break;
}
}
// DeleteTreesBelow is recursive; don't hold on to file names unnecessarily.
dir_files.clear();
}
if (env->ExceptionOccurred() == nullptr) {
for (const auto &subdir : dir_subdirs) {
if (DeleteTreesBelow(env, dir_path, dirfd(dir), subdir.c_str()) == -1) {
BAZEL_CHECK_NE(env->ExceptionOccurred(), nullptr);
break;
}
if (ForceDelete(env, *dir_path, dirfd(dir), subdir.c_str(), true) == -1) {
BAZEL_CHECK_NE(env->ExceptionOccurred(), nullptr);
break;
}
}
}
// Do not reattempt closedir() on EINTR to avoid a double-close.
if (closedir(dir) == -1 && errno != EINTR) {
// Prefer reporting the error encountered while processing entries,
// not the (unlikely) error on close.
if (env->ExceptionOccurred() == nullptr) {
POST_DELETE_TREES_BELOW_EXCEPTION(env, errno, "closedir", *dir_path,
nullptr);
}
}
dir_path->pop_back();
return env->ExceptionOccurred() == nullptr ? 0 : -1;
}
} // namespace
/*
* Class: com.google.devtools.build.lib.unix.NativePosixFiles
* Method: deleteTreesBelow
* Signature: (Ljava/lang/String;)V
* Throws: java.io.IOException
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_deleteTreesBelow(
JNIEnv* env, jobject instance, jstring path) {
JStringLatin1Holder path_chars(env, path);
if (env->ExceptionOccurred()) {
return;
}
std::vector<std::string> dir_path;
if (DeleteTreesBelow(env, &dir_path, AT_FDCWD, path_chars) == -1) {
BAZEL_CHECK_NE(env->ExceptionOccurred(), nullptr);
}
BAZEL_CHECK(dir_path.empty());
}
////////////////////////////////////////////////////////////////////////
// Linux extended file attributes
namespace {
typedef ssize_t getxattr_func(const char *path, const char *name,
void *value, size_t size, bool *attr_not_found);
static jbyteArray getxattr_common(JNIEnv *env,
jstring path,
jstring name,
getxattr_func getxattr) {
JStringLatin1Holder path_chars(env, path);
JStringLatin1Holder name_chars(env, name);
if (env->ExceptionOccurred()) {
return nullptr;
}
// TODO(bazel-team): on ERANGE, try again with larger buffer.
jbyte value[4096];
jbyteArray result = nullptr;
bool attr_not_found = false;
ssize_t size;
RESTARTABLE(getxattr(path_chars, name_chars, value, std::size(value),
&attr_not_found),
size);
if (size == -1) {
if (!attr_not_found) {
POST_EXCEPTION_FROM_ERRNO(env, errno, path_chars);
}
} else {
result = env->NewByteArray(size);
// Result may be NULL if allocation failed. In that case, we'll return the
// NULL and an OOME will be thrown when we are back in Java.
if (result != nullptr) {
env->SetByteArrayRegion(result, 0, size, value);
}
}
return result;
}
} // namespace
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_getxattr(
JNIEnv* env, jobject instance, jstring path, jstring name) {
return getxattr_common(env, path, name, portable_getxattr);
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_google_devtools_build_lib_unix_NativePosixFilesServiceImpl_lgetxattr(
JNIEnv* env, jobject instance, jstring path, jstring name) {
return getxattr_common(env, path, name, portable_lgetxattr);
}
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: pushDisableSleepNative
* Signature: ()I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_pushDisableSleepNative(
JNIEnv*, jclass) {
return portable_push_disable_sleep();
}
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: popDisableSleepNative
* Signature: ()I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_popDisableSleepNative(
JNIEnv*, jclass) {
return portable_pop_disable_sleep();
}
jobject g_suspend_callback;
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: registerSuspensionNative
* Signature: (Ljava/util/function/IntConsumer;)V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_registerSuspensionNative(
JNIEnv* env, jobject local_object, jobject callback) {
if (g_suspend_callback != nullptr) {
POST_ASSERTION_ERROR(
env, "Singleton system suspension callback already registered");
return;
}
JavaVM *java_vm = GetJavaVM(env);
if (java_vm == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to get javaVM registering system suspension callback");
return;
}
g_suspend_callback = env->NewGlobalRef(callback);
if (g_suspend_callback == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to create global ref for system suspension callback");
return;
}
portable_start_suspend_monitoring();
}
void suspend_callback(SuspensionReason value) {
if (g_suspend_callback != nullptr) {
PerformIntegerValueCallback(g_suspend_callback, value);
}
}
jobject g_thermal_callback;
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: registerThermalNative
* Signature: (Ljava/util/function/IntConsumer;)V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_registerThermalNative(
JNIEnv* env, jobject local_object, jobject callback) {
if (g_thermal_callback != nullptr) {
POST_ASSERTION_ERROR(env, "Singleton thermal callback already registered");
return;
}
JavaVM *java_vm = GetJavaVM(env);
if (java_vm == nullptr) {
POST_ASSERTION_ERROR(env,
"Unable to get javaVM registering thermal callback");
return;
}
g_thermal_callback = env->NewGlobalRef(callback);
if (g_thermal_callback == nullptr) {
POST_ASSERTION_ERROR(env,
"Unable to create global ref for thermal callback");
return;
}
portable_start_thermal_monitoring();
}
void thermal_callback(int value) {
if (g_thermal_callback != nullptr) {
PerformIntegerValueCallback(g_thermal_callback, value);
}
}
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: thermalLoadNative
* Signature: ()I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_thermalLoadNative(
JNIEnv* env, jclass) {
return portable_thermal_load();
}
jobject g_system_load_advisory_callback;
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: registerLoadAdvisoryNative
* Signature: (Ljava/util/function/IntConsumer;)V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_registerLoadAdvisoryNative(
JNIEnv* env, jobject local_object, jobject callback) {
if (g_system_load_advisory_callback != nullptr) {
POST_ASSERTION_ERROR(
env, "Singleton system load advisory callback already registered");
return;
}
JavaVM *java_vm = GetJavaVM(env);
if (java_vm == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to get javaVM registering system load advisory callback");
return;
}
g_system_load_advisory_callback = env->NewGlobalRef(callback);
if (g_system_load_advisory_callback == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to create global ref for system load advisory callback");
return;
}
portable_start_system_load_advisory_monitoring();
}
void system_load_advisory_callback(int value) {
if (g_system_load_advisory_callback != nullptr) {
PerformIntegerValueCallback(g_system_load_advisory_callback, value);
}
}
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: systemLoadAdvisoryNative
* Signature: ()I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_systemLoadAdvisoryNative(
JNIEnv* env, jclass) {
return portable_system_load_advisory();
}
jobject g_memory_pressure_callback;
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: registerMemoryPressureNative
* Signature: (Ljava/util/function/IntConsumer;)V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_registerMemoryPressureNative(
JNIEnv* env, jobject local_object, jobject callback) {
if (g_memory_pressure_callback != nullptr) {
POST_ASSERTION_ERROR(
env, "Singleton memory pressure callback already registered");
return;
}
JavaVM *java_vm = GetJavaVM(env);
if (java_vm == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to get javaVM registering memory pressure callback");
return;
}
g_memory_pressure_callback = env->NewGlobalRef(callback);
if (g_memory_pressure_callback == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to create global ref for memory pressure callback");
return;
}
portable_start_memory_pressure_monitoring();
}
void memory_pressure_callback(MemoryPressureLevel level) {
if (g_memory_pressure_callback != nullptr) {
PerformIntegerValueCallback(g_memory_pressure_callback, level);
}
}
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: systemMemoryPressureNative
* Signature: ()I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_systemMemoryPressureNative(
JNIEnv* env, jclass) {
return portable_memory_pressure();
}
jobject g_disk_space_callback;
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: registerDiskSpaceNative
* Signature: (Ljava/util/function/IntConsumer;)V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_registerDiskSpaceNative(
JNIEnv* env, jobject local_object, jobject callback) {
if (g_disk_space_callback != nullptr) {
POST_ASSERTION_ERROR(env,
"Singleton disk space callback already registered");
return;
}
JavaVM *java_vm = GetJavaVM(env);
if (java_vm == nullptr) {
POST_ASSERTION_ERROR(
env, "Unable to get javaVM registering disk space callback");
return;
}
g_disk_space_callback = env->NewGlobalRef(callback);
if (g_disk_space_callback == nullptr) {
POST_ASSERTION_ERROR(env,
"Unable to create global ref for disk space callback");
return;
}
portable_start_disk_space_monitoring();
}
void disk_space_callback(DiskSpaceLevel level) {
if (g_disk_space_callback != nullptr) {
PerformIntegerValueCallback(g_disk_space_callback, level);
}
}
jobject g_cpu_speed_callback;
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: registerCPUSpeedNative
* Signature: (Ljava/util/function/IntConsumer;)V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_registerCPUSpeedNative(
JNIEnv* env, jobject local_object, jobject callback) {
if (g_cpu_speed_callback != nullptr) {
POST_ASSERTION_ERROR(env,
"Singleton cpu speed callback already registered");
return;
}
JavaVM *java_vm = GetJavaVM(env);
if (java_vm == nullptr) {
POST_ASSERTION_ERROR(env,
"Unable to get javaVM registering cpu speed callback");
return;
}
g_cpu_speed_callback = env->NewGlobalRef(callback);
if (g_cpu_speed_callback == nullptr) {
POST_ASSERTION_ERROR(env,
"Unable to create global ref for cpu speed callback");
return;
}
portable_start_cpu_speed_monitoring();
}
void cpu_speed_callback(int speed) {
if (g_cpu_speed_callback != nullptr) {
PerformIntegerValueCallback(g_cpu_speed_callback, speed);
}
}
/*
* Class: com.google.devtools.build.lib.platform.PlatformNativeDepsServiceImpl
* Method: cpuSpeedNative
* Signature: ()I
*
* Returns 1-100 to represent CPU speed. Returns -1 in case of error.
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_platform_PlatformNativeDepsServiceImpl_cpuSpeedNative(
JNIEnv* env, jclass) {
return portable_cpu_speed();
}
} // namespace blaze_jni