Windows, JNI: use WaitableProcess in blaze_util

Replace custom process- and job-creation logic in
blaze::ExecuteProgram with the JNI library's
WaitableProcess.

Add a new overload of WaitableProcess::Create,
which allows connecting the caller's
stdin/stdout/stderr to the child process.

Closes #8113.

PiperOrigin-RevId: 245214607
diff --git a/src/main/cpp/BUILD b/src/main/cpp/BUILD
index 7135055..483df2c 100644
--- a/src/main/cpp/BUILD
+++ b/src/main/cpp/BUILD
@@ -61,7 +61,11 @@
         "//src/main/cpp/util:blaze_exit_code",
         "//src/main/cpp/util:logging",
     ] + select({
-        "//src/conditions:windows": ["//src/main/native/windows:lib-file"],
+        "//src/conditions:windows": [
+            "//src/main/native/windows:lib-file",
+            "//src/main/native/windows:lib-process",
+            "//src/main/native/windows:lib-util",
+        ],
         "//conditions:default": [],
     }),
 )
diff --git a/src/main/cpp/blaze_util_windows.cc b/src/main/cpp/blaze_util_windows.cc
index 04f7eab..fe13876 100644
--- a/src/main/cpp/blaze_util_windows.cc
+++ b/src/main/cpp/blaze_util_windows.cc
@@ -14,17 +14,19 @@
 
 #include "src/main/cpp/blaze_util_platform.h"
 
-#include <fcntl.h>
-#include <stdarg.h>  // va_start, va_end, va_list
-
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
 #include <windows.h>
-#include <lmcons.h>          // UNLEN
-#include <versionhelpers.h>  // IsWindows8OrGreater
 
-#include <io.h>            // _open
-#include <knownfolders.h>  // FOLDERID_Profile
-#include <objbase.h>       // CoTaskMemFree
-#include <shlobj.h>        // SHGetKnownFolderPath
+#include <fcntl.h>
+#include <io.h>              // _open
+#include <knownfolders.h>    // FOLDERID_Profile
+#include <lmcons.h>          // UNLEN
+#include <objbase.h>         // CoTaskMemFree
+#include <shlobj.h>          // SHGetKnownFolderPath
+#include <stdarg.h>          // va_start, va_end, va_list
+#include <versionhelpers.h>  // IsWindows8OrGreater
 
 #include <algorithm>
 #include <cstdio>
@@ -51,6 +53,7 @@
 #include "src/main/cpp/util/path_platform.h"
 #include "src/main/cpp/util/strings.h"
 #include "src/main/native/windows/file.h"
+#include "src/main/native/windows/process.h"
 #include "src/main/native/windows/util.h"
 
 namespace blaze {
@@ -329,8 +332,6 @@
   exit(exit_code);
 }
 
-
-
 // A signal-safe version of fprintf(stderr, ...).
 //
 // WARNING: any output from the blaze client may be interleaved
