1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements. See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #include "win32/apr_arch_threadproc.h"
18 #include "win32/apr_arch_file_io.h"
20 #include "apr_thread_proc.h"
21 #include "apr_file_io.h"
22 #include "apr_general.h"
23 #include "apr_strings.h"
24 #include "apr_portable.h"
31 #if APR_HAVE_PROCESS_H
35 /* We have very carefully excluded volumes of definitions from the
36 * Microsoft Platform SDK, which kill the build time performance.
37 * These the sole constants we borrow from WinBase.h and WinUser.h
39 #ifndef LOGON32_LOGON_NETWORK
40 #define LOGON32_LOGON_NETWORK 3
44 #ifndef DETACHED_PROCESS
45 #define DETACHED_PROCESS 0
47 #ifndef CREATE_UNICODE_ENVIRONMENT
48 #define CREATE_UNICODE_ENVIRONMENT 0
50 #ifndef STARTF_USESHOWWINDOW
51 #define STARTF_USESHOWWINDOW 0
58 * some of the ideas expressed herein are based off of Microsoft
59 * Knowledge Base article: Q190351
63 APR_DECLARE(apr_status_t) apr_procattr_create(apr_procattr_t **new,
66 (*new) = (apr_procattr_t *)apr_pcalloc(pool, sizeof(apr_procattr_t));
68 (*new)->cmdtype = APR_PROGRAM;
72 APR_DECLARE(apr_status_t) apr_procattr_io_set(apr_procattr_t *attr,
77 apr_status_t stat = APR_SUCCESS;
80 /* APR_CHILD_BLOCK maps to APR_WRITE_BLOCK, while
81 * APR_PARENT_BLOCK maps to APR_READ_BLOCK, so we
82 * must transpose the CHILD/PARENT blocking flags
83 * only for the stdin pipe. stdout/stderr naturally
84 * map to the correct mode.
86 if (in == APR_CHILD_BLOCK)
88 else if (in == APR_PARENT_BLOCK)
91 stat = apr_create_nt_pipe(&attr->child_in, &attr->parent_in,
93 if (stat == APR_SUCCESS)
94 stat = apr_file_inherit_unset(attr->parent_in);
96 if (out && stat == APR_SUCCESS) {
97 stat = apr_create_nt_pipe(&attr->parent_out, &attr->child_out,
99 if (stat == APR_SUCCESS)
100 stat = apr_file_inherit_unset(attr->parent_out);
102 if (err && stat == APR_SUCCESS) {
103 stat = apr_create_nt_pipe(&attr->parent_err, &attr->child_err,
105 if (stat == APR_SUCCESS)
106 stat = apr_file_inherit_unset(attr->parent_err);
111 APR_DECLARE(apr_status_t) apr_procattr_child_in_set(apr_procattr_t *attr,
112 apr_file_t *child_in,
113 apr_file_t *parent_in)
115 apr_status_t rv = APR_SUCCESS;
118 if (attr->child_in == NULL)
119 rv = apr_file_dup(&attr->child_in, child_in, attr->pool);
121 rv = apr_file_dup2(attr->child_in, child_in, attr->pool);
123 if (rv == APR_SUCCESS)
124 rv = apr_file_inherit_set(attr->child_in);
127 if (parent_in && rv == APR_SUCCESS) {
128 if (attr->parent_in == NULL)
129 rv = apr_file_dup(&attr->parent_in, parent_in, attr->pool);
131 rv = apr_file_dup2(attr->parent_in, parent_in, attr->pool);
137 APR_DECLARE(apr_status_t) apr_procattr_child_out_set(apr_procattr_t *attr,
138 apr_file_t *child_out,
139 apr_file_t *parent_out)
141 apr_status_t rv = APR_SUCCESS;
144 if (attr->child_out == NULL)
145 rv = apr_file_dup(&attr->child_out, child_out, attr->pool);
147 rv = apr_file_dup2(attr->child_out, child_out, attr->pool);
149 if (rv == APR_SUCCESS)
150 rv = apr_file_inherit_set(attr->child_out);
153 if (parent_out && rv == APR_SUCCESS) {
154 if (attr->parent_out == NULL)
155 rv = apr_file_dup(&attr->parent_out, parent_out, attr->pool);
157 rv = apr_file_dup2(attr->parent_out, parent_out, attr->pool);
163 APR_DECLARE(apr_status_t) apr_procattr_child_err_set(apr_procattr_t *attr,
164 apr_file_t *child_err,
165 apr_file_t *parent_err)
167 apr_status_t rv = APR_SUCCESS;
170 if (attr->child_err == NULL)
171 rv = apr_file_dup(&attr->child_err, child_err, attr->pool);
173 rv = apr_file_dup2(attr->child_err, child_err, attr->pool);
175 if (rv == APR_SUCCESS)
176 rv = apr_file_inherit_set(attr->child_err);
179 if (parent_err && rv == APR_SUCCESS) {
180 if (attr->parent_err == NULL)
181 rv = apr_file_dup(&attr->parent_err, parent_err, attr->pool);
183 rv = apr_file_dup2(attr->parent_err, parent_err, attr->pool);
189 APR_DECLARE(apr_status_t) apr_procattr_dir_set(apr_procattr_t *attr,
192 /* curr dir must be in native format, there are all sorts of bugs in
193 * the NT library loading code that flunk the '/' parsing test.
195 return apr_filepath_merge(&attr->currdir, NULL, dir,
196 APR_FILEPATH_NATIVE, attr->pool);
199 APR_DECLARE(apr_status_t) apr_procattr_cmdtype_set(apr_procattr_t *attr,
206 APR_DECLARE(apr_status_t) apr_procattr_detach_set(apr_procattr_t *attr,
209 attr->detached = det;
213 static const char* has_space(const char *str)
216 for (ch = str; *ch; ++ch) {
217 if (apr_isspace(*ch)) {
224 static char *apr_caret_escape_args(apr_pool_t *p, const char *str)
228 const unsigned char *s;
230 cmd = apr_palloc(p, 2 * strlen(str) + 1); /* Be safe */
231 d = (unsigned char *)cmd;
232 s = (const unsigned char *)str;
236 * Newlines to Win32/OS2 CreateProcess() are ill advised.
237 * Convert them to spaces since they are effectively white
238 * space to most applications
240 if (*s == '\r' || *s == '\n') {
255 APR_DECLARE(apr_status_t) apr_procattr_child_errfn_set(apr_procattr_t *attr,
256 apr_child_errfn_t *errfn)
262 APR_DECLARE(apr_status_t) apr_procattr_error_check_set(apr_procattr_t *attr,
269 APR_DECLARE(apr_status_t) apr_procattr_addrspace_set(apr_procattr_t *attr,
270 apr_int32_t addrspace)
272 /* won't ever be used on this platform, so don't save the flag */
276 #if APR_HAS_UNICODE_FS && !defined(_WIN32_WCE)
278 /* Used only for the NT code path, a critical section is the fastest
279 * implementation available.
281 static CRITICAL_SECTION proc_lock;
283 static apr_status_t threadproc_global_cleanup(void *ignored)
285 DeleteCriticalSection(&proc_lock);
289 /* Called from apr_initialize, we need a critical section to handle
290 * the pipe inheritance on win32. This will mutex any process create
291 * so as we change our inherited pipes, we prevent another process from
292 * also inheriting those alternate handles, and prevent the other process
293 * from failing to inherit our standard handles.
295 apr_status_t apr_threadproc_init(apr_pool_t *pool)
299 InitializeCriticalSection(&proc_lock);
300 /* register the cleanup */
301 apr_pool_cleanup_register(pool, &proc_lock,
302 threadproc_global_cleanup,
303 apr_pool_cleanup_null);
308 #else /* !APR_HAS_UNICODE_FS || defined(_WIN32_WCE) */
310 apr_status_t apr_threadproc_init(apr_pool_t *pool)
317 APR_DECLARE(apr_status_t) apr_proc_create(apr_proc_t *new,
318 const char *progname,
319 const char * const *args,
320 const char * const *env,
321 apr_procattr_t *attr,
329 PROCESS_INFORMATION pi;
330 DWORD dwCreationFlags = 0;
332 new->in = attr->parent_in;
333 new->out = attr->parent_out;
334 new->err = attr->parent_err;
336 if (attr->detached) {
337 /* If we are creating ourselves detached, Then we should hide the
338 * window we are starting in. And we had better redfine our
339 * handles for STDIN, STDOUT, and STDERR. Do not set the
340 * detached attribute for Win9x. We have found that Win9x does
341 * not manage the stdio handles properly when running old 16
342 * bit executables if the detached attribute is set.
344 if (apr_os_level >= APR_WIN_NT) {
346 * XXX DETACHED_PROCESS won't on Win9x at all; on NT/W2K
347 * 16 bit executables fail (MS KB: Q150956)
349 dwCreationFlags |= DETACHED_PROCESS;
353 /* progname must be unquoted, in native format, as there are all sorts
354 * of bugs in the NT library loader code that fault when parsing '/'.
355 * XXX progname must be NULL if this is a 16 bit app running in WOW
357 if (progname[0] == '\"') {
358 progname = apr_pstrndup(pool, progname + 1, strlen(progname) - 2);
361 if (attr->cmdtype == APR_PROGRAM || attr->cmdtype == APR_PROGRAM_ENV) {
362 char *fullpath = NULL;
363 if ((rv = apr_filepath_merge(&fullpath, attr->currdir, progname,
364 APR_FILEPATH_NATIVE, pool)) != APR_SUCCESS) {
366 attr->errfn(pool, rv,
367 apr_pstrcat(pool, "filepath_merge failed.",
368 " currdir: ", attr->currdir,
369 " progname: ", progname,NULL));
376 /* Do not fail if the path isn't parseable for APR_PROGRAM_PATH
377 * or APR_SHELLCMD. We only invoke apr_filepath_merge (with no
378 * left hand side expression) in order to correct the path slash
379 * delimiters. But the filename doesn't need to be in the CWD,
380 * nor does it need to be a filename at all (it could be a
381 * built-in shell command.)
383 char *fullpath = NULL;
384 if ((rv = apr_filepath_merge(&fullpath, "", progname,
385 APR_FILEPATH_NATIVE, pool)) == APR_SUCCESS) {
390 if (has_space(progname)) {
391 argv0 = apr_pstrcat(pool, "\"", progname, "\"", NULL);
397 /* Handle the args, seperate from argv0 */
399 for (i = 1; args && args[i]; ++i) {
400 if (has_space(args[i]) || !args[i][0]) {
401 cmdline = apr_pstrcat(pool, cmdline, " \"", args[i], "\"", NULL);
404 cmdline = apr_pstrcat(pool, cmdline, " ", args[i], NULL);
409 if (attr->cmdtype == APR_SHELLCMD || attr->cmdtype == APR_SHELLCMD_ENV) {
410 char *shellcmd = getenv("COMSPEC");
413 attr->errfn(pool, APR_EINVAL, "COMSPEC envar is not set");
417 if (shellcmd[0] == '"') {
418 progname = apr_pstrndup(pool, shellcmd + 1, strlen(shellcmd) - 2);
422 if (has_space(shellcmd)) {
423 shellcmd = apr_pstrcat(pool, "\"", shellcmd, "\"", NULL);
426 /* Command.com does not support a quoted command, while cmd.exe demands one.
428 i = strlen(progname);
429 if (i >= 11 && strcasecmp(progname + i - 11, "command.com") == 0) {
430 cmdline = apr_pstrcat(pool, shellcmd, " /C ", argv0, cmdline, NULL);
433 cmdline = apr_pstrcat(pool, shellcmd, " /C \"", argv0, cmdline, "\"", NULL);
439 #if defined(_WIN32_WCE)
442 /* Win32 is _different_ than unix. While unix will find the given
443 * program since it's already chdir'ed, Win32 cannot since the parent
444 * attempts to open the program with it's own path.
445 * ###: This solution isn't much better - it may defeat path searching
446 * when the path search was desired. Open to further discussion.
448 i = strlen(progname);
449 if (i >= 4 && (strcasecmp(progname + i - 4, ".bat") == 0
450 || strcasecmp(progname + i - 4, ".cmd") == 0))
452 char *shellcmd = getenv("COMSPEC");
455 attr->errfn(pool, APR_EINVAL, "COMSPEC envar is not set");
459 if (shellcmd[0] == '"') {
460 progname = apr_pstrndup(pool, shellcmd + 1, strlen(shellcmd) - 2);
464 if (has_space(shellcmd)) {
465 shellcmd = apr_pstrcat(pool, "\"", shellcmd, "\"", NULL);
468 i = strlen(progname);
469 if (i >= 11 && strcasecmp(progname + i - 11, "command.com") == 0) {
470 /* XXX: Still insecure - need doubled-quotes on each individual
471 * arg of cmdline. Suspect we need to postpone cmdline parsing
472 * until this moment in all four code paths, with some flags
473 * to toggle 'which flavor' is needed.
475 cmdline = apr_pstrcat(pool, shellcmd, " /C ", argv0, cmdline, NULL);
478 /* We must protect the cmdline args from any interpolation - this
479 * is not a shellcmd, and the source of argv[] is untrusted.
480 * Notice we escape ALL the cmdline args, including the quotes
481 * around the individual args themselves. No sense in allowing
482 * the shift-state to be toggled, and the application will
483 * not see the caret escapes.
485 cmdline = apr_caret_escape_args(pool, cmdline);
487 * Our app name must always be quoted so the quotes surrounding
488 * the entire /c "command args" are unambigious.
491 cmdline = apr_pstrcat(pool, shellcmd, " /C \"\"", argv0, "\"", cmdline, "\"", NULL);
494 cmdline = apr_pstrcat(pool, shellcmd, " /C \"", argv0, cmdline, "\"", NULL);
500 /* A simple command we are directly invoking. Do not pass
501 * the first arg to CreateProc() for APR_PROGRAM_PATH
502 * invocation, since it would need to be a literal and
503 * complete file path. That is; "c:\bin\aprtest.exe"
504 * would succeed, but "c:\bin\aprtest" or "aprtest.exe"
507 cmdline = apr_pstrcat(pool, argv0, cmdline, NULL);
509 if (attr->cmdtype == APR_PROGRAM_PATH) {
515 if (!env || attr->cmdtype == APR_PROGRAM_ENV ||
516 attr->cmdtype == APR_SHELLCMD_ENV) {
520 apr_size_t iEnvBlockLen;
522 * Win32's CreateProcess call requires that the environment
523 * be passed in an environment block, a null terminated block of
524 * null terminated strings.
529 iEnvBlockLen += strlen(env[i]) + 1;
535 #if APR_HAS_UNICODE_FS
539 pEnvBlock = (char *)apr_palloc(pool, iEnvBlockLen * 2);
540 dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
543 pNext = (apr_wchar_t*)pEnvBlock;
545 apr_size_t in = strlen(env[i]) + 1;
546 if ((rv = apr_conv_utf8_to_ucs2(env[i], &in,
547 pNext, &iEnvBlockLen))
550 attr->errfn(pool, rv,
552 "utf8 to ucs2 conversion failed"
553 " on this string: ", env[i], NULL));
557 pNext = wcschr(pNext, L'\0') + 1;
564 #endif /* APR_HAS_UNICODE_FS */
569 pEnvBlock = (char *)apr_palloc(pool, iEnvBlockLen);
574 strcpy(pNext, env[i]);
575 pNext = strchr(pNext, '\0') + 1;
582 #endif /* APR_HAS_ANSI_FS */
585 new->invoked = cmdline;
587 #if APR_HAS_UNICODE_FS
591 DWORD stdin_reset = 0;
592 DWORD stdout_reset = 0;
593 DWORD stderr_reset = 0;
594 apr_wchar_t *wprg = NULL;
595 apr_wchar_t *wcmd = NULL;
596 apr_wchar_t *wcwd = NULL;
599 apr_size_t nprg = strlen(progname) + 1;
600 apr_size_t nwprg = nprg + 6;
601 wprg = apr_palloc(pool, nwprg * sizeof(wprg[0]));
602 if ((rv = apr_conv_utf8_to_ucs2(progname, &nprg, wprg, &nwprg))
605 attr->errfn(pool, rv,
607 "utf8 to ucs2 conversion failed"
608 " on progname: ", progname, NULL));
615 apr_size_t ncmd = strlen(cmdline) + 1;
616 apr_size_t nwcmd = ncmd;
617 wcmd = apr_palloc(pool, nwcmd * sizeof(wcmd[0]));
618 if ((rv = apr_conv_utf8_to_ucs2(cmdline, &ncmd, wcmd, &nwcmd))
621 attr->errfn(pool, rv,
623 "utf8 to ucs2 conversion failed"
624 " on cmdline: ", cmdline, NULL));
632 apr_size_t ncwd = strlen(attr->currdir) + 1;
633 apr_size_t nwcwd = ncwd;
634 wcwd = apr_palloc(pool, ncwd * sizeof(wcwd[0]));
635 if ((rv = apr_conv_utf8_to_ucs2(attr->currdir, &ncwd,
639 attr->errfn(pool, rv,
641 "utf8 to ucs2 conversion failed"
642 " on currdir: ", attr->currdir, NULL));
648 memset(&si, 0, sizeof(si));
651 if (attr->detached) {
652 si.dwFlags |= STARTF_USESHOWWINDOW;
653 si.wShowWindow = SW_HIDE;
657 /* LOCK CRITICAL SECTION
658 * before we begin to manipulate the inherited handles
660 EnterCriticalSection(&proc_lock);
662 if ((attr->child_in && attr->child_in->filehand)
663 || (attr->child_out && attr->child_out->filehand)
664 || (attr->child_err && attr->child_err->filehand))
666 si.dwFlags |= STARTF_USESTDHANDLES;
668 si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
669 if (GetHandleInformation(si.hStdInput, &stdin_reset)
670 && (stdin_reset &= HANDLE_FLAG_INHERIT))
671 SetHandleInformation(si.hStdInput,
672 HANDLE_FLAG_INHERIT, 0);
674 if (attr->child_in && attr->child_in->filehand)
676 si.hStdInput = attr->child_in->filehand;
677 SetHandleInformation(si.hStdInput, HANDLE_FLAG_INHERIT,
678 HANDLE_FLAG_INHERIT);
681 si.hStdInput = INVALID_HANDLE_VALUE;
683 si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
684 if (GetHandleInformation(si.hStdOutput, &stdout_reset)
685 && (stdout_reset &= HANDLE_FLAG_INHERIT))
686 SetHandleInformation(si.hStdOutput,
687 HANDLE_FLAG_INHERIT, 0);
689 if (attr->child_out && attr->child_out->filehand)
691 si.hStdOutput = attr->child_out->filehand;
692 SetHandleInformation(si.hStdOutput, HANDLE_FLAG_INHERIT,
693 HANDLE_FLAG_INHERIT);
696 si.hStdOutput = INVALID_HANDLE_VALUE;
698 si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
699 if (GetHandleInformation(si.hStdError, &stderr_reset)
700 && (stderr_reset &= HANDLE_FLAG_INHERIT))
701 SetHandleInformation(si.hStdError,
702 HANDLE_FLAG_INHERIT, 0);
704 if (attr->child_err && attr->child_err->filehand)
706 si.hStdError = attr->child_err->filehand;
707 SetHandleInformation(si.hStdError, HANDLE_FLAG_INHERIT,
708 HANDLE_FLAG_INHERIT);
711 si.hStdError = INVALID_HANDLE_VALUE;
713 rv = CreateProcessW(wprg, wcmd, /* Executable & Command line */
714 NULL, NULL, /* Proc & thread security attributes */
715 TRUE, /* Inherit handles */
716 dwCreationFlags, /* Creation flags */
717 pEnvBlock, /* Environment block */
718 wcwd, /* Current directory name */
721 if ((attr->child_in && attr->child_in->filehand)
722 || (attr->child_out && attr->child_out->filehand)
723 || (attr->child_err && attr->child_err->filehand))
726 SetHandleInformation(GetStdHandle(STD_INPUT_HANDLE),
727 stdin_reset, stdin_reset);
730 SetHandleInformation(GetStdHandle(STD_OUTPUT_HANDLE),
731 stdout_reset, stdout_reset);
734 SetHandleInformation(GetStdHandle(STD_ERROR_HANDLE),
735 stderr_reset, stderr_reset);
737 /* RELEASE CRITICAL SECTION
738 * The state of the inherited handles has been restored.
740 LeaveCriticalSection(&proc_lock);
742 #else /* defined(_WIN32_WCE) */
743 rv = CreateProcessW(wprg, wcmd, /* Executable & Command line */
744 NULL, NULL, /* Proc & thread security attributes */
745 FALSE, /* must be 0 */
746 dwCreationFlags, /* Creation flags */
747 NULL, /* Environment block must be NULL */
748 NULL, /* Current directory name must be NULL*/
749 NULL, /* STARTUPINFO not supported */
753 #endif /* APR_HAS_UNICODE_FS */
758 memset(&si, 0, sizeof(si));
761 if (attr->detached) {
762 si.dwFlags |= STARTF_USESHOWWINDOW;
763 si.wShowWindow = SW_HIDE;
766 if ((attr->child_in && attr->child_in->filehand)
767 || (attr->child_out && attr->child_out->filehand)
768 || (attr->child_err && attr->child_err->filehand))
770 si.dwFlags |= STARTF_USESTDHANDLES;
772 si.hStdInput = (attr->child_in)
773 ? attr->child_in->filehand
774 : GetStdHandle(STD_INPUT_HANDLE);
776 si.hStdOutput = (attr->child_out)
777 ? attr->child_out->filehand
778 : GetStdHandle(STD_OUTPUT_HANDLE);
780 si.hStdError = (attr->child_err)
781 ? attr->child_err->filehand
782 : GetStdHandle(STD_ERROR_HANDLE);
785 rv = CreateProcessA(progname, cmdline, /* Command line */
786 NULL, NULL, /* Proc & thread security attributes */
787 TRUE, /* Inherit handles */
788 dwCreationFlags, /* Creation flags */
789 pEnvBlock, /* Environment block */
790 attr->currdir, /* Current directory name */
793 #endif /* APR_HAS_ANSI_FS */
795 /* Check CreateProcess result
798 return apr_get_os_error();
800 /* XXX Orphaned handle warning - no fix due to broken apr_proc_t api.
802 new->hproc = pi.hProcess;
803 new->pid = pi.dwProcessId;
805 if (attr->child_in) {
806 apr_file_close(attr->child_in);
808 if (attr->child_out) {
809 apr_file_close(attr->child_out);
811 if (attr->child_err) {
812 apr_file_close(attr->child_err);
814 CloseHandle(pi.hThread);
819 APR_DECLARE(apr_status_t) apr_proc_wait_all_procs(apr_proc_t *proc,
821 apr_exit_why_e *exitwhy,
822 apr_wait_how_e waithow,
825 /* Unix does apr_proc_wait(proc(-1), exitcode, exitwhy, waithow)
826 * but Win32's apr_proc_wait won't work that way. We can either
827 * register all APR created processes in some sort of AsyncWait
828 * thread, or simply walk from the global process pool for all
829 * apr_pool_note_subprocess()es registered with APR.
834 static apr_exit_why_e why_from_exit_code(DWORD exit) {
835 /* See WinNT.h STATUS_ACCESS_VIOLATION and family for how
836 * this class of failures was determined
838 if (((exit & 0xC0000000) == 0xC0000000)
839 && !(exit & 0x3FFF0000))
840 return APR_PROC_SIGNAL;
842 return APR_PROC_EXIT;
844 /* ### No way to tell if Dr Watson grabbed a core, AFAICT. */
847 APR_DECLARE(apr_status_t) apr_proc_wait(apr_proc_t *proc,
848 int *exitcode, apr_exit_why_e *exitwhy,
849 apr_wait_how_e waithow)
854 if (waithow == APR_WAIT)
859 if ((stat = WaitForSingleObject(proc->hproc, time)) == WAIT_OBJECT_0) {
860 if (GetExitCodeProcess(proc->hproc, &stat)) {
864 *exitwhy = why_from_exit_code(stat);
865 CloseHandle(proc->hproc);
867 return APR_CHILD_DONE;
870 else if (stat == WAIT_TIMEOUT) {
871 return APR_CHILD_NOTDONE;
873 return apr_get_os_error();