Skip to content

Commit

Permalink
Windows,JNI: implement native DeletePath method
Browse files Browse the repository at this point in the history
Implement a native version of the Java
Path.delete().

The new JNI function is more robust than Java IO
file deletion function because it can also delete
readonly files.

The new JNI function can tolerate some concurrent
modification errors, i.e. when other processes
move or remove the file we are trying to delete.
We'll use this method in WindowsFileSystem.delete
to reduce the likelihood of such concurrent
modifications crashing Bazel.

See bazelbuild#5513

Change-Id: I8a03568171b6911f688587b65ca14031ed86b54b
RELNOTES: none
  • Loading branch information
laszlocsomor committed Jul 5, 2018
1 parent 55d0c1c commit 96c1599
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/windows/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib:os_util",
"//src/main/java/com/google/devtools/build/lib/clock",
"//src/main/java/com/google/devtools/build/lib/concurrent",
"//src/main/java/com/google/devtools/build/lib/profiler",
"//src/main/java/com/google/devtools/build/lib/shell",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/windows/jni",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
Expand Down Expand Up @@ -48,6 +50,21 @@ public String getFileSystemType(Path path) {
return "ntfs";
}

@Override
public boolean delete(Path path) throws IOException {
System.err.println("DEBUG[CICA] WindowsFileSystem.delete(" + path + ")");
long startTime = Profiler.nanoTimeMaybe();
try {
return WindowsFileOperations.deletePath(path.getPathString());
} catch (java.nio.file.DirectoryNotEmptyException e) {
throw new IOException(path.getPathString() + ERR_DIRECTORY_NOT_EMPTY);
} catch (java.nio.file.AccessDeniedException e) {
throw new IOException(path.getPathString() + ERR_PERMISSION_DENIED);
} finally {
profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, path.getPathString());
}
}

@Override
protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
Path targetPath =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,21 @@ private WindowsFileOperations() {
private static final int IS_JUNCTION_NO = 1;
private static final int IS_JUNCTION_ERROR = 2;

// Keep DELETE_PATH_* values in sync with src/main/native/windows/file.cc.
private static final int DELETE_PATH_SUCCESS = 0;
private static final int DELETE_PATH_DOES_NOT_EXIST = 1;
private static final int DELETE_PATH_DIRECTORY_NOT_EMPTY = 2;
private static final int DELETE_PATH_ACCESS_DENIED = 3;
private static final int DELETE_PATH_ERROR = 4;

private static native int nativeIsJunction(String path, String[] error);

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

private static native boolean nativeCreateJunction(String name, String target, String[] error);

private static native int nativeDeletePath(String path, String[] error);

/** Determines whether `path` is a junction point or directory symlink. */
public static boolean isJunction(String path) throws IOException {
WindowsJniLoader.loadJni();
Expand Down Expand Up @@ -121,4 +130,22 @@ public static void createJunction(String name, String target) throws IOException
String.format("Cannot create junction (name=%s, target=%s): %s", name, target, error[0]));
}
}

public static boolean deletePath(String path) throws IOException {
WindowsJniLoader.loadJni();
String[] error = new String[] {null};
int result = nativeDeletePath(asLongPath(path), error);
switch (result) {
case DELETE_PATH_SUCCESS:
return true;
case DELETE_PATH_DOES_NOT_EXIST:
return false;
case DELETE_PATH_DIRECTORY_NOT_EMPTY:
throw new java.nio.file.DirectoryNotEmptyException(path);
case DELETE_PATH_ACCESS_DENIED:
throw new java.nio.file.AccessDeniedException(path);
default:
throw new IOException(String.format("Cannot delete path '%s': %s", path, error[0]));
}
}
}
16 changes: 16 additions & 0 deletions src/main/native/windows/file-jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,19 @@ Java_com_google_devtools_build_lib_windows_jni_WindowsFileOperations_nativeCreat
}
return JNI_TRUE;
}

extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_jni_WindowsFileOperations_nativeDeletePath(
JNIEnv* env, jclass clazz, jstring path, jobjectArray error_msg_holder) {
std::wstring wpath(bazel::windows::GetJavaWstring(env, path));
std::wstring error;
int result = bazel::windows::DeletePath(wpath, &error);
if (result != bazel::windows::DELETE_PATH_SUCCESS && !error.empty() &&
CanReportError(env, error_msg_holder)) {
ReportLastError(bazel::windows::MakeErrorMessage(
WSTR(__FILE__), __LINE__, L"nativeDeletePath", wpath,
error),
env, error_msg_holder);
}
return result;
}
93 changes: 93 additions & 0 deletions src/main/native/windows/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,98 @@ wstring CreateJunction(const wstring& junction_name,
return L"";
}

