diff options
Diffstat (limited to 'src/main/native/windows')
-rw-r--r-- | src/main/native/windows/BUILD | 43 | ||||
-rw-r--r-- | src/main/native/windows/file.cc | 186 | ||||
-rw-r--r-- | src/main/native/windows/file.h | 86 | ||||
-rw-r--r-- | src/main/native/windows/processes.cc | 643 | ||||
-rw-r--r-- | src/main/native/windows/util.cc | 168 | ||||
-rw-r--r-- | src/main/native/windows/util.h | 95 |
6 files changed, 1221 insertions, 0 deletions
diff --git a/src/main/native/windows/BUILD b/src/main/native/windows/BUILD new file mode 100644 index 0000000000..c7dead2469 --- /dev/null +++ b/src/main/native/windows/BUILD @@ -0,0 +1,43 @@ +package(default_visibility = ["//visibility:private"]) + +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src/main/native:__pkg__"], +) + +# TODO(xingao): verify that this filegroup contains exactly what it has to wrt. +# where it is used (//src/main/native:embedded_tools). +# Context: https://github.com/bazelbuild/bazel/commit/33d05f6b551cf2fdb257cb536a5e864d095144a1 +filegroup( + name = "embedded_tools", + srcs = [":srcs"], + visibility = ["//src/main/native:__pkg__"], +) + +filegroup( + name = "raw-sources", + srcs = glob([ + "*.cc", + "*.h", + ]), + visibility = ["//src/main/native:__pkg__"], +) + +cc_library( + name = "lib-file", + srcs = ["file.cc"], + hdrs = ["file.h"], + visibility = [ + "//src/main/cpp:__subpackages__", + "//src/test/cpp:__subpackages__", + "//src/test/native:__subpackages__", + ], + deps = [":lib-util"], +) + +cc_library( + name = "lib-util", + srcs = ["util.cc"], + hdrs = ["util.h"], +) diff --git a/src/main/native/windows/file.cc b/src/main/native/windows/file.cc new file mode 100644 index 0000000000..b5dc5974c8 --- /dev/null +++ b/src/main/native/windows/file.cc @@ -0,0 +1,186 @@ +// Copyright 2016 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 <windows.h> + +#include <memory> +#include <sstream> +#include <string> + +#include "src/main/native/windows/file.h" +#include "src/main/native/windows/util.h" + +namespace bazel { +namespace windows { + +using std::string; +using std::unique_ptr; +using std::wstring; + +int IsJunctionOrDirectorySymlink(const WCHAR* path) { + DWORD attrs = ::GetFileAttributesW(path); + if (attrs == INVALID_FILE_ATTRIBUTES) { + return IS_JUNCTION_ERROR; + } else { + if ((attrs & FILE_ATTRIBUTE_DIRECTORY) && + (attrs & FILE_ATTRIBUTE_REPARSE_POINT)) { + return IS_JUNCTION_YES; + } else { + return IS_JUNCTION_NO; + } + } +} + +bool GetLongPath(const WCHAR* path, unique_ptr<WCHAR[]>* result) { + DWORD size = ::GetLongPathNameW(path, NULL, 0); + if (size == 0) { + return false; + } + result->reset(new WCHAR[size]); + ::GetLongPathNameW(path, result->get(), size); + return true; +} + +HANDLE OpenDirectory(const WCHAR* path, bool read_write) { + return ::CreateFileW( + /* lpFileName */ path, + /* dwDesiredAccess */ + read_write ? (GENERIC_READ | GENERIC_WRITE) : GENERIC_READ, + /* dwShareMode */ 0, + /* lpSecurityAttributes */ NULL, + /* dwCreationDisposition */ OPEN_EXISTING, + /* dwFlagsAndAttributes */ FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_BACKUP_SEMANTICS, + /* hTemplateFile */ NULL); +} + +#pragma pack(push, 4) +typedef struct _JunctionDescription { + typedef struct _Header { + DWORD ReparseTag; + WORD ReparseDataLength; + WORD Reserved; + } Header; + + typedef struct _WriteDesc { + WORD SubstituteNameOffset; + WORD SubstituteNameLength; + WORD PrintNameOffset; + WORD PrintNameLength; + } WriteDesc; + + Header header; + WriteDesc write; + WCHAR PathBuffer[ANYSIZE_ARRAY]; +} JunctionDescription; +#pragma pack(pop) + +string CreateJunction(const wstring& junction_name, + const wstring& junction_target) { + const wstring target = HasUncPrefix(junction_target.c_str()) + ? junction_target.substr(4) + : junction_target; + // The entire JunctionDescription cannot be larger than + // MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes. + // + // The structure's layout is: + // [JunctionDescription::Header] + // [JunctionDescription::WriteDesc] + // ---- start of JunctionDescription::PathBuffer ---- + // [4 WCHARs] : "\??\" prefix + // [target.size() WCHARs] : junction target name + // [1 WCHAR] : null-terminator + // [target.size() WCHARs] : junction target displayed name + // [1 WCHAR] : null-terminator + // The sum of these must not exceed MAXIMUM_REPARSE_DATA_BUFFER_SIZE. + // We can rearrange this to get the limit for target.size(). + static const size_t kMaxJunctionTargetLen = + ((MAXIMUM_REPARSE_DATA_BUFFER_SIZE - sizeof(JunctionDescription::Header) - + sizeof(JunctionDescription::WriteDesc) - + /* one "\??\" prefix */ sizeof(WCHAR) * 4 - + /* two null terminators */ sizeof(WCHAR) * 2) / + /* two copies of the string are stored */ 2) / + sizeof(WCHAR); + if (target.size() > kMaxJunctionTargetLen) { + std::stringstream error; + error << "junction target is too long (" << target.size() + << " characters, limit: " << kMaxJunctionTargetLen << ")"; + return error.str(); + } + const wstring name = HasUncPrefix(junction_name.c_str()) + ? junction_name + : (wstring(L"\\\\?\\") + junction_name); + + // Junctions are directories, so create one + if (!::CreateDirectoryW(name.c_str(), NULL)) { + return string("CreateDirectoryW failed"); + } + + AutoHandle handle(OpenDirectory(name.c_str(), true)); + if (!handle.IsValid()) { + return string("OpenDirectory failed"); + } + + char reparse_buffer_bytes[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + JunctionDescription* reparse_buffer = + reinterpret_cast<JunctionDescription*>(reparse_buffer_bytes); + memset(reparse_buffer_bytes, 0, MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + + // "\??\" is meaningful to the kernel, it's a synomym for the "\DosDevices\" + // object path. (NOT to be confused with "\\?\" which is meaningful for the + // Win32 API.) We need to use this prefix to tell the kernel where the reparse + // point is pointing to. + memcpy(reparse_buffer->PathBuffer, L"\\??\\", 4 * sizeof(WCHAR)); + memcpy(reparse_buffer->PathBuffer + 4, target.c_str(), + target.size() * sizeof(WCHAR)); + + // In addition to their target, junctions also have another string which is a + // user-visible name of where the junction points, as listed by "dir". This + // can be any string and won't affect the usability of the junction. + // MKLINK uses the target path without the "\??\" prefix as the display name, + // so let's do that here too. This is also in line with how UNIX behaves. + // Using a dummy or fake display name would be pure evil, it would make the + // output of `dir` look like: + // 2017-01-18 01:37 PM <JUNCTION> juncname [dummy string] + memcpy(reparse_buffer->PathBuffer + 4 + target.size() + 1, target.c_str(), + target.size() * sizeof(WCHAR)); + + reparse_buffer->write.SubstituteNameOffset = 0; + reparse_buffer->write.SubstituteNameLength = + (4 + target.size()) * sizeof(WCHAR); + reparse_buffer->write.PrintNameOffset = + reparse_buffer->write.SubstituteNameLength + + /* null-terminator */ sizeof(WCHAR); + reparse_buffer->write.PrintNameLength = target.size() * sizeof(WCHAR); + + reparse_buffer->header.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + reparse_buffer->header.ReparseDataLength = + sizeof(JunctionDescription::WriteDesc) + + reparse_buffer->write.SubstituteNameLength + + reparse_buffer->write.PrintNameLength + + /* 2 null-terminators */ (2 * sizeof(WCHAR)); + reparse_buffer->header.Reserved = 0; + + DWORD bytes_returned; + if (!::DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparse_buffer, + reparse_buffer->header.ReparseDataLength + + sizeof(JunctionDescription::Header), + NULL, 0, &bytes_returned, NULL)) { + return string("DeviceIoControl(FSCTL_SET_REPARSE_POINT) failed"); + } + return ""; +} + +} // namespace windows +} // namespace bazel diff --git a/src/main/native/windows/file.h b/src/main/native/windows/file.h new file mode 100644 index 0000000000..b1a57101ff --- /dev/null +++ b/src/main/native/windows/file.h @@ -0,0 +1,86 @@ +// Copyright 2017 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. +#ifndef BAZEL_SRC_MAIN_NATIVE_WINDOWS_FILE_H_ +#define BAZEL_SRC_MAIN_NATIVE_WINDOWS_FILE_H_ + +#include <windows.h> + +#include <memory> +#include <string> + +namespace bazel { +namespace windows { + +using std::string; +using std::unique_ptr; +using std::wstring; + +template <typename char_type> +bool HasUncPrefix(const char_type* path) { + return path[0] == '\\' && (path[1] == '\\' || path[1] == '?') && + (path[2] == '.' || path[2] == '?') && path[3] == '\\'; +} + +// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations +enum { + IS_JUNCTION_YES = 0, + IS_JUNCTION_NO = 1, + IS_JUNCTION_ERROR = 2, +}; + +// Determines whether `path` is a junction (or directory symlink). +// +// `path` should be an absolute, normalized, Windows-style path, with "\\?\" +// prefix if it's longer than MAX_PATH. +// +// To read about differences between junctions and directory symlinks, +// see http://superuser.com/a/343079. In Bazel we only ever create junctions. +// +// Returns: +// - IS_JUNCTION_YES, if `path` exists and is either a directory junction or a +// directory symlink +// - IS_JUNCTION_NO, if `path` exists but is neither a directory junction nor a +// directory symlink; also when `path` is a symlink to a directory but it was +// created using "mklink" instead of "mklink /d", as such symlinks don't +// behave the same way as directories (e.g. they can't be listed) +// - IS_JUNCTION_ERROR, if `path` doesn't exist or some error occurred +int IsJunctionOrDirectorySymlink(const WCHAR* path); + +// Computes the long version of `path` if it has any 8dot3 style components. +// Returns true upon success and sets `result` to point to the buffer. +// `path` must be an absolute, normalized, Windows style path, with a "\\?\" +// prefix if it's longer than MAX_PATH. The result will have a "\\?\" prefix if +// and only if `path` had one as well. (It's the caller's responsibility to keep +// or remove this prefix.) +bool GetLongPath(const WCHAR* path, unique_ptr<WCHAR[]>* result); + +// Opens a directory using CreateFileW. +// `path` must be a valid Windows path, with "\\?\" prefix if it's long. +// If `read_write` is true then the directory is opened for reading and writing, +// otherwise only for reading. +HANDLE OpenDirectory(const WCHAR* path, bool read_write); + +// Creates a junction at `name`, pointing to `target`. +// Returns the empty string upon success, or a human-readable error message upon +// failure. +// Neither `junction_name` nor `junction_target` needs to have a "\\?\" prefix, +// not even if they are longer than MAX_PATH, though it's okay if they do. This +// function will add the right prefixes as necessary. +string CreateJunction(const wstring& junction_name, + const wstring& junction_target); + +} // namespace windows +} // namespace bazel + +#endif // BAZEL_SRC_MAIN_NATIVE_WINDOWS_FILE_H_ diff --git a/src/main/native/windows/processes.cc b/src/main/native/windows/processes.cc new file mode 100644 index 0000000000..89001733ea --- /dev/null +++ b/src/main/native/windows/processes.cc @@ -0,0 +1,643 @@ +// Copyright 2016 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. + +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +#include <ctype.h> +#include <jni.h> +#include <stdlib.h> +#include <string.h> +#include <wchar.h> +#include <windows.h> + +#include <atomic> +#include <functional> +#include <memory> +#include <string> +#include <type_traits> // static_assert + +#include "src/main/native/windows/file.h" +#include "src/main/native/windows/util.h" + +// Ensure we can safely cast (const) jchar* to (const) WCHAR* and LP(C)WSTR. +// This is true with MSVC but not always with GCC. +static_assert(sizeof(jchar) == sizeof(WCHAR), + "jchar and WCHAR should be the same size"); + +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetpid( + JNIEnv* env, jclass clazz) { + return GetCurrentProcessId(); +} + +struct NativeOutputStream { + HANDLE handle_; + std::string error_; + std::atomic<bool> closed_; + NativeOutputStream() + : handle_(INVALID_HANDLE_VALUE), error_(""), closed_(false) {} + + void close() { + closed_.store(true); + if (handle_ == INVALID_HANDLE_VALUE) { + return; + } + + CancelIoEx(handle_, NULL); + CloseHandle(handle_); + handle_ = INVALID_HANDLE_VALUE; + } +}; + +struct NativeProcess { + HANDLE stdin_; + NativeOutputStream stdout_; + NativeOutputStream stderr_; + HANDLE process_; + HANDLE job_; + DWORD pid_; + std::string error_; + + NativeProcess() + : stdin_(INVALID_HANDLE_VALUE), + stdout_(), + stderr_(), + process_(INVALID_HANDLE_VALUE), + job_(INVALID_HANDLE_VALUE), + error_("") {} +}; + +class JavaByteArray { + public: + JavaByteArray(JNIEnv* env, jbyteArray java_array) + : env_(env), + array_(java_array), + size_(java_array != nullptr ? env->GetArrayLength(java_array) : 0), + ptr_(java_array != nullptr ? env->GetByteArrayElements(java_array, NULL) + : nullptr) {} + + ~JavaByteArray() { + if (array_ != nullptr) { + env_->ReleaseByteArrayElements(array_, ptr_, 0); + array_ = nullptr; + size_ = 0; + ptr_ = nullptr; + } + } + + jsize size() { return size_; } + jbyte* ptr() { return ptr_; } + + private: + JNIEnv* env_; + jbyteArray array_; + jsize size_; + jbyte* ptr_; +}; + +static bool NestedJobsSupported() { + OSVERSIONINFOEX version_info; + version_info.dwOSVersionInfoSize = sizeof(version_info); + if (!GetVersionEx(reinterpret_cast<OSVERSIONINFO*>(&version_info))) { + return false; + } + + return version_info.dwMajorVersion > 6 || + version_info.dwMajorVersion == 6 && version_info.dwMinorVersion >= 2; +} + +static void MaybeReportLastError(const std::string& reason, JNIEnv* env, + jobjectArray error_msg_holder) { + if (error_msg_holder != nullptr && + env->GetArrayLength(error_msg_holder) > 0) { + std::string error_str = bazel::windows::GetLastErrorString(reason); + jstring error_msg = env->NewStringUTF(error_str.c_str()); + env->SetObjectArrayElement(error_msg_holder, 0, error_msg); + } +} + +static std::string GetJavaUTFString(JNIEnv* env, jstring str) { + std::string result; + if (str != nullptr) { + const char* jstr = env->GetStringUTFChars(str, nullptr); + result.assign(jstr); + env->ReleaseStringUTFChars(str, jstr); + } + return result; +} + +static std::wstring GetJavaWstring(JNIEnv* env, jstring str) { + std::wstring result; + if (str != nullptr) { + const jchar* jstr = env->GetStringChars(str, nullptr); + // We can safely reinterpret_cast because of the static_assert checking that + // sizeof(jchar) = sizeof(WCHAR). + result.assign(reinterpret_cast<const WCHAR*>(jstr)); + env->ReleaseStringChars(str, jstr); + } + return result; +} + +static std::wstring AddUncPrefixMaybe(const std::wstring& path) { + return (path.size() >= MAX_PATH) ? (std::wstring(L"\\\\?\\") + path) : path; +} + +static jlong PtrAsJlong(void* p) { return reinterpret_cast<jlong>(p); } + +static std::string AsExecutableForCreateProcess(JNIEnv* env, jstring path, + std::string* result) { + return bazel::windows::AsExecutablePathForCreateProcess( + GetJavaUTFString(env, path), + [env, path]() { return GetJavaWstring(env, path); }, result); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeCreateProcess( + JNIEnv* env, jclass clazz, jstring java_argv0, jstring java_argv_rest, + jbyteArray java_env, jstring java_cwd, jstring java_stdout_redirect, + jstring java_stderr_redirect) { + NativeProcess* result = new NativeProcess(); + + std::string argv0; + std::string error_msg(AsExecutableForCreateProcess(env, java_argv0, &argv0)); + if (!error_msg.empty()) { + result->error_ = error_msg; + return PtrAsJlong(result); + } + + std::string commandline = argv0 + " " + GetJavaUTFString(env, java_argv_rest); + std::wstring stdout_redirect = + AddUncPrefixMaybe(GetJavaWstring(env, java_stdout_redirect)); + std::wstring stderr_redirect = + AddUncPrefixMaybe(GetJavaWstring(env, java_stderr_redirect)); + std::string cwd; + error_msg = bazel::windows::AsShortPath( + GetJavaUTFString(env, java_cwd), + [env, java_cwd]() { return GetJavaWstring(env, java_cwd); }, &cwd); + if (!error_msg.empty()) { + result->error_ = error_msg; + return PtrAsJlong(result); + } + + std::unique_ptr<char[]> mutable_commandline(new char[commandline.size() + 1]); + strncpy(mutable_commandline.get(), commandline.c_str(), + commandline.size() + 1); + + SECURITY_ATTRIBUTES sa = {0}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + + // Standard file handles are closed even if the process was successfully + // created. If this was not so, operations on these file handles would not + // return immediately if the process is terminated. + // Therefore we make these handles auto-closing (by using AutoHandle). + bazel::windows::AutoHandle stdin_process; + bazel::windows::AutoHandle stdout_process; + bazel::windows::AutoHandle stderr_process; + bazel::windows::AutoHandle thread; + PROCESS_INFORMATION process_info = {0}; + STARTUPINFOA startup_info = {0}; + JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_info = {0}; + + JavaByteArray env_map(env, java_env); + if (env_map.ptr() != nullptr) { + if (env_map.size() < 2) { + result->error_ = "The environment must be at least two bytes long"; + return PtrAsJlong(result); + } else if (env_map.ptr()[env_map.size() - 1] != 0 || + env_map.ptr()[env_map.size() - 2] != 0) { + result->error_ = "Environment array must end with two null bytes"; + return PtrAsJlong(result); + } + } + + if (!CreatePipe(&stdin_process.handle, &result->stdin_, &sa, 0)) { + result->error_ = bazel::windows::GetLastErrorString("CreatePipe(stdin)"); + return PtrAsJlong(result); + } + + if (!stdout_redirect.empty()) { + result->stdout_.close(); + + stdout_process.handle = CreateFileW( + /* lpFileName */ stdout_redirect.c_str(), + /* dwDesiredAccess */ FILE_APPEND_DATA, + /* dwShareMode */ 0, + /* lpSecurityAttributes */ &sa, + /* dwCreationDisposition */ OPEN_ALWAYS, + /* dwFlagsAndAttributes */ FILE_ATTRIBUTE_NORMAL, + /* hTemplateFile */ NULL); + + if (!stdout_process.IsValid()) { + result->error_ = bazel::windows::GetLastErrorString("CreateFile(stdout)"); + return PtrAsJlong(result); + } + } else { + if (!CreatePipe(&result->stdout_.handle_, &stdout_process.handle, &sa, 0)) { + result->error_ = bazel::windows::GetLastErrorString("CreatePipe(stdout)"); + return PtrAsJlong(result); + } + } + + // The value of the stderr HANDLE. + // If stdout and stderr are redirected to the same file, this will be equal to + // stdout_process.handle and stderr_process.handle will be + // INVALID_HANDLE_VALUE, so the two AutoHandle objects' d'tors will not + // attempt to close stdout's handle twice. + // If stdout != stderr, then stderr_handle = stderr_process.handle, and these + // are distinct from stdout_process.handle, so again the d'tors will do the + // right thing, closing the handles. + // In both cases, we DO NOT close stderr_handle, since it's either + // stdout_process's or stderr_process's d'tor doing so. + HANDLE stderr_handle = INVALID_HANDLE_VALUE; + + if (!stderr_redirect.empty()) { + result->stderr_.close(); + if (stdout_redirect == stderr_redirect) { + stderr_handle = stdout_process.handle; + // do not set stderr_process.handle; it equals stdout_process.handle and + // the AutoHandle d'tor would attempt to close it again + } else { + stderr_handle = CreateFileW( + /* lpFileName */ stderr_redirect.c_str(), + /* dwDesiredAccess */ FILE_APPEND_DATA, + /* dwShareMode */ 0, + /* lpSecurityAttributes */ &sa, + /* dwCreationDisposition */ OPEN_ALWAYS, + /* dwFlagsAndAttributes */ FILE_ATTRIBUTE_NORMAL, + /* hTemplateFile */ NULL); + + if (stderr_handle == INVALID_HANDLE_VALUE) { + result->error_ = + bazel::windows::GetLastErrorString("CreateFile(stderr)"); + return PtrAsJlong(result); + } + // stderr_process != stdout_process, so set its handle, so the AutoHandle + // d'tor will close it + stderr_process.handle = stderr_handle; + } + } else { + if (!CreatePipe(&result->stderr_.handle_, &stderr_handle, &sa, 0)) { + result->error_ = bazel::windows::GetLastErrorString("CreatePipe(stderr)"); + return PtrAsJlong(result); + } + stderr_process.handle = stderr_handle; + } + + // MDSN says that the default for job objects is that breakaway is not + // allowed. Thus, we don't need to do any more setup here. + HANDLE job = CreateJobObject(NULL, NULL); + if (job == NULL) { + result->error_ = bazel::windows::GetLastErrorString("CreateJobObject()"); + return PtrAsJlong(result); + } + + result->job_ = job; + + job_info.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, + &job_info, sizeof(job_info))) { + result->error_ = + bazel::windows::GetLastErrorString("SetInformationJobObject()"); + return PtrAsJlong(result); + } + + startup_info.hStdInput = stdin_process.handle; + startup_info.hStdOutput = stdout_process.handle; + startup_info.hStdError = stderr_handle; + startup_info.dwFlags |= STARTF_USESTDHANDLES; + + BOOL ok = CreateProcessA( + /* lpApplicationName */ NULL, + /* lpCommandLine */ mutable_commandline.get(), + /* lpProcessAttributes */ NULL, + /* lpThreadAttributes */ NULL, + /* bInheritHandles */ TRUE, + /* dwCreationFlags */ CREATE_NO_WINDOW // Don't create a console window + | CREATE_NEW_PROCESS_GROUP // So that Ctrl-Break is not propagated + | CREATE_SUSPENDED, // So that it doesn't start a new job itself + /* lpEnvironment */ env_map.ptr(), + /* lpCurrentDirectory */ cwd.empty() ? nullptr : cwd.c_str(), + /* lpStartupInfo */ &startup_info, + /* lpProcessInformation */ &process_info); + + if (!ok) { + result->error_ = bazel::windows::GetLastErrorString("CreateProcess()"); + return PtrAsJlong(result); + } + + result->pid_ = process_info.dwProcessId; + result->process_ = process_info.hProcess; + thread.handle = process_info.hThread; + + if (!AssignProcessToJobObject(result->job_, result->process_)) { + BOOL is_in_job = false; + if (IsProcessInJob(result->process_, NULL, &is_in_job) && is_in_job && + !NestedJobsSupported()) { + // We are on a pre-Windows 8 system and the Bazel is already in a job. + // We can't create nested jobs, so just revert to TerminateProcess() and + // hope for the best. In batch mode, the launcher puts Bazel in a job so + // that will take care of cleanup once the command finishes. + CloseHandle(result->job_); + result->job_ = INVALID_HANDLE_VALUE; + } else { + result->error_ = + bazel::windows::GetLastErrorString("AssignProcessToJobObject()"); + return PtrAsJlong(result); + } + } + + // Now that we put the process in a new job object, we can start executing it + if (ResumeThread(thread) == -1) { + result->error_ = bazel::windows::GetLastErrorString("ResumeThread()"); + return PtrAsJlong(result); + } + + result->error_ = ""; + return PtrAsJlong(result); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWriteStdin( + JNIEnv* env, jclass clazz, jlong process_long, jbyteArray java_bytes, + jint offset, jint length) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + + JavaByteArray bytes(env, java_bytes); + if (offset < 0 || length <= 0 || offset > bytes.size() - length) { + process->error_ = "Array index out of bounds"; + return -1; + } + + DWORD bytes_written; + + if (!::WriteFile(process->stdin_, bytes.ptr() + offset, length, + &bytes_written, NULL)) { + process->error_ = bazel::windows::GetLastErrorString("WriteFile()"); + bytes_written = -1; + } + + process->error_ = ""; + return bytes_written; +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetStdout( + JNIEnv* env, jclass clazz, jlong process_long) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + return PtrAsJlong(&process->stdout_); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetStderr( + JNIEnv* env, jclass clazz, jlong process_long) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + return PtrAsJlong(&process->stderr_); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeReadStream( + JNIEnv* env, jclass clazz, jlong stream_long, jbyteArray java_bytes, + jint offset, jint length) { + NativeOutputStream* stream = + reinterpret_cast<NativeOutputStream*>(stream_long); + + JavaByteArray bytes(env, java_bytes); + if (offset < 0 || length <= 0 || offset > bytes.size() - length) { + stream->error_ = "Array index out of bounds"; + return -1; + } + + if (stream->handle_ == INVALID_HANDLE_VALUE || stream->closed_.load()) { + stream->error_ = ""; + return 0; + } + + DWORD bytes_read; + if (!::ReadFile(stream->handle_, bytes.ptr() + offset, length, &bytes_read, + NULL)) { + // Check if either the other end closed the pipe or we did it with + // NativeOutputStream.close() . In the latter case, we'll get a "system + // call interrupted" error. + if (GetLastError() == ERROR_BROKEN_PIPE || stream->closed_.load()) { + // End of file. + stream->error_ = ""; + bytes_read = 0; + } else { + stream->error_ = bazel::windows::GetLastErrorString("ReadFile()"); + bytes_read = -1; + } + } else { + stream->error_ = ""; + } + + return bytes_read; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetExitCode( + JNIEnv* env, jclass clazz, jlong process_long) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + DWORD exit_code; + if (!GetExitCodeProcess(process->process_, &exit_code)) { + process->error_ = + bazel::windows::GetLastErrorString("GetExitCodeProcess()"); + return -1; + } + + return exit_code; +} + +// return values: +// 0: Wait completed successfully +// 1: Timeout +// 2: Wait returned with an error +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWaitFor( + JNIEnv* env, jclass clazz, jlong process_long, jlong java_timeout) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + HANDLE handles[1] = {process->process_}; + DWORD win32_timeout = java_timeout < 0 ? INFINITE : java_timeout; + jint result; + switch (WaitForMultipleObjects(1, handles, FALSE, win32_timeout)) { + case 0: + result = 0; + break; + + case WAIT_TIMEOUT: + result = 1; + break; + + case WAIT_FAILED: + result = 2; + break; + + default: + process->error_ = "WaitForMultipleObjects() returned unknown result"; + result = 2; + break; + } + + // Close the pipe handles so that any pending nativeReadStream() calls + // return. This will call CancelIoEx() on the file handles in order to make + // ReadFile() in nativeReadStream() return; otherwise, CloseHandle() would + // hang. + // + // This protects against a subprocess being created, it passing the write + // side of the stdout/stderr pipes to a subprocess, then dying. In that case, + // if we didn't do this, the Java side of the code would hang waiting for the + // streams to finish. + // + // An alternative implementation would be to rely on job control terminating + // the subprocesses, but we don't want to assume that it's always available. + process->stdout_.close(); + process->stderr_.close(); + + if (process->stdin_ != INVALID_HANDLE_VALUE) { + CloseHandle(process->stdin_); + process->stdin_ = INVALID_HANDLE_VALUE; + } + + return result; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetProcessPid( + JNIEnv* env, jclass clazz, jlong process_long) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + process->error_ = ""; + return GetProcessId(process->process_); // MSDN says that this cannot fail +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeTerminate( + JNIEnv* env, jclass clazz, jlong process_long) { + static const UINT exit_code = 130; // 128 + SIGINT, like on Linux + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + + if (process->job_ != INVALID_HANDLE_VALUE) { + if (!TerminateJobObject(process->job_, exit_code)) { + process->error_ = + bazel::windows::GetLastErrorString("TerminateJobObject()"); + return JNI_FALSE; + } + } else if (process->process_ != INVALID_HANDLE_VALUE) { + if (!TerminateProcess(process->process_, exit_code)) { + process->error_ = + bazel::windows::GetLastErrorString("TerminateProcess()"); + return JNI_FALSE; + } + } + + process->error_ = ""; + return JNI_TRUE; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeDeleteProcess( + JNIEnv* env, jclass clazz, jlong process_long) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + + if (process->stdin_ != INVALID_HANDLE_VALUE) { + CloseHandle(process->stdin_); + } + + process->stdout_.close(); + process->stderr_.close(); + + if (process->process_ != INVALID_HANDLE_VALUE) { + CloseHandle(process->process_); + } + + if (process->job_ != INVALID_HANDLE_VALUE) { + CloseHandle(process->job_); + } + + delete process; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeCloseStream( + JNIEnv* env, jclass clazz, jlong stream_long) { + NativeOutputStream* stream = + reinterpret_cast<NativeOutputStream*>(stream_long); + stream->close(); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeProcessGetLastError( + JNIEnv* env, jclass clazz, jlong process_long) { + NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); + jstring result = env->NewStringUTF(process->error_.c_str()); + process->error_ = ""; + return result; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeStreamGetLastError( + JNIEnv* env, jclass clazz, jlong stream_long) { + NativeOutputStream* stream = + reinterpret_cast<NativeOutputStream*>(stream_long); + jstring result = env->NewStringUTF(stream->error_.c_str()); + stream->error_ = ""; + return result; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeIsJunction( + JNIEnv* env, jclass clazz, jstring path, jobjectArray error_msg_holder) { + int result = bazel::windows::IsJunctionOrDirectorySymlink( + GetJavaWstring(env, path).c_str()); + if (result == bazel::windows::IS_JUNCTION_ERROR) { + MaybeReportLastError( + std::string("GetFileAttributes(") + GetJavaUTFString(env, path) + ")", + env, error_msg_holder); + } + return result; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetLongPath( + JNIEnv* env, jclass clazz, jstring path, jobjectArray result_holder, + jobjectArray error_msg_holder) { + std::unique_ptr<WCHAR[]> result; + bool success = + bazel::windows::GetLongPath(GetJavaWstring(env, path).c_str(), &result); + if (!success) { + MaybeReportLastError( + std::string("GetLongPathName(") + GetJavaUTFString(env, path) + ")", + env, error_msg_holder); + return JNI_FALSE; + } + env->SetObjectArrayElement( + result_holder, 0, + env->NewString(reinterpret_cast<const jchar*>(result.get()), + wcslen(result.get()))); + return JNI_TRUE; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeCreateJunction( + JNIEnv* env, jclass clazz, jstring name, jstring target, + jobjectArray error_msg_holder) { + std::string error = bazel::windows::CreateJunction( + GetJavaWstring(env, name), GetJavaWstring(env, target)); + if (!error.empty()) { + MaybeReportLastError(error, env, error_msg_holder); + return JNI_FALSE; + } + return JNI_TRUE; +} diff --git a/src/main/native/windows/util.cc b/src/main/native/windows/util.cc new file mode 100644 index 0000000000..8f43fe7b74 --- /dev/null +++ b/src/main/native/windows/util.cc @@ -0,0 +1,168 @@ +// Copyright 2016 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 <stdio.h> +#include <stdlib.h> +#include <windows.h> + +#include <algorithm> +#include <functional> +#include <memory> +#include <string> + +#include "src/main/native/windows/util.h" + +namespace bazel { +namespace windows { + +using std::function; +using std::string; +using std::unique_ptr; +using std::wstring; + +string GetLastErrorString(const string& cause) { + DWORD last_error = GetLastError(); + if (last_error == 0) { + return ""; + } + + LPSTR message; + DWORD size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&message, 0, NULL); + + if (size == 0) { + char buf[256]; + snprintf(buf, sizeof(buf), + "%s: Error %d (cannot format message due to error %d)", + cause.c_str(), last_error, GetLastError()); + buf[sizeof(buf) - 1] = 0; + } + + string result = string(message); + LocalFree(message); + return cause + ": " + result; +} + +static void QuotePath(const string& path, string* result) { + *result = string("\"") + path + "\""; +} + +static bool IsSeparator(char c) { return c == '/' || c == '\\'; } + +static bool HasSeparator(const string& s) { + return s.find_first_of('/') != string::npos || + s.find_first_of('\\') != string::npos; +} + +static bool Contains(const string& s, const char* substr) { + return s.find(substr) != string::npos; +} + +string AsShortPath(string path, function<wstring()> path_as_wstring, + string* result) { + if (path.empty()) { + result->clear(); + return ""; + } + if (path[0] == '"') { + return string("path should not be quoted"); + } + if (IsSeparator(path[0])) { + return string("path='") + path + "' is absolute"; + } + if (Contains(path, "/./") || Contains(path, "\\.\\") || + Contains(path, "/..") || Contains(path, "\\..")) { + return string("path='") + path + "' is not normalized"; + } + if (path.size() >= MAX_PATH && !HasSeparator(path)) { + return string("path='") + path + "' is just a file name but too long"; + } + if (HasSeparator(path) && + !(isalpha(path[0]) && path[1] == ':' && IsSeparator(path[2]))) { + return string("path='") + path + "' is not an absolute path"; + } + // At this point we know the path is either just a file name (shorter than + // MAX_PATH), or an absolute, normalized, Windows-style path (of any length). + + std::replace(path.begin(), path.end(), '/', '\\'); + // Fast-track: the path is already short. + if (path.size() < MAX_PATH) { + *result = path; + return ""; + } + // At this point we know that the path is at least MAX_PATH long and that it's + // absolute, normalized, and Windows-style. + + // Retrieve string as UTF-16 path, add "\\?\" prefix. + wstring wlong = wstring(L"\\\\?\\") + path_as_wstring(); + + // Experience shows that: + // - GetShortPathNameW's result has a "\\?\" prefix if and only if the input + // did too (though this behavior is not documented on MSDN) + // - CreateProcess{A,W} only accept an executable of MAX_PATH - 1 length + // Therefore for our purposes the acceptable shortened length is + // MAX_PATH + 4 (null-terminated). That is, MAX_PATH - 1 for the shortened + // path, plus a potential "\\?\" prefix that's only there if `wlong` also had + // it and which we'll omit from `result`, plus a null terminator. + static const size_t kMaxShortPath = MAX_PATH + 4; + + WCHAR wshort[kMaxShortPath]; + DWORD wshort_size = ::GetShortPathNameW(wlong.c_str(), NULL, 0); + if (wshort_size == 0) { + return GetLastErrorString(string("GetShortPathName failed (path=") + path + + ")"); + } + + if (wshort_size >= kMaxShortPath) { + return string("GetShortPathName would not shorten the path enough (path=") + + path + ")"; + } + GetShortPathNameW(wlong.c_str(), wshort, kMaxShortPath); + + // Convert the result to UTF-8. + char mbs_short[MAX_PATH]; + size_t mbs_size = wcstombs( + mbs_short, + wshort + 4, // we know it has a "\\?\" prefix, because `wlong` also did + MAX_PATH); + if (mbs_size < 0 || mbs_size >= MAX_PATH) { + return string("wcstombs failed (path=") + path + ")"; + } + mbs_short[mbs_size] = 0; + + *result = mbs_short; + return ""; +} + +string AsExecutablePathForCreateProcess(const string& path, + function<wstring()> path_as_wstring, + string* result) { + if (path.empty()) { + return string("path should not be empty"); + } + string error = AsShortPath(path, path_as_wstring, result); + if (error.empty()) { + // Quote the path in case it's something like "c:\foo\app name.exe". + // Do this unconditionally, there's no harm in quoting. Quotes are not + // allowed inside paths so we don't need to escape quotes. + QuotePath(*result, result); + } + return error; +} + +} // namespace windows +} // namespace bazel diff --git a/src/main/native/windows/util.h b/src/main/native/windows/util.h new file mode 100644 index 0000000000..1e3c2ceaf1 --- /dev/null +++ b/src/main/native/windows/util.h @@ -0,0 +1,95 @@ +// Copyright 2016 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. + +#ifndef BAZEL_SRC_MAIN_NATIVE_WINDOWS_UTIL_H__ +#define BAZEL_SRC_MAIN_NATIVE_WINDOWS_UTIL_H__ + +#include <windows.h> + +#include <functional> +#include <memory> +#include <string> + +namespace bazel { +namespace windows { + +using std::function; +using std::string; +using std::unique_ptr; +using std::wstring; + +// A wrapper for the `HANDLE` type that calls CloseHandle in its d'tor. +// WARNING: do not use for HANDLE returned by FindFirstFile; those must be +// closed with FindClose (otherwise they aren't closed properly). +struct AutoHandle { + AutoHandle(HANDLE _handle = INVALID_HANDLE_VALUE) : handle(_handle) {} + + ~AutoHandle() { + ::CloseHandle(handle); // succeeds if handle == INVALID_HANDLE_VALUE + handle = INVALID_HANDLE_VALUE; + } + + bool IsValid() { return handle != INVALID_HANDLE_VALUE && handle != NULL; } + + AutoHandle& operator=(const HANDLE& rhs) { + ::CloseHandle(handle); + handle = rhs; + return *this; + } + + operator HANDLE() const { return handle; } + + HANDLE handle; +}; + +string GetLastErrorString(const string& cause); + +// Same as `AsExecutablePathForCreateProcess` except it won't quote the result. +string AsShortPath(string path, function<wstring()> path_as_wstring, + string* result); + +// Computes a path suitable as the executable part in CreateProcessA's cmdline. +// +// The null-terminated executable path for CreateProcessA has to fit into +// MAX_PATH, therefore the limit for the executable's path is MAX_PATH - 1 +// (not including null terminator). This method attempts to convert the input +// `path` to a short format to fit it into the MAX_PATH - 1 limit. +// +// `path` must be either an absolute, normalized, Windows-style path with drive +// letter (e.g. "c:\foo\bar.exe", but no "\foo\bar.exe"), or must be just a file +// name (e.g. "cmd.exe") that's shorter than MAX_PATH (without null-terminator). +// In both cases, `path` must be unquoted. +// +// `path_as_wstring` must be a function that retrieves `path` as (or converts it +// to) a wstring, without performing any transformations on the path. +// +// If this function succeeds, it returns an empty string (indicating no error), +// and sets `result` to the resulting path, which is always quoted, and is +// always at most MAX_PATH + 1 long (MAX_PATH - 1 without null terminator, plus +// two quotes). If there's any error, this function returns the error message. +// +// If `path` is at most MAX_PATH - 1 long (not including null terminator), the +// result will be that (plus quotes). +// Otherwise this method attempts to compute an 8dot3 style short name for +// `path`, and if that succeeds and the result is at most MAX_PATH - 1 long (not +// including null terminator), then that will be the result (plus quotes). +// Otherwise this function fails and returns an error message. +string AsExecutablePathForCreateProcess(const string& path, + function<wstring()> path_as_wstring, + string* result); + +} // namespace windows +} // namespace bazel + +#endif // BAZEL_SRC_MAIN_NATIVE_WINDOWS_UTIL_H__ |