Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ctime in file digest cache key #18115

Merged
merged 1 commit into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,8 @@ private static FileArtifactValue fileArtifactValueFromStat(
}

private static void setPathReadOnlyAndExecutableIfFile(Path path) throws IOException {
if (path.isFile(Symlinks.NOFOLLOW)) {
FileStatus stat = path.statIfFound(Symlinks.NOFOLLOW);
if (stat != null && stat.isFile() && stat.getPermissions() != 0555) {
setPathReadOnlyAndExecutable(path);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ public long getNodeId() {
return status.getInodeNumber();
}

int getPermissions() { return status.getPermissions(); }
@Override
public int getPermissions() {
return status.getPermissions();
}

@Override
public String toString() { return status.toString(); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ private static class CacheKey {
/** File system identifier of the file (typically the inode number). */
private final long nodeId;

/** Last change time of the file. */
private final long changeTime;

/** Last modification time of the file. */
private final long modifiedTime;

Expand All @@ -70,6 +73,7 @@ private static class CacheKey {
public CacheKey(Path path, FileStatus status) throws IOException {
this.path = path.asFragment();
this.nodeId = status.getNodeId();
this.changeTime = status.getLastChangeTime();
this.modifiedTime = status.getLastModifiedTime();
this.size = status.getSize();
}
Expand All @@ -84,6 +88,7 @@ public boolean equals(Object object) {
CacheKey key = (CacheKey) object;
return path.equals(key.path)
&& nodeId == key.nodeId
&& changeTime == key.changeTime
&& modifiedTime == key.modifiedTime
&& size == key.size;
}
Expand All @@ -94,6 +99,7 @@ public int hashCode() {
int result = 17;
result = 31 * result + path.hashCode();
result = 31 * result + Longs.hashCode(nodeId);
result = 31 * result + Longs.hashCode(changeTime);
result = 31 * result + Longs.hashCode(modifiedTime);
result = 31 * result + Longs.hashCode(size);
return result;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,16 @@ public interface FileStatus {
* ought to cause the node ID of b to change, but appending / modifying b should not.
*/
long getNodeId() throws IOException;

/**
* Returns the file's permissions in POSIX format (e.g. 0755) if possible without performing
* additional IO, otherwise (or if unsupported by the file system) returns -1.
*
* <p>If accurate group and other permissions aren't available, the returned value should attempt
* to mimic a umask of 022 (i.e. read and execute permissions extend to group and other, write
* does not).
*/
default int getPermissions() {
return -1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ public long getNodeId() {
return System.identityHashCode(this);
}

@Override
public final int getPermissions() {
int permissions = 0;
// Emulate the default umask of 022.
if (isReadable) {
permissions |= 0444;
}
if (isWritable) {
permissions |= 0200;
}
if (isExecutable) {
permissions |= 0111;
}
return permissions;
}

@Override
public final InMemoryContentInfo inode() {
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package com.google.devtools.build.lib.windows;

import com.google.devtools.build.lib.jni.JniLoader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.AccessDeniedException;

/** File operations on Windows. */
public class WindowsFileOperations {
Expand Down Expand Up @@ -82,6 +84,12 @@ public Status getStatus() {
// IS_SYMLINK_OR_JUNCTION_ERROR = 1;
private static final int IS_SYMLINK_OR_JUNCTION_DOES_NOT_EXIST = 2;

// Keep GET_CHANGE_TIME_* values in sync with src/main/native/windows/file.cc.
private static final int GET_CHANGE_TIME_SUCCESS = 0;
// private static final int GET_CHANGE_TIME_ERROR = 1;
private static final int GET_CHANGE_TIME_DOES_NOT_EXIST = 2;
private static final int GET_CHANGE_TIME_ACCESS_DENIED = 3;

// Keep CREATE_JUNCTION_* values in sync with src/main/native/windows/file.h.
private static final int CREATE_JUNCTION_SUCCESS = 0;
// CREATE_JUNCTION_ERROR = 1;
Expand Down Expand Up @@ -114,6 +122,9 @@ public Status getStatus() {
private static native int nativeIsSymlinkOrJunction(
String path, boolean[] result, String[] error);

private static native int nativeGetChangeTime(
String path, boolean followReparsePoints, long[] result, String[] error);

private static native boolean nativeGetLongPath(String path, String[] result, String[] error);

private static native int nativeCreateJunction(String name, String target, String[] error);
Expand Down Expand Up @@ -143,6 +154,25 @@ public static boolean isSymlinkOrJunction(String path) throws IOException {
throw new IOException(String.format("Cannot tell if '%s' is link: %s", path, error[0]));
}

/** Returns the time at which the file was last changed, including metadata changes. */
public static long getLastChangeTime(String path, boolean followReparsePoints)
throws IOException {
long[] result = new long[] {0};
String[] error = new String[] {null};
switch (nativeGetChangeTime(asLongPath(path), followReparsePoints, result, error)) {
case GET_CHANGE_TIME_SUCCESS:
return result[0];
case GET_CHANGE_TIME_DOES_NOT_EXIST:
throw new FileNotFoundException(path);
case GET_CHANGE_TIME_ACCESS_DENIED:
throw new AccessDeniedException(path);
default:
// This is GET_CHANGE_TIME_ERROR (1). The JNI code puts a custom message in 'error[0]'.
break;
}
throw new IOException(String.format("Cannot get last change time of '%s': %s", path, error[0]));
}

/**
* Returns the long path associated with the input `path`.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ protected FileStatus stat(PathFragment path, boolean followSymlinks) throws IOEx
}

final boolean isSymbolicLink = !followSymlinks && fileIsSymbolicLink(file);
final long lastChangeTime =
WindowsFileOperations.getLastChangeTime(getNioPath(path).toString(), followSymlinks);
FileStatus status =
new FileStatus() {
@Override
Expand Down Expand Up @@ -193,15 +195,20 @@ public long getLastModifiedTime() throws IOException {

@Override
public long getLastChangeTime() {
// This is the best we can do with Java NIO...
return attributes.lastModifiedTime().toMillis();
return lastChangeTime;
}

@Override
public long getNodeId() {
// TODO(bazel-team): Consider making use of attributes.fileKey().
return -1;
}

@Override
public int getPermissions() {
// Files on Windows are implicitly readable and executable.
return 0555 | (attributes.isReadOnly() ? 0 : 0200);
}
};

return status;
Expand Down
23 changes: 23 additions & 0 deletions src/main/native/windows/file-jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,29 @@ Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeIsSymlink
return static_cast<jint>(result);
}

extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetChangeTime(
JNIEnv* env, jclass clazz, jstring path, jboolean follow_reparse_points,
jlongArray result_holder, jobjectArray error_msg_holder) {
std::wstring wpath(bazel::windows::GetJavaWstring(env, path));
std::wstring error;
jlong ctime = 0;
int result =
bazel::windows::GetChangeTime(wpath.c_str(), follow_reparse_points,
reinterpret_cast<int64_t*>(&ctime), &error);
if (result == bazel::windows::GetChangeTimeResult::kSuccess) {
env->SetLongArrayRegion(result_holder, 0, 1, &ctime);
} else {
if (!error.empty() && CanReportError(env, error_msg_holder)) {
ReportLastError(
bazel::windows::MakeErrorMessage(
WSTR(__FILE__), __LINE__, L"nativeGetChangeTime", wpath, error),
env, error_msg_holder);
}
}
return static_cast<jint>(result);
}

extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetLongPath(
JNIEnv* env, jclass clazz, jstring path, jobjectArray result_holder,
Expand Down
49 changes: 49 additions & 0 deletions src/main/native/windows/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <WinIoCtl.h>
#include <stdint.h> // uint8_t
#include <versionhelpers.h>
#include <winbase.h>
#include <windows.h>

#include <memory>
Expand Down Expand Up @@ -119,6 +120,54 @@ int IsSymlinkOrJunction(const WCHAR* path, bool* result, wstring* error) {
}
}

int GetChangeTime(const WCHAR* path, bool follow_reparse_points,
int64_t* result, wstring* error) {
if (!IsAbsoluteNormalizedWindowsPath(path)) {
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__, L"GetChangeTime",
path, L"expected an absolute Windows path");
}
return GetChangeTimeResult::kError;
}

AutoHandle handle;
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS;
if (!follow_reparse_points) {
flags |= FILE_FLAG_OPEN_REPARSE_POINT;
}
handle = CreateFileW(path, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, flags, nullptr);

if (!handle.IsValid()) {
DWORD err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
return GetChangeTimeResult::kDoesNotExist;
} else if (err == ERROR_ACCESS_DENIED) {
return GetChangeTimeResult::kAccessDenied;
}
if (error) {
*error =
MakeErrorMessage(WSTR(__FILE__), __LINE__, L"CreateFileW", path, err);
}
return GetChangeTimeResult::kError;
}

FILE_BASIC_INFO info;
if (!GetFileInformationByHandleEx(handle, FileBasicInfo, (LPVOID)&info,
sizeof(FILE_BASIC_INFO))) {
DWORD err = GetLastError();
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"GetFileInformationByHandleEx", path, err);
}
return GetChangeTimeResult::kError;
}

*result = info.ChangeTime.QuadPart;
return GetChangeTimeResult::kSuccess;
}

wstring GetLongPath(const WCHAR* path, unique_ptr<WCHAR[]>* result) {
if (!IsAbsoluteNormalizedWindowsPath(path)) {
return MakeErrorMessage(WSTR(__FILE__), __LINE__, L"GetLongPath", path,
Expand Down
17 changes: 17 additions & 0 deletions src/main/native/windows/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ struct IsSymlinkOrJunctionResult {
};
};

// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
struct GetChangeTimeResult {
enum {
kSuccess = 0,
kError = 1,
kDoesNotExist = 2,
kAccessDenied = 3,
};
};

// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
struct DeletePathResult {
enum {
Expand Down Expand Up @@ -136,6 +146,13 @@ struct ReadSymlinkOrJunctionResult {
// see http://superuser.com/a/343079. In Bazel we only ever create junctions.
int IsSymlinkOrJunction(const WCHAR* path, bool* result, wstring* error);

// Retrieves the FILETIME at which `path` was last changed, including metadata.
//
// `path` should be an absolute, normalized, Windows-style path, with "\\?\"
// prefix if it's longer than MAX_PATH.
int GetChangeTime(const WCHAR* path, bool follow_reparse_points,
int64_t* result, wstring* error);

// Computes the long version of `path` if it has any 8dot3 style components.
// Returns the empty string upon success, or a human-readable error message upon
// failure.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,10 @@ public void resettingOutputs() throws Exception {

// Reset this output, which will make the handler stat the file again.
handler.resetOutputs(ImmutableList.of(artifact));
chmodCalls.clear(); // Permit a second chmod call for the artifact.
chmodCalls.clear();
assertThat(handler.getMetadata(artifact).getSize()).isEqualTo(10);
assertThat(chmodCalls).containsExactly(outputPath);
// The handler should not have chmodded the file as it already has the correct permission.
assertThat(chmodCalls).isEmpty();
}

@Test
Expand Down
Loading