mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 12:24:24 +00:00
8a43a891f3
Follow-up to b6c5e52af6
that fixes broken test.
TEST=process_inherit_stdio_test
Change-Id: Ice2188af03f3230d0b8d1161fedce333e43b862b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/237482
Reviewed-by: Siva Annamalai <asiva@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
1158 lines
37 KiB
C++
1158 lines
37 KiB
C++
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
#include "platform/globals.h"
|
|
#if defined(DART_HOST_OS_WINDOWS)
|
|
|
|
#include "bin/process.h"
|
|
|
|
#include <process.h> // NOLINT
|
|
#include <psapi.h> // NOLINT
|
|
#include <vector>
|
|
|
|
#include "bin/builtin.h"
|
|
#include "bin/dartutils.h"
|
|
#include "bin/eventhandler.h"
|
|
#include "bin/lockers.h"
|
|
#include "bin/socket.h"
|
|
#include "bin/thread.h"
|
|
#include "bin/utils.h"
|
|
#include "bin/utils_win.h"
|
|
#include "platform/syslog.h"
|
|
|
|
namespace dart {
|
|
namespace bin {
|
|
|
|
static const int kReadHandle = 0;
|
|
static const int kWriteHandle = 1;
|
|
|
|
int Process::global_exit_code_ = 0;
|
|
Mutex* Process::global_exit_code_mutex_ = nullptr;
|
|
Process::ExitHook Process::exit_hook_ = NULL;
|
|
|
|
// ProcessInfo is used to map a process id to the process handle,
|
|
// wait handle for registered exit code event and the pipe used to
|
|
// communicate the exit code of the process to Dart.
|
|
// ProcessInfo objects are kept in the static singly-linked
|
|
// ProcessInfoList.
|
|
class ProcessInfo {
|
|
public:
|
|
ProcessInfo(DWORD process_id,
|
|
HANDLE process_handle,
|
|
HANDLE wait_handle,
|
|
HANDLE exit_pipe)
|
|
: process_id_(process_id),
|
|
process_handle_(process_handle),
|
|
wait_handle_(wait_handle),
|
|
exit_pipe_(exit_pipe) {}
|
|
|
|
~ProcessInfo() {
|
|
BOOL success = CloseHandle(process_handle_);
|
|
if (!success) {
|
|
FATAL("Failed to close process handle");
|
|
}
|
|
success = CloseHandle(exit_pipe_);
|
|
if (!success) {
|
|
FATAL("Failed to close process exit code pipe");
|
|
}
|
|
}
|
|
|
|
DWORD pid() { return process_id_; }
|
|
HANDLE process_handle() { return process_handle_; }
|
|
HANDLE wait_handle() { return wait_handle_; }
|
|
HANDLE exit_pipe() { return exit_pipe_; }
|
|
ProcessInfo* next() { return next_; }
|
|
void set_next(ProcessInfo* next) { next_ = next; }
|
|
|
|
private:
|
|
// Process id.
|
|
DWORD process_id_;
|
|
// Process handle.
|
|
HANDLE process_handle_;
|
|
// Wait handle identifying the exit-code wait operation registered
|
|
// with RegisterWaitForSingleObject.
|
|
HANDLE wait_handle_;
|
|
// File descriptor for pipe to report exit code.
|
|
HANDLE exit_pipe_;
|
|
// Link to next ProcessInfo object in the singly-linked list.
|
|
ProcessInfo* next_;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(ProcessInfo);
|
|
};
|
|
|
|
// Singly-linked list of ProcessInfo objects for all active processes
|
|
// started from Dart.
|
|
class ProcessInfoList {
|
|
public:
|
|
static void Init();
|
|
static void Cleanup();
|
|
|
|
static void AddProcess(DWORD pid, HANDLE handle, HANDLE pipe) {
|
|
// Register a callback to extract the exit code, when the process
|
|
// is signaled. The callback runs in a independent thread from the OS pool.
|
|
// Because the callback depends on the process list containing
|
|
// the process, lock the mutex until the process is added to the list.
|
|
MutexLocker locker(mutex_);
|
|
HANDLE wait_handle = INVALID_HANDLE_VALUE;
|
|
BOOL success = RegisterWaitForSingleObject(
|
|
&wait_handle, handle, &ExitCodeCallback, reinterpret_cast<PVOID>(pid),
|
|
INFINITE, WT_EXECUTEONLYONCE);
|
|
if (!success) {
|
|
FATAL("Failed to register exit code wait operation.");
|
|
}
|
|
ProcessInfo* info = new ProcessInfo(pid, handle, wait_handle, pipe);
|
|
// Mutate the process list under the mutex.
|
|
info->set_next(active_processes_);
|
|
active_processes_ = info;
|
|
}
|
|
|
|
static bool LookupProcess(DWORD pid,
|
|
HANDLE* handle,
|
|
HANDLE* wait_handle,
|
|
HANDLE* pipe) {
|
|
MutexLocker locker(mutex_);
|
|
ProcessInfo* current = active_processes_;
|
|
while (current != NULL) {
|
|
if (current->pid() == pid) {
|
|
*handle = current->process_handle();
|
|
*wait_handle = current->wait_handle();
|
|
*pipe = current->exit_pipe();
|
|
return true;
|
|
}
|
|
current = current->next();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void RemoveProcess(DWORD pid) {
|
|
MutexLocker locker(mutex_);
|
|
ProcessInfo* prev = NULL;
|
|
ProcessInfo* current = active_processes_;
|
|
while (current != NULL) {
|
|
if (current->pid() == pid) {
|
|
if (prev == NULL) {
|
|
active_processes_ = current->next();
|
|
} else {
|
|
prev->set_next(current->next());
|
|
}
|
|
delete current;
|
|
return;
|
|
}
|
|
prev = current;
|
|
current = current->next();
|
|
}
|
|
}
|
|
|
|
private:
|
|
// Callback called when an exit code is available from one of the
|
|
// processes in the list.
|
|
static void CALLBACK ExitCodeCallback(PVOID data, BOOLEAN timed_out) {
|
|
if (timed_out) {
|
|
return;
|
|
}
|
|
DWORD pid = reinterpret_cast<UINT_PTR>(data);
|
|
HANDLE handle;
|
|
HANDLE wait_handle;
|
|
HANDLE exit_pipe;
|
|
bool success = LookupProcess(pid, &handle, &wait_handle, &exit_pipe);
|
|
if (!success) {
|
|
FATAL("Failed to lookup process in list of active processes");
|
|
}
|
|
// Unregister the event in a non-blocking way.
|
|
BOOL ok = UnregisterWait(wait_handle);
|
|
if (!ok && (GetLastError() != ERROR_IO_PENDING)) {
|
|
FATAL("Failed unregistering wait operation");
|
|
}
|
|
// Get and report the exit code to Dart.
|
|
int exit_code;
|
|
ok = GetExitCodeProcess(handle, reinterpret_cast<DWORD*>(&exit_code));
|
|
if (!ok) {
|
|
FATAL1("GetExitCodeProcess failed %d\n", GetLastError());
|
|
}
|
|
int negative = 0;
|
|
if (exit_code < 0) {
|
|
exit_code = abs(exit_code);
|
|
negative = 1;
|
|
}
|
|
int message[2] = {exit_code, negative};
|
|
DWORD written;
|
|
ok = WriteFile(exit_pipe, message, sizeof(message), &written, NULL);
|
|
// If the process has been closed, the read end of the exit
|
|
// pipe has been closed. It is therefore not a problem that
|
|
// WriteFile fails with a closed pipe error
|
|
// (ERROR_NO_DATA). Other errors should not happen.
|
|
if (ok && (written != sizeof(message))) {
|
|
FATAL("Failed to write entire process exit message");
|
|
} else if (!ok && (GetLastError() != ERROR_NO_DATA)) {
|
|
FATAL1("Failed to write exit code: %d", GetLastError());
|
|
}
|
|
// Remove the process from the list of active processes.
|
|
RemoveProcess(pid);
|
|
}
|
|
|
|
// Linked list of ProcessInfo objects for all active processes
|
|
// started from Dart code.
|
|
static ProcessInfo* active_processes_;
|
|
// Mutex protecting all accesses to the linked list of active
|
|
// processes.
|
|
static Mutex* mutex_;
|
|
|
|
DISALLOW_ALLOCATION();
|
|
DISALLOW_IMPLICIT_CONSTRUCTORS(ProcessInfoList);
|
|
};
|
|
|
|
ProcessInfo* ProcessInfoList::active_processes_ = NULL;
|
|
Mutex* ProcessInfoList::mutex_ = nullptr;
|
|
|
|
// Types of pipes to create.
|
|
enum NamedPipeType { kInheritRead, kInheritWrite, kInheritNone };
|
|
|
|
// Create a pipe for communicating with a new process. The handles array
|
|
// will contain the read and write ends of the pipe. Based on the type
|
|
// one of the handles will be inheritable.
|
|
// NOTE: If this function returns false the handles might have been allocated
|
|
// and the caller should make sure to close them in case of an error.
|
|
static bool CreateProcessPipe(HANDLE handles[2],
|
|
wchar_t* pipe_name,
|
|
NamedPipeType type) {
|
|
// Security attributes describing an inheritable handle.
|
|
SECURITY_ATTRIBUTES inherit_handle;
|
|
inherit_handle.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
inherit_handle.bInheritHandle = TRUE;
|
|
inherit_handle.lpSecurityDescriptor = NULL;
|
|
|
|
if (type == kInheritRead) {
|
|
handles[kWriteHandle] =
|
|
CreateNamedPipeW(pipe_name, PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_BYTE | PIPE_WAIT,
|
|
1, // Number of pipes
|
|
1024, // Out buffer size
|
|
1024, // In buffer size
|
|
0, // Timeout in ms
|
|
NULL);
|
|
|
|
if (handles[kWriteHandle] == INVALID_HANDLE_VALUE) {
|
|
Syslog::PrintErr("CreateNamedPipe failed %d\n", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
handles[kReadHandle] =
|
|
CreateFileW(pipe_name, GENERIC_READ, 0, &inherit_handle, OPEN_EXISTING,
|
|
FILE_READ_ATTRIBUTES | FILE_FLAG_OVERLAPPED, NULL);
|
|
if (handles[kReadHandle] == INVALID_HANDLE_VALUE) {
|
|
Syslog::PrintErr("CreateFile failed %d\n", GetLastError());
|
|
return false;
|
|
}
|
|
} else {
|
|
ASSERT((type == kInheritWrite) || (type == kInheritNone));
|
|
handles[kReadHandle] =
|
|
CreateNamedPipeW(pipe_name, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_BYTE | PIPE_WAIT,
|
|
1, // Number of pipes
|
|
1024, // Out buffer size
|
|
1024, // In buffer size
|
|
0, // Timeout in ms
|
|
NULL);
|
|
|
|
if (handles[kReadHandle] == INVALID_HANDLE_VALUE) {
|
|
Syslog::PrintErr("CreateNamedPipe failed %d\n", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
handles[kWriteHandle] = CreateFileW(
|
|
pipe_name, GENERIC_WRITE, 0,
|
|
(type == kInheritWrite) ? &inherit_handle : NULL, OPEN_EXISTING,
|
|
FILE_WRITE_ATTRIBUTES | FILE_FLAG_OVERLAPPED, NULL);
|
|
if (handles[kWriteHandle] == INVALID_HANDLE_VALUE) {
|
|
Syslog::PrintErr("CreateFile failed %d\n", GetLastError());
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void CloseProcessPipe(HANDLE handles[2]) {
|
|
for (int i = kReadHandle; i < kWriteHandle; i++) {
|
|
if (handles[i] != INVALID_HANDLE_VALUE) {
|
|
if (!CloseHandle(handles[i])) {
|
|
Syslog::PrintErr("CloseHandle failed %d\n", GetLastError());
|
|
}
|
|
handles[i] = INVALID_HANDLE_VALUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CloseProcessPipes(HANDLE handles1[2],
|
|
HANDLE handles2[2],
|
|
HANDLE handles3[2],
|
|
HANDLE handles4[2]) {
|
|
CloseProcessPipe(handles1);
|
|
CloseProcessPipe(handles2);
|
|
CloseProcessPipe(handles3);
|
|
CloseProcessPipe(handles4);
|
|
}
|
|
|
|
static int SetOsErrorMessage(char** os_error_message) {
|
|
int error_code = GetLastError();
|
|
const int kMaxMessageLength = 256;
|
|
wchar_t message[kMaxMessageLength];
|
|
FormatMessageIntoBuffer(error_code, message, kMaxMessageLength);
|
|
*os_error_message = StringUtilsWin::WideToUtf8(message);
|
|
return error_code;
|
|
}
|
|
|
|
// Open an inheritable handle to NUL.
|
|
static HANDLE OpenNul() {
|
|
SECURITY_ATTRIBUTES inherit_handle;
|
|
inherit_handle.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
inherit_handle.bInheritHandle = TRUE;
|
|
inherit_handle.lpSecurityDescriptor = NULL;
|
|
HANDLE nul = CreateFile(L"NUL", GENERIC_READ | GENERIC_WRITE, 0,
|
|
&inherit_handle, OPEN_EXISTING, 0, NULL);
|
|
if (nul == INVALID_HANDLE_VALUE) {
|
|
Syslog::PrintErr("CloseHandle failed %d\n", GetLastError());
|
|
}
|
|
return nul;
|
|
}
|
|
|
|
typedef BOOL(WINAPI* InitProcThreadAttrListFn)(LPPROC_THREAD_ATTRIBUTE_LIST,
|
|
DWORD,
|
|
DWORD,
|
|
PSIZE_T);
|
|
|
|
typedef BOOL(WINAPI* UpdateProcThreadAttrFn)(LPPROC_THREAD_ATTRIBUTE_LIST,
|
|
DWORD,
|
|
DWORD_PTR,
|
|
PVOID,
|
|
SIZE_T,
|
|
PVOID,
|
|
PSIZE_T);
|
|
|
|
typedef VOID(WINAPI* DeleteProcThreadAttrListFn)(LPPROC_THREAD_ATTRIBUTE_LIST);
|
|
|
|
static InitProcThreadAttrListFn init_proc_thread_attr_list = NULL;
|
|
static UpdateProcThreadAttrFn update_proc_thread_attr = NULL;
|
|
static DeleteProcThreadAttrListFn delete_proc_thread_attr_list = NULL;
|
|
|
|
static Mutex* initialized_mutex = nullptr;
|
|
static bool load_attempted = false;
|
|
|
|
static bool EnsureInitialized() {
|
|
HMODULE kernel32_module = GetModuleHandleW(L"kernel32.dll");
|
|
if (!load_attempted) {
|
|
MutexLocker locker(initialized_mutex);
|
|
if (load_attempted) {
|
|
return (delete_proc_thread_attr_list != NULL);
|
|
}
|
|
init_proc_thread_attr_list = reinterpret_cast<InitProcThreadAttrListFn>(
|
|
GetProcAddress(kernel32_module, "InitializeProcThreadAttributeList"));
|
|
update_proc_thread_attr = reinterpret_cast<UpdateProcThreadAttrFn>(
|
|
GetProcAddress(kernel32_module, "UpdateProcThreadAttribute"));
|
|
delete_proc_thread_attr_list = reinterpret_cast<DeleteProcThreadAttrListFn>(
|
|
GetProcAddress(kernel32_module, "DeleteProcThreadAttributeList"));
|
|
load_attempted = true;
|
|
return (delete_proc_thread_attr_list != NULL);
|
|
}
|
|
return (delete_proc_thread_attr_list != NULL);
|
|
}
|
|
|
|
const int kMaxPipeNameSize = 80;
|
|
template <int Count>
|
|
static int GenerateNames(wchar_t pipe_names[Count][kMaxPipeNameSize]) {
|
|
UUID uuid;
|
|
RPC_STATUS status = UuidCreateSequential(&uuid);
|
|
if ((status != RPC_S_OK) && (status != RPC_S_UUID_LOCAL_ONLY)) {
|
|
return status;
|
|
}
|
|
RPC_WSTR uuid_string;
|
|
status = UuidToStringW(&uuid, &uuid_string);
|
|
if (status != RPC_S_OK) {
|
|
return status;
|
|
}
|
|
for (int i = 0; i < Count; i++) {
|
|
static const wchar_t* prefix = L"\\\\.\\Pipe\\dart";
|
|
_snwprintf(pipe_names[i], kMaxPipeNameSize, L"%s_%s_%d", prefix,
|
|
uuid_string, i + 1);
|
|
}
|
|
status = RpcStringFreeW(&uuid_string);
|
|
if (status != RPC_S_OK) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
class ProcessStarter {
|
|
public:
|
|
ProcessStarter(const char* path,
|
|
char* arguments[],
|
|
intptr_t arguments_length,
|
|
const char* working_directory,
|
|
char* environment[],
|
|
intptr_t environment_length,
|
|
ProcessStartMode mode,
|
|
intptr_t* in,
|
|
intptr_t* out,
|
|
intptr_t* err,
|
|
intptr_t* id,
|
|
intptr_t* exit_handler,
|
|
char** os_error_message)
|
|
: path_(path),
|
|
working_directory_(working_directory),
|
|
mode_(mode),
|
|
in_(in),
|
|
out_(out),
|
|
err_(err),
|
|
id_(id),
|
|
exit_handler_(exit_handler),
|
|
os_error_message_(os_error_message) {
|
|
stdin_handles_[kReadHandle] = INVALID_HANDLE_VALUE;
|
|
stdin_handles_[kWriteHandle] = INVALID_HANDLE_VALUE;
|
|
stdout_handles_[kReadHandle] = INVALID_HANDLE_VALUE;
|
|
stdout_handles_[kWriteHandle] = INVALID_HANDLE_VALUE;
|
|
stderr_handles_[kReadHandle] = INVALID_HANDLE_VALUE;
|
|
stderr_handles_[kWriteHandle] = INVALID_HANDLE_VALUE;
|
|
exit_handles_[kReadHandle] = INVALID_HANDLE_VALUE;
|
|
exit_handles_[kWriteHandle] = INVALID_HANDLE_VALUE;
|
|
|
|
// Transform input strings to system format.
|
|
const wchar_t* system_path = StringUtilsWin::Utf8ToWide(path_);
|
|
wchar_t** system_arguments;
|
|
system_arguments = reinterpret_cast<wchar_t**>(
|
|
Dart_ScopeAllocate(arguments_length * sizeof(*system_arguments)));
|
|
for (int i = 0; i < arguments_length; i++) {
|
|
system_arguments[i] = StringUtilsWin::Utf8ToWide(arguments[i]);
|
|
}
|
|
|
|
// Compute command-line length.
|
|
int command_line_length = wcslen(system_path);
|
|
for (int i = 0; i < arguments_length; i++) {
|
|
command_line_length += wcslen(system_arguments[i]);
|
|
}
|
|
// Account for null termination and one space per argument.
|
|
command_line_length += arguments_length + 1;
|
|
|
|
// Put together command-line string.
|
|
command_line_ = reinterpret_cast<wchar_t*>(
|
|
Dart_ScopeAllocate(command_line_length * sizeof(*command_line_)));
|
|
int len = 0;
|
|
int remaining = command_line_length;
|
|
int written =
|
|
_snwprintf(command_line_ + len, remaining, L"%s", system_path);
|
|
len += written;
|
|
remaining -= written;
|
|
ASSERT(remaining >= 0);
|
|
for (int i = 0; i < arguments_length; i++) {
|
|
written = _snwprintf(command_line_ + len, remaining, L" %s",
|
|
system_arguments[i]);
|
|
len += written;
|
|
remaining -= written;
|
|
ASSERT(remaining >= 0);
|
|
}
|
|
|
|
// Create environment block if an environment is supplied.
|
|
environment_block_ = NULL;
|
|
if (environment != NULL) {
|
|
wchar_t** system_environment;
|
|
system_environment = reinterpret_cast<wchar_t**>(
|
|
Dart_ScopeAllocate(environment_length * sizeof(*system_environment)));
|
|
// Convert environment strings to system strings.
|
|
for (intptr_t i = 0; i < environment_length; i++) {
|
|
system_environment[i] = StringUtilsWin::Utf8ToWide(environment[i]);
|
|
}
|
|
|
|
// An environment block is a sequence of zero-terminated strings
|
|
// followed by a block-terminating zero char.
|
|
intptr_t block_size = 1;
|
|
for (intptr_t i = 0; i < environment_length; i++) {
|
|
block_size += wcslen(system_environment[i]) + 1;
|
|
}
|
|
environment_block_ = reinterpret_cast<wchar_t*>(
|
|
Dart_ScopeAllocate(block_size * sizeof(*environment_block_)));
|
|
intptr_t block_index = 0;
|
|
for (intptr_t i = 0; i < environment_length; i++) {
|
|
intptr_t len = wcslen(system_environment[i]);
|
|
intptr_t result = _snwprintf(environment_block_ + block_index, len,
|
|
L"%s", system_environment[i]);
|
|
ASSERT(result == len);
|
|
block_index += len;
|
|
environment_block_[block_index++] = '\0';
|
|
}
|
|
// Block-terminating zero char.
|
|
environment_block_[block_index++] = '\0';
|
|
ASSERT(block_index == block_size);
|
|
}
|
|
|
|
system_working_directory_ = NULL;
|
|
if (working_directory_ != NULL) {
|
|
system_working_directory_ =
|
|
StringUtilsWin::Utf8ToWide(working_directory_);
|
|
}
|
|
|
|
attribute_list_ = NULL;
|
|
}
|
|
|
|
~ProcessStarter() {
|
|
if (attribute_list_ != NULL) {
|
|
delete_proc_thread_attr_list(attribute_list_);
|
|
}
|
|
}
|
|
|
|
int Start() {
|
|
// Create pipes required.
|
|
int err = CreatePipes();
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
// Setup info structures.
|
|
STARTUPINFOEXW startup_info;
|
|
ZeroMemory(&startup_info, sizeof(startup_info));
|
|
startup_info.StartupInfo.cb = sizeof(startup_info);
|
|
if (mode_ != kInheritStdio) {
|
|
startup_info.StartupInfo.hStdInput = stdin_handles_[kReadHandle];
|
|
startup_info.StartupInfo.hStdOutput = stdout_handles_[kWriteHandle];
|
|
startup_info.StartupInfo.hStdError = stderr_handles_[kWriteHandle];
|
|
startup_info.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
|
|
|
bool supports_proc_thread_attr_lists = EnsureInitialized();
|
|
if (supports_proc_thread_attr_lists) {
|
|
// Setup the handles to inherit. We only want to inherit the three
|
|
// handles for stdin, stdout and stderr.
|
|
SIZE_T size = 0;
|
|
// The call to determine the size of an attribute list always fails with
|
|
// ERROR_INSUFFICIENT_BUFFER and that error should be ignored.
|
|
if (!init_proc_thread_attr_list(NULL, 1, 0, &size) &&
|
|
(GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
attribute_list_ = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(
|
|
Dart_ScopeAllocate(size));
|
|
ZeroMemory(attribute_list_, size);
|
|
if (!init_proc_thread_attr_list(attribute_list_, 1, 0, &size)) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
inherited_handles_ = {stdin_handles_[kReadHandle],
|
|
stdout_handles_[kWriteHandle],
|
|
stderr_handles_[kWriteHandle]};
|
|
if (!update_proc_thread_attr(
|
|
attribute_list_, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
|
inherited_handles_.data(),
|
|
inherited_handles_.size() * sizeof(HANDLE), NULL, NULL)) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
startup_info.lpAttributeList = attribute_list_;
|
|
}
|
|
}
|
|
|
|
PROCESS_INFORMATION process_info;
|
|
ZeroMemory(&process_info, sizeof(process_info));
|
|
|
|
// Create process.
|
|
DWORD creation_flags =
|
|
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT;
|
|
if (!Process::ModeIsAttached(mode_)) {
|
|
creation_flags |= DETACHED_PROCESS;
|
|
} else {
|
|
// Unless we are inherting stdio which means there is some console
|
|
// associated with the app, we want to ensure no console window pops
|
|
// up for the spawned child.
|
|
if (mode_ != kInheritStdio) {
|
|
// Normally stdout for console dart application is associated with a
|
|
// console that is launched from, but for gui applications(flutter on
|
|
// windows) console might be absent, will be created by CreateProcessW
|
|
// below. When that happens we ensure that console window doesn't
|
|
// pop up.
|
|
creation_flags |= CREATE_NO_WINDOW;
|
|
}
|
|
}
|
|
BOOL result = CreateProcessW(
|
|
NULL, // ApplicationName
|
|
command_line_,
|
|
NULL, // ProcessAttributes
|
|
NULL, // ThreadAttributes
|
|
TRUE, // InheritHandles
|
|
creation_flags, environment_block_, system_working_directory_,
|
|
reinterpret_cast<STARTUPINFOW*>(&startup_info), &process_info);
|
|
|
|
if (result == 0) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
|
|
if (mode_ != kInheritStdio) {
|
|
CloseHandle(stdin_handles_[kReadHandle]);
|
|
CloseHandle(stdout_handles_[kWriteHandle]);
|
|
CloseHandle(stderr_handles_[kWriteHandle]);
|
|
}
|
|
if (Process::ModeIsAttached(mode_)) {
|
|
ProcessInfoList::AddProcess(process_info.dwProcessId,
|
|
process_info.hProcess,
|
|
exit_handles_[kWriteHandle]);
|
|
}
|
|
if (mode_ != kDetached) {
|
|
// Connect the three stdio streams.
|
|
if (Process::ModeHasStdio(mode_)) {
|
|
FileHandle* stdin_handle = new FileHandle(stdin_handles_[kWriteHandle]);
|
|
FileHandle* stdout_handle =
|
|
new FileHandle(stdout_handles_[kReadHandle]);
|
|
FileHandle* stderr_handle =
|
|
new FileHandle(stderr_handles_[kReadHandle]);
|
|
*in_ = reinterpret_cast<intptr_t>(stdout_handle);
|
|
*out_ = reinterpret_cast<intptr_t>(stdin_handle);
|
|
*err_ = reinterpret_cast<intptr_t>(stderr_handle);
|
|
}
|
|
if (Process::ModeIsAttached(mode_)) {
|
|
FileHandle* exit_handle = new FileHandle(exit_handles_[kReadHandle]);
|
|
*exit_handler_ = reinterpret_cast<intptr_t>(exit_handle);
|
|
}
|
|
}
|
|
|
|
CloseHandle(process_info.hThread);
|
|
|
|
// Return process id.
|
|
*id_ = process_info.dwProcessId;
|
|
return 0;
|
|
}
|
|
|
|
int CreatePipes() {
|
|
// Generate unique pipe names for the four named pipes needed.
|
|
wchar_t pipe_names[4][kMaxPipeNameSize];
|
|
int status = GenerateNames<4>(pipe_names);
|
|
if (status != 0) {
|
|
SetOsErrorMessage(os_error_message_);
|
|
Syslog::PrintErr("UuidCreateSequential failed %d\n", status);
|
|
return status;
|
|
}
|
|
|
|
if (mode_ != kDetached) {
|
|
// Open pipes for stdin, stdout, stderr and for communicating the exit
|
|
// code.
|
|
if (Process::ModeHasStdio(mode_)) {
|
|
if (!CreateProcessPipe(stdin_handles_, pipe_names[0], kInheritRead) ||
|
|
!CreateProcessPipe(stdout_handles_, pipe_names[1], kInheritWrite) ||
|
|
!CreateProcessPipe(stderr_handles_, pipe_names[2], kInheritWrite)) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
}
|
|
// Only open exit code pipe for non detached processes.
|
|
if (Process::ModeIsAttached(mode_)) {
|
|
if (!CreateProcessPipe(exit_handles_, pipe_names[3], kInheritNone)) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
}
|
|
} else {
|
|
// Open NUL for stdin, stdout, and stderr.
|
|
stdin_handles_[kReadHandle] = OpenNul();
|
|
if (stdin_handles_[kReadHandle] == INVALID_HANDLE_VALUE) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
|
|
stdout_handles_[kWriteHandle] = OpenNul();
|
|
if (stdout_handles_[kWriteHandle] == INVALID_HANDLE_VALUE) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
|
|
stderr_handles_[kWriteHandle] = OpenNul();
|
|
if (stderr_handles_[kWriteHandle] == INVALID_HANDLE_VALUE) {
|
|
return CleanupAndReturnError();
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int CleanupAndReturnError() {
|
|
int error_code = SetOsErrorMessage(os_error_message_);
|
|
CloseProcessPipes(stdin_handles_, stdout_handles_, stderr_handles_,
|
|
exit_handles_);
|
|
return error_code;
|
|
}
|
|
|
|
HANDLE stdin_handles_[2];
|
|
HANDLE stdout_handles_[2];
|
|
HANDLE stderr_handles_[2];
|
|
HANDLE exit_handles_[2];
|
|
|
|
const wchar_t* system_working_directory_;
|
|
wchar_t* command_line_;
|
|
wchar_t* environment_block_;
|
|
std::vector<HANDLE> inherited_handles_;
|
|
LPPROC_THREAD_ATTRIBUTE_LIST attribute_list_;
|
|
|
|
const char* path_;
|
|
const char* working_directory_;
|
|
ProcessStartMode mode_;
|
|
intptr_t* in_;
|
|
intptr_t* out_;
|
|
intptr_t* err_;
|
|
intptr_t* id_;
|
|
intptr_t* exit_handler_;
|
|
char** os_error_message_;
|
|
|
|
private:
|
|
DISALLOW_ALLOCATION();
|
|
DISALLOW_IMPLICIT_CONSTRUCTORS(ProcessStarter);
|
|
};
|
|
|
|
int Process::Start(Namespace* namespc,
|
|
const char* path,
|
|
char* arguments[],
|
|
intptr_t arguments_length,
|
|
const char* working_directory,
|
|
char* environment[],
|
|
intptr_t environment_length,
|
|
ProcessStartMode mode,
|
|
intptr_t* in,
|
|
intptr_t* out,
|
|
intptr_t* err,
|
|
intptr_t* id,
|
|
intptr_t* exit_handler,
|
|
char** os_error_message) {
|
|
ProcessStarter starter(path, arguments, arguments_length, working_directory,
|
|
environment, environment_length, mode, in, out, err,
|
|
id, exit_handler, os_error_message);
|
|
return starter.Start();
|
|
}
|
|
|
|
class BufferList : public BufferListBase {
|
|
public:
|
|
BufferList() : read_pending_(true) {}
|
|
|
|
// Indicate that data has been read into the buffer provided to
|
|
// overlapped read.
|
|
void DataIsRead(intptr_t size) {
|
|
ASSERT(read_pending_ == true);
|
|
set_data_size(data_size() + size);
|
|
set_free_size(free_size() - size);
|
|
ASSERT(free_size() >= 0);
|
|
read_pending_ = false;
|
|
}
|
|
|
|
// The access to the read buffer for overlapped read.
|
|
bool GetReadBuffer(uint8_t** buffer, intptr_t* size) {
|
|
ASSERT(!read_pending_);
|
|
if (free_size() == 0) {
|
|
if (!Allocate()) {
|
|
return false;
|
|
}
|
|
}
|
|
ASSERT(free_size() > 0);
|
|
ASSERT(free_size() <= kBufferSize);
|
|
*buffer = FreeSpaceAddress();
|
|
*size = free_size();
|
|
read_pending_ = true;
|
|
return true;
|
|
}
|
|
|
|
intptr_t GetDataSize() { return data_size(); }
|
|
|
|
uint8_t* GetFirstDataBuffer() {
|
|
ASSERT(head() != NULL);
|
|
ASSERT(head() == tail());
|
|
ASSERT(data_size() <= kBufferSize);
|
|
return head()->data();
|
|
}
|
|
|
|
void FreeDataBuffer() { Free(); }
|
|
|
|
private:
|
|
bool read_pending_;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(BufferList);
|
|
};
|
|
|
|
class OverlappedHandle {
|
|
public:
|
|
OverlappedHandle() {}
|
|
|
|
void Init(HANDLE handle, HANDLE event) {
|
|
handle_ = handle;
|
|
event_ = event;
|
|
ClearOverlapped();
|
|
}
|
|
|
|
bool HasEvent(HANDLE event) { return (event_ == event); }
|
|
|
|
bool Read() {
|
|
// Get the data read as a result of a completed overlapped operation.
|
|
if (overlapped_.InternalHigh > 0) {
|
|
buffer_.DataIsRead(overlapped_.InternalHigh);
|
|
} else {
|
|
buffer_.DataIsRead(0);
|
|
}
|
|
|
|
// Keep reading until error or pending operation.
|
|
while (true) {
|
|
ClearOverlapped();
|
|
uint8_t* buffer;
|
|
intptr_t buffer_size;
|
|
if (!buffer_.GetReadBuffer(&buffer, &buffer_size)) {
|
|
return false;
|
|
}
|
|
BOOL ok = ReadFile(handle_, buffer, buffer_size, NULL, &overlapped_);
|
|
if (!ok) {
|
|
return (GetLastError() == ERROR_IO_PENDING);
|
|
}
|
|
buffer_.DataIsRead(overlapped_.InternalHigh);
|
|
}
|
|
}
|
|
|
|
Dart_Handle GetData() { return buffer_.GetData(); }
|
|
|
|
intptr_t GetDataSize() { return buffer_.GetDataSize(); }
|
|
|
|
uint8_t* GetFirstDataBuffer() { return buffer_.GetFirstDataBuffer(); }
|
|
|
|
void FreeDataBuffer() { return buffer_.FreeDataBuffer(); }
|
|
|
|
#if defined(DEBUG)
|
|
bool IsEmpty() const { return buffer_.IsEmpty(); }
|
|
#endif
|
|
|
|
void Close() {
|
|
CloseHandle(handle_);
|
|
CloseHandle(event_);
|
|
handle_ = INVALID_HANDLE_VALUE;
|
|
overlapped_.hEvent = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
private:
|
|
void ClearOverlapped() {
|
|
memset(&overlapped_, 0, sizeof(overlapped_));
|
|
overlapped_.hEvent = event_;
|
|
}
|
|
|
|
OVERLAPPED overlapped_;
|
|
HANDLE handle_;
|
|
HANDLE event_;
|
|
BufferList buffer_;
|
|
|
|
DISALLOW_ALLOCATION();
|
|
DISALLOW_COPY_AND_ASSIGN(OverlappedHandle);
|
|
};
|
|
|
|
bool Process::Wait(intptr_t pid,
|
|
intptr_t in,
|
|
intptr_t out,
|
|
intptr_t err,
|
|
intptr_t exit_event,
|
|
ProcessResult* result) {
|
|
// Close input to the process right away.
|
|
reinterpret_cast<FileHandle*>(in)->Close();
|
|
|
|
// All pipes created to the sub-process support overlapped IO.
|
|
FileHandle* stdout_handle = reinterpret_cast<FileHandle*>(out);
|
|
ASSERT(stdout_handle->SupportsOverlappedIO());
|
|
FileHandle* stderr_handle = reinterpret_cast<FileHandle*>(err);
|
|
ASSERT(stderr_handle->SupportsOverlappedIO());
|
|
FileHandle* exit_handle = reinterpret_cast<FileHandle*>(exit_event);
|
|
ASSERT(exit_handle->SupportsOverlappedIO());
|
|
|
|
// Create three events for overlapped IO. These are created as already
|
|
// signalled to ensure they have read called at least once.
|
|
static const int kHandles = 3;
|
|
HANDLE events[kHandles];
|
|
for (int i = 0; i < kHandles; i++) {
|
|
events[i] = CreateEvent(NULL, FALSE, TRUE, NULL);
|
|
}
|
|
|
|
// Setup the structure for handling overlapped IO.
|
|
OverlappedHandle oh[kHandles];
|
|
oh[0].Init(stdout_handle->handle(), events[0]);
|
|
oh[1].Init(stderr_handle->handle(), events[1]);
|
|
oh[2].Init(exit_handle->handle(), events[2]);
|
|
|
|
// Continue until all handles are closed.
|
|
int alive = kHandles;
|
|
while (alive > 0) {
|
|
// Blocking call waiting for events from the child process.
|
|
DWORD wait_result = WaitForMultipleObjects(alive, events, FALSE, INFINITE);
|
|
|
|
// Find the handle signalled.
|
|
int index = wait_result - WAIT_OBJECT_0;
|
|
for (int i = 0; i < kHandles; i++) {
|
|
if (oh[i].HasEvent(events[index])) {
|
|
bool ok = oh[i].Read();
|
|
if (!ok) {
|
|
if (GetLastError() == ERROR_BROKEN_PIPE) {
|
|
oh[i].Close();
|
|
alive--;
|
|
if (index < alive) {
|
|
events[index] = events[alive];
|
|
}
|
|
} else if (err != ERROR_IO_PENDING) {
|
|
DWORD e = GetLastError();
|
|
oh[0].Close();
|
|
oh[1].Close();
|
|
oh[2].Close();
|
|
SetLastError(e);
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// All handles closed and all data read.
|
|
result->set_stdout_data(oh[0].GetData());
|
|
result->set_stderr_data(oh[1].GetData());
|
|
DEBUG_ASSERT(oh[0].IsEmpty());
|
|
DEBUG_ASSERT(oh[1].IsEmpty());
|
|
|
|
// Calculate the exit code.
|
|
ASSERT(oh[2].GetDataSize() == 8);
|
|
uint32_t exit_codes[2];
|
|
memmove(&exit_codes, oh[2].GetFirstDataBuffer(), sizeof(exit_codes));
|
|
oh[2].FreeDataBuffer();
|
|
intptr_t exit_code = exit_codes[0];
|
|
intptr_t negative = exit_codes[1];
|
|
if (negative != 0) {
|
|
exit_code = -exit_code;
|
|
}
|
|
result->set_exit_code(exit_code);
|
|
return true;
|
|
}
|
|
|
|
bool Process::Kill(intptr_t id, int signal) {
|
|
USE(signal); // signal is not used on Windows.
|
|
HANDLE process_handle;
|
|
HANDLE wait_handle;
|
|
HANDLE exit_pipe;
|
|
// First check the process info list for the process to get a handle to it.
|
|
bool success = ProcessInfoList::LookupProcess(id, &process_handle,
|
|
&wait_handle, &exit_pipe);
|
|
// For detached processes we don't have the process registered in the
|
|
// process info list. Try to look it up through the OS.
|
|
if (!success) {
|
|
process_handle = OpenProcess(PROCESS_TERMINATE, FALSE, id);
|
|
// The process is already dead.
|
|
if (process_handle == INVALID_HANDLE_VALUE) {
|
|
return false;
|
|
}
|
|
}
|
|
BOOL result = TerminateProcess(process_handle, -1);
|
|
return result ? true : false;
|
|
}
|
|
|
|
void Process::TerminateExitCodeHandler() {
|
|
// Nothing needs to be done on Windows.
|
|
}
|
|
|
|
intptr_t Process::CurrentProcessId() {
|
|
return static_cast<intptr_t>(GetCurrentProcessId());
|
|
}
|
|
|
|
int64_t Process::CurrentRSS() {
|
|
// Although the documentation at
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getprocessmemoryinfo
|
|
// claims that GetProcessMemoryInfo is UWP compatible, it is actually not
|
|
// hence this function cannot work when compiled in UWP mode.
|
|
#ifdef DART_TARGET_OS_WINDOWS_UWP
|
|
return -1;
|
|
#else
|
|
PROCESS_MEMORY_COUNTERS pmc;
|
|
if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
|
|
return -1;
|
|
}
|
|
return pmc.WorkingSetSize;
|
|
#endif
|
|
}
|
|
|
|
int64_t Process::MaxRSS() {
|
|
#ifdef DART_TARGET_OS_WINDOWS_UWP
|
|
return -1;
|
|
#else
|
|
PROCESS_MEMORY_COUNTERS pmc;
|
|
if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
|
|
return -1;
|
|
}
|
|
return pmc.PeakWorkingSetSize;
|
|
#endif
|
|
}
|
|
|
|
static SignalInfo* signal_handlers = NULL;
|
|
static Mutex* signal_mutex = nullptr;
|
|
|
|
SignalInfo::~SignalInfo() {
|
|
FileHandle* file_handle = reinterpret_cast<FileHandle*>(fd_);
|
|
file_handle->Close();
|
|
file_handle->Release();
|
|
}
|
|
|
|
BOOL WINAPI SignalHandler(DWORD signal) {
|
|
MutexLocker lock(signal_mutex);
|
|
const SignalInfo* handler = signal_handlers;
|
|
bool handled = false;
|
|
while (handler != NULL) {
|
|
if (handler->signal() == signal) {
|
|
int value = 0;
|
|
SocketBase::Write(handler->fd(), &value, 1, SocketBase::kAsync);
|
|
handled = true;
|
|
}
|
|
handler = handler->next();
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
intptr_t GetWinSignal(intptr_t signal) {
|
|
switch (signal) {
|
|
case kSighup:
|
|
return CTRL_CLOSE_EVENT;
|
|
case kSigint:
|
|
return CTRL_C_EVENT;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
intptr_t Process::SetSignalHandler(intptr_t signal) {
|
|
signal = GetWinSignal(signal);
|
|
if (signal == -1) {
|
|
SetLastError(ERROR_NOT_SUPPORTED);
|
|
return -1;
|
|
}
|
|
|
|
// Generate a unique pipe name for the named pipe.
|
|
wchar_t pipe_name[kMaxPipeNameSize];
|
|
int status = GenerateNames<1>(&pipe_name);
|
|
if (status != 0) {
|
|
return status;
|
|
}
|
|
|
|
HANDLE fds[2];
|
|
if (!CreateProcessPipe(fds, pipe_name, kInheritNone)) {
|
|
int error_code = GetLastError();
|
|
CloseProcessPipe(fds);
|
|
SetLastError(error_code);
|
|
return -1;
|
|
}
|
|
MutexLocker lock(signal_mutex);
|
|
FileHandle* write_handle = new FileHandle(fds[kWriteHandle]);
|
|
write_handle->EnsureInitialized(EventHandler::delegate());
|
|
intptr_t write_fd = reinterpret_cast<intptr_t>(write_handle);
|
|
if (signal_handlers == NULL) {
|
|
if (SetConsoleCtrlHandler(SignalHandler, true) == 0) {
|
|
int error_code = GetLastError();
|
|
// Since SetConsoleCtrlHandler failed, the IO completion port will
|
|
// never receive an event for this handle, and will therefore never
|
|
// release the reference Retained by EnsureInitialized(). So, we
|
|
// have to do a second Release() here.
|
|
write_handle->Release();
|
|
write_handle->Release();
|
|
CloseProcessPipe(fds);
|
|
SetLastError(error_code);
|
|
return -1;
|
|
}
|
|
}
|
|
signal_handlers =
|
|
new SignalInfo(write_fd, signal, /*oldact=*/nullptr, signal_handlers);
|
|
return reinterpret_cast<intptr_t>(new FileHandle(fds[kReadHandle]));
|
|
}
|
|
|
|
void Process::ClearSignalHandler(intptr_t signal, Dart_Port port) {
|
|
signal = GetWinSignal(signal);
|
|
if (signal == -1) {
|
|
return;
|
|
}
|
|
MutexLocker lock(signal_mutex);
|
|
SignalInfo* handler = signal_handlers;
|
|
while (handler != NULL) {
|
|
bool remove = false;
|
|
if (handler->signal() == signal) {
|
|
if ((port == ILLEGAL_PORT) || (handler->port() == port)) {
|
|
if (signal_handlers == handler) {
|
|
signal_handlers = handler->next();
|
|
}
|
|
handler->Unlink();
|
|
FileHandle* file_handle = reinterpret_cast<FileHandle*>(handler->fd());
|
|
file_handle->Release();
|
|
remove = true;
|
|
}
|
|
}
|
|
SignalInfo* next = handler->next();
|
|
if (remove) {
|
|
delete handler;
|
|
}
|
|
handler = next;
|
|
}
|
|
if (signal_handlers == NULL) {
|
|
USE(SetConsoleCtrlHandler(SignalHandler, false));
|
|
}
|
|
}
|
|
|
|
void Process::ClearSignalHandlerByFd(intptr_t fd, Dart_Port port) {
|
|
MutexLocker lock(signal_mutex);
|
|
SignalInfo* handler = signal_handlers;
|
|
while (handler != NULL) {
|
|
bool remove = false;
|
|
if (handler->fd() == fd) {
|
|
if ((port == ILLEGAL_PORT) || (handler->port() == port)) {
|
|
if (signal_handlers == handler) {
|
|
signal_handlers = handler->next();
|
|
}
|
|
handler->Unlink();
|
|
FileHandle* file_handle = reinterpret_cast<FileHandle*>(handler->fd());
|
|
file_handle->Release();
|
|
remove = true;
|
|
}
|
|
}
|
|
SignalInfo* next = handler->next();
|
|
if (remove) {
|
|
delete handler;
|
|
}
|
|
handler = next;
|
|
}
|
|
if (signal_handlers == NULL) {
|
|
USE(SetConsoleCtrlHandler(SignalHandler, false));
|
|
}
|
|
}
|
|
|
|
void ProcessInfoList::Init() {
|
|
active_processes_ = nullptr;
|
|
ASSERT(ProcessInfoList::mutex_ == nullptr);
|
|
ProcessInfoList::mutex_ = new Mutex();
|
|
}
|
|
|
|
void ProcessInfoList::Cleanup() {
|
|
ASSERT(ProcessInfoList::mutex_ != nullptr);
|
|
delete ProcessInfoList::mutex_;
|
|
ProcessInfoList::mutex_ = nullptr;
|
|
}
|
|
|
|
void Process::Init() {
|
|
ProcessInfoList::Init();
|
|
|
|
signal_handlers = NULL;
|
|
ASSERT(signal_mutex == nullptr);
|
|
signal_mutex = new Mutex();
|
|
|
|
ASSERT(initialized_mutex == nullptr);
|
|
initialized_mutex = new Mutex();
|
|
load_attempted = false;
|
|
|
|
ASSERT(Process::global_exit_code_mutex_ == nullptr);
|
|
Process::global_exit_code_mutex_ = new Mutex();
|
|
}
|
|
|
|
void Process::Cleanup() {
|
|
ClearAllSignalHandlers();
|
|
|
|
ASSERT(signal_mutex != nullptr);
|
|
delete signal_mutex;
|
|
signal_mutex = nullptr;
|
|
|
|
ASSERT(initialized_mutex != nullptr);
|
|
delete initialized_mutex;
|
|
initialized_mutex = nullptr;
|
|
|
|
ASSERT(Process::global_exit_code_mutex_ != nullptr);
|
|
delete Process::global_exit_code_mutex_;
|
|
Process::global_exit_code_mutex_ = nullptr;
|
|
|
|
ProcessInfoList::Cleanup();
|
|
}
|
|
|
|
} // namespace bin
|
|
} // namespace dart
|
|
|
|
#endif // defined(DART_HOST_OS_WINDOWS)
|