@@ -482,51 +483,56 @@
 static const int MAX_CMDLINE_LENGTH = 32768;
 
 struct CmdLine {
-  char cmdline[MAX_CMDLINE_LENGTH];
+  WCHAR cmdline[MAX_CMDLINE_LENGTH];
 };
 static void CreateCommandLine(CmdLine* result, const string& exe,
                               const std::vector<string>& args_vector) {
-  std::ostringstream cmdline;
+  std::wstringstream cmdline;
   string short_exe;
-  string error;
-  if (!blaze_util::AsShortWindowsPath(exe, &short_exe, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "CreateCommandLine: AsShortWindowsPath(" << exe << "): " << error;
+  if (!exe.empty()) {
+    string error;
+    if (!blaze_util::AsShortWindowsPath(exe, &short_exe, &error)) {
+      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
+          << "CreateCommandLine: AsShortWindowsPath(" << exe << "): " << error;
+    }
+    wstring wshort_exe = blaze_util::CstringToWstring(short_exe.c_str()).get();
+    cmdline << L'\"' << wshort_exe << L'\"';
   }
+
   bool first = true;
   for (const auto& s : args_vector) {
     if (first) {
+      // Skip first argument, it is equal to 'exe'.
       first = false;
-      // Skip first argument, instead use quoted executable name.
-      cmdline << '\"' << short_exe << '\"';
       continue;
     } else {
-      cmdline << ' ';
+      cmdline << L' ';
     }
 
     bool has_space = s.find(" ") != string::npos;
 
     if (has_space) {
-      cmdline << '\"';
+      cmdline << L'\"';
     }
 
-    std::string::const_iterator it = s.begin();
-    while (it != s.end()) {
-      char ch = *it++;
+    wstring ws = blaze_util::CstringToWstring(s.c_str()).get();
+    std::wstring::const_iterator it = ws.begin();
+    while (it != ws.end()) {
+      wchar_t ch = *it++;
       switch (ch) {
-        case '"':
+        case L'"':
           // Escape double quotes
-          cmdline << "\\\"";
+          cmdline << L"\\\"";
           break;
 
-        case '\\':
-          if (it == s.end()) {
+        case L'\\':
+          if (it == ws.end()) {
             // Backslashes at the end of the string are quoted if we add quotes
-            cmdline << (has_space ? "\\\\" : "\\");
+            cmdline << (has_space ? L"\\\\" : L"\\");
           } else {
             // Backslashes everywhere else are quoted if they are followed by a
             // quote or a backslash
-            cmdline << (*it == '"' || *it == '\\' ? "\\\\" : "\\");
+            cmdline << (*it == L'"' || *it == L'\\' ? L"\\\\" : L"\\");
           }
           break;
 
@@ -536,20 +542,21 @@
     }
 
     if (has_space) {
-      cmdline << '\"';
+      cmdline << L'\"';
     }
   }
 
-  string cmdline_str = cmdline.str();
+  wstring cmdline_str = cmdline.str();
   if (cmdline_str.size() >= MAX_CMDLINE_LENGTH) {
     BAZEL_DIE(blaze_exit_code::INTERNAL_ERROR)
         << "Command line too long (" << cmdline_str.size() << " > "
-        << MAX_CMDLINE_LENGTH << "): " << cmdline_str;
+        << MAX_CMDLINE_LENGTH
+        << "): " << blaze_util::WstringToCstring(cmdline_str.c_str()).get();
   }
 
   // Copy command line into a mutable buffer.
   // CreateProcess is allowed to mutate its command line argument.
-  strncpy(result->cmdline, cmdline_str.c_str(), MAX_CMDLINE_LENGTH - 1);
+  wcsncpy(result->cmdline, cmdline_str.c_str(), MAX_CMDLINE_LENGTH - 1);
   result->cmdline[MAX_CMDLINE_LENGTH - 1] = 0;
 }
 
@@ -711,8 +718,8 @@
   }
 
   PROCESS_INFORMATION processInfo = {0};
-  STARTUPINFOEXA startupInfoEx = {0};
-  lpAttributeList->InitStartupInfoExA(&startupInfoEx);
+  STARTUPINFOEXW startupInfoEx = {0};
+  lpAttributeList->InitStartupInfoExW(&startupInfoEx);
 
   CmdLine cmdline;
   CreateCommandLine(&cmdline, exe, args_vector);
@@ -721,14 +728,14 @@
   {
     WithEnvVars env_obj(env);
 
-    ok = CreateProcessA(
+    ok = CreateProcessW(
         /* lpApplicationName */ NULL,
         /* lpCommandLine */ cmdline.cmdline,
         /* lpProcessAttributes */ NULL,
         /* lpThreadAttributes */ NULL,
         /* bInheritHandles */ TRUE,
         /* dwCreationFlags */ DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP |
-        EXTENDED_STARTUPINFO_PRESENT,
+            EXTENDED_STARTUPINFO_PRESENT,
         /* lpEnvironment */ NULL,
         /* lpCurrentDirectory */ NULL,
         /* lpStartupInfo */ &startupInfoEx.StartupInfo,
@@ -770,85 +777,28 @@
 // argument vector, wait for it to finish, then exit ourselves with the exitcode
 // of that program.
 void ExecuteProgram(const string& exe, const std::vector<string>& args_vector) {
+  std::wstring wexe = blaze_util::CstringToWstring(exe.c_str()).get();
+
   CmdLine cmdline;
-  CreateCommandLine(&cmdline, exe, args_vector);
+  CreateCommandLine(&cmdline, "", args_vector);
 
-  STARTUPINFOA startupInfo = {0};
-  startupInfo.cb = sizeof(STARTUPINFOA);
-
-  PROCESS_INFORMATION processInfo = {0};
-
-  HANDLE job = INVALID_HANDLE_VALUE;
-  if (NestedJobsSupported()) {
-    job = CreateJobObject(NULL, NULL);
-    if (job == NULL) {
-      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "ExecuteProgram(" << exe
-          << "): CreateJobObject failed: " << GetLastErrorString();
-    }
-
-    JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_info = {0};
-    job_info.BasicLimitInformation.LimitFlags =
-        JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
-
-    if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation,
-                                 &job_info, sizeof(job_info))) {
-      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "ExecuteProgram(" << exe
-          << "): SetInformationJobObject failed: " << GetLastErrorString();
-    }
-  }
-
-  BOOL success = CreateProcessA(
-      /* lpApplicationName */ NULL,
-      /* lpCommandLine */ cmdline.cmdline,
-      /* lpProcessAttributes */ NULL,
-      /* lpThreadAttributes */ NULL,
-      /* bInheritHandles */ TRUE,
-      /* dwCreationFlags */ CREATE_SUSPENDED,
-      /* lpEnvironment */ NULL,
-      /* lpCurrentDirectory */ NULL,
-      /* lpStartupInfo */ &startupInfo,
-      /* lpProcessInformation */ &processInfo);
-
-  if (!success) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "ExecuteProgram(" << exe << "): CreateProcess(" << cmdline.cmdline
-        << ") failed: " << GetLastErrorString();
-  }
-
-  // On Windows versions that support nested jobs (Windows 8 and above), we
-  // assign the Bazel server to a job object. Every process that Bazel creates,
-  // as well as all their child processes, will be assigned to this job object.
-  // When the Bazel server terminates the OS can reliably kill the entire
-  // process tree under it. On Windows versions that don't support nested jobs
-  // (Windows 7), we don't assign the Bazel server to a big job object. Instead,
-  // when Bazel creates new processes, it does so using the JNI library. The
-  // library assigns individual job objects to each subprocess. This way when
-  // these processes terminate, the OS can kill all their subprocesses. Bazel's
-  // own subprocesses are not in a job object though, so we only create
-  // subprocesses via the JNI library.
-  if (job != INVALID_HANDLE_VALUE) {
-    if (!AssignProcessToJobObject(job, processInfo.hProcess)) {
-      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "ExecuteProgram(" << exe
-          << "): AssignProcessToJobObject failed: " << GetLastErrorString();
-    }
-  }
-  // Now that we potentially put the process into a new job object, we can start
-  // running it.
-  if (ResumeThread(processInfo.hThread) == -1) {
+  bazel::windows::WaitableProcess proc;
+  std::wstring werror;
+  if (!proc.Create(wexe, cmdline.cmdline, nullptr, L"", &werror) ||
+      proc.WaitFor(-1, nullptr, &werror) !=
+          bazel::windows::WaitableProcess::kWaitSuccess) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
         << "ExecuteProgram(" << exe
-        << "): ResumeThread failed: " << GetLastErrorString();
+        << ") failed: " << blaze_util::WstringToCstring(werror.c_str()).get();
   }
-
-  WaitForSingleObject(processInfo.hProcess, INFINITE);
-  DWORD exit_code;
-  GetExitCodeProcess(processInfo.hProcess, &exit_code);
-  CloseHandle(processInfo.hProcess);
-  CloseHandle(processInfo.hThread);
-  exit(exit_code);
+  werror.clear();
+  int x = proc.GetExitCode(&werror);
+  if (!werror.empty()) {
+    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
+        << "ExecuteProgram(" << exe
+        << ") failed: " << blaze_util::WstringToCstring(werror.c_str()).get();
+  }
+  exit(x);
 }
 
 const char kListSeparator = ';';
diff --git a/src/main/native/windows/BUILD b/src/main/native/windows/BUILD
index e8b7a0b..8567b35 100644
--- a/src/main/native/windows/BUILD
+++ b/src/main/native/windows/BUILD
@@ -38,6 +38,7 @@
     srcs = ["process.cc"],
     hdrs = ["process.h"],
     visibility = [
+        "//src/main/cpp:__pkg__",
         "//tools/test:__pkg__",
     ],
     deps = [":lib-util"],
@@ -48,6 +49,7 @@
     srcs = ["util.cc"],
     hdrs = ["util.h"],
     visibility = [
+        "//src/main/cpp:__pkg__",
         "//src/tools/launcher/util:__pkg__",
         "//tools/test:__pkg__",
     ],
diff --git a/src/main/native/windows/process.cc b/src/main/native/windows/process.cc
index 7aa6092..4d61a39 100644
--- a/src/main/native/windows/process.cc
+++ b/src/main/native/windows/process.cc
@@ -29,12 +29,48 @@
   return s.str();
 }
 
+static bool DupeHandle(HANDLE h, AutoHandle* out, std::wstring* error) {
+  HANDLE dup;
+  if (!DuplicateHandle(GetCurrentProcess(), h, GetCurrentProcess(), &dup, 0,
+                       TRUE, DUPLICATE_SAME_ACCESS)) {
+    DWORD err = GetLastError();
+    *error =
+        MakeErrorMessage(WSTR(__FILE__), __LINE__, L"DupeHandle", L"", err);
+    return false;
+  }
+  *out = dup;
+  return true;
+}
+
+bool WaitableProcess::Create(const std::wstring& argv0,
+                             const std::wstring& argv_rest, void* env,
+                             const std::wstring& wcwd, std::wstring* error) {
+  AutoHandle dup_in, dup_out, dup_err;
+  if (!DupeHandle(GetStdHandle(STD_INPUT_HANDLE), &dup_in, error) ||
+      !DupeHandle(GetStdHandle(STD_OUTPUT_HANDLE), &dup_out, error) ||
+      !DupeHandle(GetStdHandle(STD_ERROR_HANDLE), &dup_err, error)) {
+    return false;
+  }
+  return Create(argv0, argv_rest, env, wcwd, dup_in, dup_out, dup_err, nullptr,
+                true, error);
+}
+
 bool WaitableProcess::Create(const std::wstring& argv0,
                              const std::wstring& argv_rest, void* env,
                              const std::wstring& wcwd, HANDLE stdin_process,
                              HANDLE stdout_process, HANDLE stderr_process,
                              LARGE_INTEGER* opt_out_start_time,
                              std::wstring* error) {
+  return Create(argv0, argv_rest, env, wcwd, stdin_process, stdout_process,
+                stderr_process, opt_out_start_time, false, error);
+}
+
+bool WaitableProcess::Create(const std::wstring& argv0,
+                             const std::wstring& argv_rest, void* env,
+                             const std::wstring& wcwd, HANDLE stdin_process,
+                             HANDLE stdout_process, HANDLE stderr_process,
+                             LARGE_INTEGER* opt_out_start_time,
+                             bool create_window, std::wstring* error) {
   std::wstring cwd;
   std::wstring error_msg(AsShortPath(wcwd, &cwd));
   if (!error_msg.empty()) {
@@ -131,9 +167,8 @@
           /* lpProcessAttributes */ NULL,
           /* lpThreadAttributes */ NULL,
           /* bInheritHandles */ attr_list->InheritAnyHandles() ? TRUE : FALSE,
-          /* dwCreationFlags */ CREATE_NO_WINDOW  // Don't create console
-                                                  // window
-              | CREATE_NEW_PROCESS_GROUP  // So that Ctrl-Break isn't propagated
+          /* dwCreationFlags */ (create_window ? 0 : CREATE_NO_WINDOW) |
+              CREATE_NEW_PROCESS_GROUP  // So that Ctrl-Break isn't propagated
               | CREATE_SUSPENDED  // So that it doesn't start a new job itself
               | EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
           /* lpEnvironment */ env,
diff --git a/src/main/native/windows/process.h b/src/main/native/windows/process.h
index de0e1e4..0f43eda 100644
--- a/src/main/native/windows/process.h
+++ b/src/main/native/windows/process.h
@@ -40,6 +40,9 @@
   WaitableProcess() : pid_(0), exit_code_(STILL_ACTIVE) {}
 
   bool Create(const std::wstring& argv0, const std::wstring& argv_rest,
+              void* env, const std::wstring& wcwd, std::wstring* error);
+
+  bool Create(const std::wstring& argv0, const std::wstring& argv_rest,
               void* env, const std::wstring& wcwd, HANDLE stdin_process,
               HANDLE stdout_process, HANDLE stderr_process,
               LARGE_INTEGER* opt_out_start_time, std::wstring* error);
@@ -54,6 +57,12 @@
   DWORD GetPid() const { return pid_; }
 
  private:
+  bool Create(const std::wstring& argv0, const std::wstring& argv_rest,
+              void* env, const std::wstring& wcwd, HANDLE stdin_process,
+              HANDLE stdout_process, HANDLE stderr_process,
+              LARGE_INTEGER* opt_out_start_time, bool create_window,
+              std::wstring* error);
+
   AutoHandle process_, job_, ioport_;
   DWORD pid_, exit_code_;
 };
diff --git a/src/main/native/windows/util.cc b/src/main/native/windows/util.cc
index 829fd5c..ea3a59b 100644
--- a/src/main/native/windows/util.cc
+++ b/src/main/native/windows/util.cc
@@ -161,18 +161,6 @@
   return reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(data_.get());
 }
 
-void AutoAttributeList::InitStartupInfoExA(STARTUPINFOEXA* startup_info) const {
-  ZeroMemory(startup_info, sizeof(STARTUPINFOEXA));
-  startup_info->StartupInfo.cb = sizeof(STARTUPINFOEXA);
-  if (InheritAnyHandles()) {
-    startup_info->StartupInfo.dwFlags = STARTF_USESTDHANDLES;
-    startup_info->StartupInfo.hStdInput = handles_.StdIn();
-    startup_info->StartupInfo.hStdOutput = handles_.StdOut();
-    startup_info->StartupInfo.hStdError = handles_.StdErr();
-    startup_info->lpAttributeList = *this;
-  }
-}
-
 void AutoAttributeList::InitStartupInfoExW(STARTUPINFOEXW* startup_info) const {
   ZeroMemory(startup_info, sizeof(STARTUPINFOEXW));
   startup_info->StartupInfo.cb = sizeof(STARTUPINFOEXW);
diff --git a/src/main/native/windows/util.h b/src/main/native/windows/util.h
index 2766017..f5bf048 100644
--- a/src/main/native/windows/util.h
+++ b/src/main/native/windows/util.h
@@ -75,8 +75,6 @@
 
   bool InheritAnyHandles() const { return handles_.ValidHandlesCount() > 0; }
 
-  void InitStartupInfoExA(STARTUPINFOEXA* startup_info) const;
-
   void InitStartupInfoExW(STARTUPINFOEXW* startup_info) const;
 
  private: