Skip to content
This repository has been archived by the owner on May 4, 2018. It is now read-only.

win: process trees / setsid? #338

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/win/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ static void uv_init(void) {
/* Initialize FS */
uv_fs_init();

/* Initialize job object */
uv_process_job_init();

/* Initialize console */
uv_console_init();
}
Expand Down
2 changes: 1 addition & 1 deletion src/win/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ void uv_process_proc_exit(uv_loop_t* loop, uv_process_t* handle);
void uv_process_proc_close(uv_loop_t* loop, uv_process_t* handle);
void uv_process_close(uv_loop_t* loop, uv_process_t* handle);
void uv_process_endgame(uv_loop_t* loop, uv_process_t* handle);

void uv_process_job_init();

/*
* C-ares integration
Expand Down
80 changes: 79 additions & 1 deletion src/win/process.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,69 @@ typedef struct env_var {
goto done; \
}

static HANDLE uv_process_job_handle = INVALID_HANDLE_VALUE;

/*
*
* Initialize the job object that will kill child processes when parent dies.
*
* *nix systems behave like this by default, but in Windows, the following is required:
*
* 1) When the process is initialized, a new job object is created with the attribute KILL_ON_JOB_CLOSE.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that you don't go over 80 characters per line

* 2) When a child is spawned, it is associated to that job object.
* 3) Because of KILL_ON_JOB_CLOSE, if the process is terminated, the job object handle will be closed
* and this will cause the child processes to be killed automatically.
*
* NOTES:
* - If the process is started in an environment that does not allow 'breakaway', a new job object will
* not be created and the child processes will automatically be assocaited to the parent job object.
* - The job objects we create allow breakaway so that the chain of job objects can be constructed.
* - In order to associate the child process with the job object, the child needs to be started SUSPENDED,
* associated with the job object, and then resumed.
*
* See http://msdn.microsoft.com/en-us/library/windows/desktop/ms684161(v=vs.85).aspx
*
*/
void uv_process_job_init() {
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits;
BOOL create_job = FALSE;
BOOL is_in_job;

/* Check if we can create a job object - only if parent allows to breakaway */
/* or if there is no job object */
if (!IsProcessInJob(GetCurrentProcess(), NULL, &is_in_job)) {
uv_fatal_error(GetLastError(), "IsProcessInJob");
}
if (!is_in_job) {
create_job = TRUE;
}
else {
if (!QueryInformationJobObject(NULL, JobObjectExtendedLimitInformation, &limits, sizeof(limits), NULL)) {
uv_fatal_error(GetLastError(), "QueryInformationJobObject");
}

create_job =
limits.BasicLimitInformation.LimitFlags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK ||
limits.BasicLimitInformation.LimitFlags & JOB_OBJECT_LIMIT_BREAKAWAY_OK;
}

/* Create the job object with proper attributes */
if (create_job) {
uv_process_job_handle = CreateJobObjectW(NULL, NULL);
if (!uv_process_job_handle) {
uv_fatal_error(GetLastError(), "CreateJobObjectW");
}

RtlZeroMemory(&limits, sizeof(limits));
limits.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | /* Kill child processes when parent closes */
JOB_OBJECT_LIMIT_BREAKAWAY_OK; /* Allow child processes to breakaway from job */

if (!SetInformationJobObject(uv_process_job_handle, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) {
uv_fatal_error(GetLastError(), "SetInformationJobObject");
}
}
}

static void uv_process_init(uv_loop_t* loop, uv_process_t* handle) {
handle->type = UV_PROCESS;
Expand Down Expand Up @@ -704,6 +767,7 @@ void uv_process_proc_exit(uv_loop_t* loop, uv_process_t* handle) {
/* Clean-up the process handle. */
CloseHandle(handle->process_handle);
handle->process_handle = INVALID_HANDLE_VALUE;

} else {
/* We probably left the child stdio handles open to report the error */
/* asynchronously, so close them now. */
Expand Down Expand Up @@ -992,7 +1056,9 @@ int uv_spawn(uv_loop_t* loop, uv_process_t* process,
NULL,
NULL,
1,
CREATE_UNICODE_ENVIRONMENT,
CREATE_UNICODE_ENVIRONMENT |
CREATE_SUSPENDED | /* To assign process to job object after creation */
CREATE_BREAKAWAY_FROM_JOB, /* So child processes can create job objects for their own descendents */
env,
cwd,
&startup,
Expand All @@ -1001,6 +1067,18 @@ int uv_spawn(uv_loop_t* loop, uv_process_t* process,
process->process_handle = info.hProcess;
process->pid = info.dwProcessId;

/* Assign the process to the job object (if we created one) */
if (uv_process_job_handle) {
if (!AssignProcessToJobObject(uv_process_job_handle, process->process_handle)) {
uv_fatal_error(GetLastError(), "AssignProcessToJobObject");
}
}

/* Resume the process, now that it is part of the job object */
if (ResumeThread(info.hThread) == -1) {
uv_fatal_error(GetLastError(), "ResumeThread");
}

if (options.stdin_stream &&
options.stdin_stream->ipc) {
options.stdin_stream->ipc_pid = info.dwProcessId;
Expand Down
44 changes: 43 additions & 1 deletion test/run-tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
#define TEST_TIMEOUT 5000

static int maybe_run_test(int argc, char **argv);

static int spawn_tree_child();

int main(int argc, char **argv) {
platform_init(argc, argv);
Expand Down Expand Up @@ -297,5 +297,47 @@ static int maybe_run_test(int argc, char **argv) {
while (1) uv_sleep(10000);
}

if (strcmp(argv[1], "spawn_tree_child") == 0) {
return spawn_tree_child();
}

if (strcmp(argv[1], "spawn_tree_grandchild") == 0) {
printf("tree: grandchild\n");

/* Wait 5 seconds and assert. The 'child' should kill the 'grandchild' after 200ms */
/* So if the test passes, this code is not reached */
uv_sleep(5000);
ASSERT(0);
}

return run_test(argv[1], TEST_TIMEOUT, 0);
}

static int spawn_tree_child() {
int r;
uv_process_t child;
uv_process_options_t options;
char* args[3];
char program[1024];
size_t program_size = sizeof(program) / sizeof(char);

printf("tree: child\n");

uv_exepath(program, &program_size);
args[0] = program;
args[1] = "spawn_tree_grandchild";
args[2] = NULL;

memset(&options, 0, sizeof(options));
options.file = program;
options.args = args;
r = uv_spawn(uv_default_loop(), &child, options);
ASSERT(r == 0);

uv_sleep(200);
#ifdef _WIN32
return 1;
#else
return 0;
#endif
}
2 changes: 2 additions & 0 deletions test/test-list.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ TEST_DECLARE (spawn_stdin)
TEST_DECLARE (spawn_and_kill)
TEST_DECLARE (spawn_and_kill_with_std)
TEST_DECLARE (spawn_and_ping)
TEST_DECLARE (spawn_tree)
TEST_DECLARE (kill)
TEST_DECLARE (fs_file_noent)
TEST_DECLARE (fs_file_nametoolong)
Expand Down Expand Up @@ -285,6 +286,7 @@ TASK_LIST_START
TEST_ENTRY (spawn_and_kill)
TEST_ENTRY (spawn_and_kill_with_std)
TEST_ENTRY (spawn_and_ping)
TEST_ENTRY (spawn_tree)
TEST_ENTRY (kill)
#ifdef _WIN32
TEST_ENTRY (spawn_detect_pipe_name_collisions_on_windows)
Expand Down
33 changes: 33 additions & 0 deletions test/test-spawn.c
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,39 @@ TEST_IMPL(spawn_and_ping) {
}


TEST_IMPL(spawn_tree) {
int r;

/*
* no_term_signal because child exits properly
*/
no_term_signal = 1;

/* Spawn 'spawn_tree_child' (run-tests.c), which does the following:
*
* 1) Spawn `grandchild` (also in run-tests.c)
* 2) Wait 200ms
* 3) Exit
*
* When the child exits, the grandchild is expected to be killed as well.
* If not, the grandchild will assert after 5 seconds.
*
* (This is not the default behavior in Windows, so Job Objects are used in `uv_spawn`)
*/
init_process_options("spawn_tree_child", kill_cb);
r = uv_spawn(uv_default_loop(), &process, options);
ASSERT(r == 0);

r = uv_run(uv_default_loop());
ASSERT(r == 0);

ASSERT(exit_cb_called == 1);
ASSERT(close_cb_called == 1);

return 0;
}


TEST_IMPL(kill) {
int r;
uv_err_t err;
Expand Down