int DeletePath(const wstring& path, wstring* error) {
const wchar_t* wpath = path.c_str();
if (!DeleteFileW(wpath)) {
DWORD err = GetLastError();
if (err == ERROR_SHARING_VIOLATION) {
// The file or directory is in use by some process.
return DELETE_PATH_ACCESS_DENIED;
} else if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
// The file or directory does not exist, or a parent directory does not
// exist, or a parent directory or is not a directory.
return DELETE_PATH_DOES_NOT_EXIST;
}

if (err != ERROR_ACCESS_DENIED) {
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__, L"DeleteFileW",
wpath, err);
}
fprintf(stderr, "DEBUG: line: %d, err: %d\n", __LINE__, err);
return DELETE_PATH_ERROR;
}

// Access denied because the file is read-only or it is a directory.
DWORD attr = GetFileAttributesW(wpath);
if (attr == INVALID_FILE_ATTRIBUTES) {
err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
// The file disappeared since the last API call (DeleteFileW).
return DELETE_PATH_DOES_NOT_EXIST;
} else {
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"GetFileAttributesW", wpath, err);
}
return DELETE_PATH_ERROR;
}
}

if (attr & FILE_ATTRIBUTE_DIRECTORY) {
// It's a directory or a junction.
if (RemoveDirectoryW(wpath)) {
return DELETE_PATH_SUCCESS;
} else {
err = GetLastError();
if (err == ERROR_SHARING_VIOLATION) {
return DELETE_PATH_ACCESS_DENIED;
} else if (err == ERROR_DIR_NOT_EMPTY) {
return DELETE_PATH_DIRECTORY_NOT_EMPTY;
} else if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
// The file disappeared since the last API call (GetFileAttributesW).
return DELETE_PATH_DOES_NOT_EXIST;
} else {
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"DeleteDirectoryW", wpath, err);
}
return DELETE_PATH_ERROR;
}
}
} else {
// It's a file and it's probably read-only.
// Make it writable then try deleting it again.
attr &= ~FILE_ATTRIBUTE_READONLY;
if (!SetFileAttributesW(wpath, attr)) {
err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
// The file disappeared since the last API call (GetFileAttributesW).
return DELETE_PATH_DOES_NOT_EXIST;
}
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"SetFileAttributesW", wpath, err);
}
return DELETE_PATH_ERROR;
}
if (!DeleteFileW(wpath)) {
err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
// The file disappeared since the last API call (SetFileAttributesW).
return DELETE_PATH_DOES_NOT_EXIST;
}
if (error) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__, L"DeleteFileW",
wpath, err);
}
return DELETE_PATH_ERROR;
}
}
}

return DELETE_PATH_SUCCESS;
}

} // namespace windows
} // namespace bazel
16 changes: 16 additions & 0 deletions src/main/native/windows/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ enum {
IS_JUNCTION_ERROR = 2,
};

// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
enum {
DELETE_PATH_SUCCESS = 0,
DELETE_PATH_DOES_NOT_EXIST = 1,
DELETE_PATH_DIRECTORY_NOT_EMPTY = 2,
DELETE_PATH_ACCESS_DENIED = 3,
DELETE_PATH_ERROR = 4,
};

// Determines whether `path` is a junction (or directory symlink).
//
// `path` should be an absolute, normalized, Windows-style path, with "\\?\"
Expand Down Expand Up @@ -85,6 +94,13 @@ HANDLE OpenDirectory(const WCHAR* path, bool read_write);
wstring CreateJunction(const wstring& junction_name,
const wstring& junction_target);

// Deletes the file or directory at `path`.
// Returns DELETE_PATH_SUCCESS if it successfully deleted the path, otherwise
// returns one of the other DELETE_PATH_* constants.
// Returns DELETE_PATH_ERROR for unexpected errors. If `error` is not null, the
// function writes an error message into it.
int DeletePath(const wstring& path, wstring* error);

} // namespace windows
} // namespace bazel

Expand Down
121 changes: 121 additions & 0 deletions src/test/native/windows/file_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
namespace bazel {
namespace windows {

#define TOSTRING(x) #x
#define TOWSTRING1(x) L##x
#define TOWSTRING(x) TOWSTRING1(x)
#define WLINE TOWSTRING(TOSTRING(__LINE__))

using blaze_util::DeleteAllUnder;
using blaze_util::GetTestTmpDirW;
using std::unique_ptr;
Expand Down Expand Up @@ -108,5 +113,121 @@ TEST_F(WindowsFileOperationsTest, TestCreateJunction) {
::GetFileAttributesW((name + L"4\\bar").c_str()));
}

TEST_F(WindowsFileOperationsTest, TestCanDeleteExistingFile) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring path = tmp + L"\\file" WLINE;
EXPECT_TRUE(blaze_util::CreateDummyFile(path));
ASSERT_EQ(DeletePath(path.c_str(), nullptr), DELETE_PATH_SUCCESS);
}

TEST_F(WindowsFileOperationsTest, TestCanDeleteExistingDirectory) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring path = tmp + L"\\dir" WLINE;
EXPECT_TRUE(CreateDirectoryW(path.c_str(), NULL));
ASSERT_EQ(DeletePath(path.c_str(), nullptr), DELETE_PATH_SUCCESS);
}

TEST_F(WindowsFileOperationsTest, TestCanDeleteExistingJunction) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring name = tmp + L"\\junc" WLINE;
wstring target = tmp + L"\\target" WLINE;
EXPECT_TRUE(CreateDirectoryW(target.c_str(), NULL));
EXPECT_EQ(L"", CreateJunction(name, target));
ASSERT_EQ(DeletePath(name.c_str(), nullptr), DELETE_PATH_SUCCESS);
}

TEST_F(WindowsFileOperationsTest, TestCanDeleteExistingJunctionWithoutTarget) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring name = tmp + L"\\junc" WLINE;
wstring target = tmp + L"\\target" WLINE;
EXPECT_TRUE(CreateDirectoryW(target.c_str(), NULL));
EXPECT_EQ(L"", CreateJunction(name, target));
EXPECT_TRUE(RemoveDirectoryW(target.c_str()));
// The junction still exists, its target does not.
EXPECT_NE(GetFileAttributesW(name.c_str()), INVALID_FILE_ATTRIBUTES);
EXPECT_EQ(GetFileAttributesW(target.c_str()), INVALID_FILE_ATTRIBUTES);
// We can delete the dangling junction.
ASSERT_EQ(DeletePath(name.c_str(), nullptr), DELETE_PATH_SUCCESS);
}

TEST_F(WindowsFileOperationsTest, TestCannotDeleteNonExistentPath) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring path = tmp + L"\\dummy" WLINE;
EXPECT_EQ(GetFileAttributesW(path.c_str()), INVALID_FILE_ATTRIBUTES);
ASSERT_EQ(DeletePath(path.c_str(), nullptr), DELETE_PATH_DOES_NOT_EXIST);
}

TEST_F(WindowsFileOperationsTest, TestCannotDeletePathWhereParentIsFile) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring parent = tmp + L"\\file" WLINE;
wstring child = parent + L"\\file" WLINE;
EXPECT_TRUE(blaze_util::CreateDummyFile(parent));
ASSERT_EQ(DeletePath(child.c_str(), nullptr), DELETE_PATH_DOES_NOT_EXIST);
}

TEST_F(WindowsFileOperationsTest, TestCannotDeleteNonEmptyDirectory) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring parent = tmp + L"\\dir" WLINE;
wstring child = parent + L"\\file" WLINE;
EXPECT_TRUE(CreateDirectoryW(parent.c_str(), NULL));
EXPECT_TRUE(blaze_util::CreateDummyFile(child));
ASSERT_EQ(DeletePath(parent.c_str(), nullptr),
DELETE_PATH_DIRECTORY_NOT_EMPTY);
}

TEST_F(WindowsFileOperationsTest, TestCannotDeleteBusyFile) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring path = tmp + L"\\file" WLINE;
EXPECT_TRUE(blaze_util::CreateDummyFile(path));
HANDLE h = CreateFileW(path.c_str(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
EXPECT_NE(h, INVALID_HANDLE_VALUE);
ASSERT_EQ(DeletePath(path.c_str(), nullptr), DELETE_PATH_ACCESS_DENIED);
CloseHandle(h);
}

TEST_F(WindowsFileOperationsTest, TestCannotDeleteBusyDirectory) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring path = tmp + L"\\dir" WLINE;
EXPECT_TRUE(CreateDirectoryW(path.c_str(), NULL));
HANDLE h = CreateFileW(path.c_str(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL);
EXPECT_NE(h, INVALID_HANDLE_VALUE);
ASSERT_EQ(DeletePath(path.c_str(), nullptr), DELETE_PATH_ACCESS_DENIED);
CloseHandle(h);
}

TEST_F(WindowsFileOperationsTest, TestCannotDeleteBusyJunction) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring name = tmp + L"\\junc" WLINE;
wstring target = tmp + L"\\target" WLINE;
EXPECT_TRUE(CreateDirectoryW(target.c_str(), NULL));
EXPECT_EQ(L"", CreateJunction(name, target));
// Open the junction itself (do not follow symlinks).
HANDLE h = CreateFileW(
name.c_str(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
ASSERT_EQ(DeletePath(name.c_str(), nullptr), DELETE_PATH_ACCESS_DENIED);
CloseHandle(h);
}

TEST_F(WindowsFileOperationsTest, TestCanDeleteJunctionWhoseTargetIsBusy) {
wstring tmp(kUncPrefix + GetTestTmpDirW());
wstring name = tmp + L"\\junc" WLINE;
wstring target = tmp + L"\\target" WLINE;
EXPECT_TRUE(CreateDirectoryW(target.c_str(), NULL));
EXPECT_EQ(L"", CreateJunction(name, target));
// Open the junction's target (follow symlinks).
HANDLE h = CreateFileW(target.c_str(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL);
ASSERT_EQ(DeletePath(name.c_str(), nullptr), DELETE_PATH_SUCCESS);
CloseHandle(h);
}

#undef TOSTRING
#undef TOWSTRING1
#undef TOWSTRING
#undef WLINE

} // namespace windows
} // namespace bazel

0 comments on commit 96c1599

Please sign in to comment.