diff options
Diffstat (limited to 'src/kmk/w32/winchildren.c')
-rw-r--r-- | src/kmk/w32/winchildren.c | 3729 |
1 files changed, 3729 insertions, 0 deletions
diff --git a/src/kmk/w32/winchildren.c b/src/kmk/w32/winchildren.c new file mode 100644 index 0000000..635fe99 --- /dev/null +++ b/src/kmk/w32/winchildren.c @@ -0,0 +1,3729 @@ +/* $Id: winchildren.c 3359 2020-06-05 16:17:17Z bird $ */ +/** @file + * Child process creation and management for kmk. + */ + +/* + * Copyright (c) 2018 knut st. osmundsen <bird-kBuild-spamx@anduin.net> + * + * This file is part of kBuild. + * + * kBuild is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * kBuild is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with kBuild. If not, see <http://www.gnu.org/licenses/> + * + */ + +/* No GNU coding style here atm, convert if upstreamed. */ + +/** @page pg_win_children Windows child process creation and managment + * + * This new implementation aims at addressing the following: + * + * 1. Speed up process creation by doing the expensive CreateProcess call + * in a worker thread. + * + * 2. No 64 process limit imposed by WaitForMultipleObjects. + * + * 3. Better distribute jobs among processor groups. + * + * 4. Offloading more expensive kmkbuiltin operations to worker threads, + * making the main thread focus on managing child processes. + * + * 5. Output synchronization using reusable pipes. + * + * + * To be quite honest, the first item (CreateProcess expense) didn't occur to me + * at first and was more of a sideeffect discovered along the way. A test + * rebuilding IPRT went from 4m52s to 3m19s on a 8 thread system. + * + * The 2nd and 3rd goals are related to newer build servers that have lots of + * CPU threads and various Windows NT (aka NT OS/2 at the time) design choices + * made in the late 1980ies. + * + * WaitForMultipleObjects does not support waiting for more than 64 objects, + * unlike poll and select. This is just something everyone ends up having to + * work around in the end. + * + * Affinity masks are uintptr_t sized, so 64-bit hosts can only manage 64 + * processors and 32-bit only 32. Workaround was introduced with Windows 7 + * (IIRC) and is called processor groups. The CPU threads are grouped into 1 or + * more groups of up to 64 processors. Processes are generally scheduled to a + * signle processor group at first, but threads may be changed to be scheduled + * on different groups. This code will try distribute children evenly among the + * processor groups, using a very simple algorithm (see details in code). + * + * + * @section sec_win_children_av Remarks on Microsoft Defender and other AV + * + * Part of the motivation for writing this code was horrible CPU utilization on + * a brand new AMD Threadripper 1950X system with lots of memory and SSDs, + * running 64-bit Windows 10 build 16299. + * + * Turns out Microsoft defender adds some overhead to CreateProcess + * and other stuff: + * - Old make with CreateProcess on main thread: + * - With runtime defender enabled: 14 min 6 seconds + * - With runtime defender disabled: 4 min 49 seconds + * - New make with CreateProcess on worker thread (this code): + * - With runtime defender enabled: 6 min 29 seconds + * - With runtime defender disabled: 4 min 36 seconds + * - With runtime defender disabled out dir only: 5 min 59 seconds + * + * See also kWorker / kSubmit for more bickering about AV & disk encryption. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <Windows.h> +#include <Winternl.h> + +#include "../makeint.h" +#include "../job.h" +#include "../filedef.h" +#include "../debug.h" +#include "../kmkbuiltin.h" +#include "winchildren.h" + +#include <assert.h> +#include <process.h> +#include <intrin.h> + +#include "nt/nt_child_inject_standard_handles.h" +#include "console.h" + +#ifndef KMK_BUILTIN_STANDALONE +extern void kmk_cache_exec_image_w(const wchar_t *); /* imagecache.c */ +#endif + +/* Option values from main.c: */ +extern const char *win_job_object_mode; +extern const char *win_job_object_name; +extern int win_job_object_no_kill; + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#define MKWINCHILD_MAX_PATH 1024 + +#define MKWINCHILD_DO_SET_PROCESSOR_GROUP + +/** Checks the UTF-16 environment variable pointed to is the PATH. */ +#define IS_PATH_ENV_VAR(a_cwcVar, a_pwszVar) \ + ( (a_cwcVar) >= 5 \ + && (a_pwszVar)[4] == L'=' \ + && ((a_pwszVar)[0] == L'P' || (a_pwszVar)[0] == L'p') \ + && ((a_pwszVar)[1] == L'A' || (a_pwszVar)[1] == L'a') \ + && ((a_pwszVar)[2] == L'T' || (a_pwszVar)[2] == L't') \ + && ((a_pwszVar)[3] == L'H' || (a_pwszVar)[3] == L'h') ) + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** Pointer to a childcare worker thread. */ +typedef struct WINCHILDCAREWORKER *PWINCHILDCAREWORKER; +/** Pointer to a windows child process. */ +typedef struct WINCHILD *PWINCHILD; + + +/** + * Child process type. + */ +typedef enum WINCHILDTYPE +{ + WINCHILDTYPE_INVALID = 0, + /** Normal child process. */ + WINCHILDTYPE_PROCESS, +#ifdef KMK + /** kmkbuiltin command. */ + WINCHILDTYPE_BUILT_IN, + /** kmkbuiltin_append result write out. */ + WINCHILDTYPE_APPEND, + /** kSubmit job. */ + WINCHILDTYPE_SUBMIT, + /** kmk_redirect job. */ + WINCHILDTYPE_REDIRECT, +#endif + /** End of valid child types. */ + WINCHILDTYPE_END +} WINCHILDTYPE; + + +/** + * Windows child process. + */ +typedef struct WINCHILD +{ + /** Magic / eyecatcher (WINCHILD_MAGIC). */ + ULONG uMagic; + /** Child type. */ + WINCHILDTYPE enmType; + /** Pointer to the next child process. */ + PWINCHILD pNext; + /** The pid for this child. */ + pid_t pid; + /** The make child structure associated with this child. */ + struct child *pMkChild; + + /** The process exit code. */ + int iExitCode; + /** Kill signal, in case we or someone else killed it. */ + int iSignal; + /** Set if core was dumped. */ + int fCoreDumped; + /** Set if the a child process is a candidate for cl.exe where we supress + * annoying source name output. */ + BOOL fProbableClExe; + /** The worker executing this child. */ + PWINCHILDCAREWORKER pWorker; + + /** Type specific data. */ + union + { + /** Data for WINCHILDTYPE_PROCESS. */ + struct + { + /** Argument vector (single allocation, strings following array). */ + char **papszArgs; + /** Length of the argument strings. */ + size_t cbArgsStrings; + /** Environment vector. Only a copy if fEnvIsCopy is set. */ + char **papszEnv; + /** If we made a copy of the environment, this is the size of the + * strings and terminator string (not in array). This is done to + * speed up conversion, since MultiByteToWideChar can handle '\0'. */ + size_t cbEnvStrings; + /** The make shell to use (copy). */ + char *pszShell; + /** Handle to use for standard out. */ + HANDLE hStdOut; + /** Handle to use for standard out. */ + HANDLE hStdErr; + /** Whether to close hStdOut after creating the process. */ + BOOL fCloseStdOut; + /** Whether to close hStdErr after creating the process. */ + BOOL fCloseStdErr; + /** Whether to catch output from the process. */ + BOOL fCatchOutput; + + /** Child process handle. */ + HANDLE hProcess; + } Process; + + /** Data for WINCHILDTYPE_BUILT_IN. */ + struct + { + /** The built-in command. */ + PCKMKBUILTINENTRY pBuiltIn; + /** Number of arguments. */ + int cArgs; + /** Argument vector (single allocation, strings following array). */ + char **papszArgs; + /** Environment vector. Only a copy if fEnvIsCopy is set. */ + char **papszEnv; + } BuiltIn; + + /** Data for WINCHILDTYPE_APPEND. */ + struct + { + /** The filename. */ + char *pszFilename; + /** How much to append. */ + size_t cbAppend; + /** What to append. */ + char *pszAppend; + /** Whether to truncate the file. */ + int fTruncate; + } Append; + + /** Data for WINCHILDTYPE_SUBMIT. */ + struct + { + /** The event we're to wait on (hooked up to a pipe) */ + HANDLE hEvent; + /** Parameter for the cleanup callback. */ + void *pvSubmitWorker; + /** Standard output catching pipe. Optional. */ + PWINCCWPIPE pStdOut; + /** Standard error catching pipe. Optional. */ + PWINCCWPIPE pStdErr; + } Submit; + + /** Data for WINCHILDTYPE_REDIRECT. */ + struct + { + /** Child process handle. */ + HANDLE hProcess; + } Redirect; + } u; + +} WINCHILD; +/** WINCHILD::uMagic value. */ +#define WINCHILD_MAGIC 0xbabebabeU + + +/** + * Data for a windows childcare worker thread. + * + * We use one worker thread per child, reusing the threads when possible. + * + * This setup helps avoid the 64-bit handle with the WaitForMultipleObject API. + * + * It also helps using all CPUs on systems with more than one CPU group + * (typically systems with more than 64 CPU threads or/and multiple sockets, or + * special configs). + * + * This helps facilitates using pipes for collecting output child rather + * than temporary files. Pipes doesn't involve NTFS and can easily be reused. + * + * Finally, kBuild specific, this allows running kmkbuiltin_xxxx commands in + * threads. + */ +typedef struct WINCHILDCAREWORKER +{ + /** Magic / eyecatcher (WINCHILDCAREWORKER_MAGIC). */ + ULONG uMagic; + /** The worker index. */ + unsigned int idxWorker; + /** The processor group for this worker. */ + unsigned int iProcessorGroup; + /** The thread ID. */ + unsigned int tid; + /** The thread handle. */ + HANDLE hThread; + /** The event the thread is idling on. */ + HANDLE hEvtIdle; + /** The pipe catching standard output from a child. */ + PWINCCWPIPE pStdOut; + /** The pipe catching standard error from a child. */ + PWINCCWPIPE pStdErr; + + /** Pointer to the current child. */ + PWINCHILD volatile pCurChild; + /** List of children pending execution on this worker. + * This is updated atomitically just like g_pTailCompletedChildren. */ + PWINCHILD volatile pTailTodoChildren; + /** TRUE if idle, FALSE if not. */ + long volatile fIdle; +} WINCHILDCAREWORKER; +/** WINCHILD::uMagic value. */ +#define WINCHILDCAREWORKER_MAGIC 0xdad0dad0U + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Whether it's initialized or not. */ +static BOOL g_fInitialized = FALSE; +/** Set when we're shutting down everything. */ +static BOOL volatile g_fShutdown = FALSE; +/** Event used to wait for children. */ +static HANDLE g_hEvtWaitChildren = INVALID_HANDLE_VALUE; +/** Number of childcare workers currently in g_papChildCareworkers. */ +static unsigned g_cChildCareworkers = 0; +/** Maximum number of childcare workers in g_papChildCareworkers. */ +static unsigned g_cChildCareworkersMax = 0; +/** Pointer to childcare workers. */ +static PWINCHILDCAREWORKER *g_papChildCareworkers = NULL; +/** The processor group allocator state. */ +static MKWINCHILDCPUGROUPALLOCSTATE g_ProcessorGroupAllocator; +/** Number of processor groups in the system. */ +static unsigned g_cProcessorGroups = 1; +/** Array detailing how many active processors there are in each group. */ +static unsigned const *g_pacProcessorsInGroup = &g_cProcessorGroups; +/** Kernel32!GetActiveProcessorGroupCount */ +static WORD (WINAPI *g_pfnGetActiveProcessorGroupCount)(VOID); +/** Kernel32!GetActiveProcessorCount */ +static DWORD (WINAPI *g_pfnGetActiveProcessorCount)(WORD); +/** Kernel32!SetThreadGroupAffinity */ +static BOOL (WINAPI *g_pfnSetThreadGroupAffinity)(HANDLE, CONST GROUP_AFFINITY *, GROUP_AFFINITY *); +/** NTDLL!NtQueryInformationProcess */ +static NTSTATUS (NTAPI *g_pfnNtQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); +/** Set if the windows host is 64-bit. */ +static BOOL g_f64BitHost = (K_ARCH_BITS == 64); +/** Windows version info. + * @note Putting this before the volatile stuff, hoping to keep it in a + * different cache line than the static bits above. */ +static OSVERSIONINFOA g_VersionInfo = { sizeof(g_VersionInfo), 4, 0, 1381, VER_PLATFORM_WIN32_NT, {0} }; + +/** Children that has been completed. + * This is updated atomically, pushing completed children in LIFO fashion + * (thus 'tail'), then hitting g_hEvtWaitChildren if head. */ +static PWINCHILD volatile g_pTailCompletedChildren = NULL; + +/** Number of idle pending children. + * This is updated before g_hEvtWaitChildren is signalled. */ +static unsigned volatile g_cPendingChildren = 0; + +/** Number of idle childcare worker threads. */ +static unsigned volatile g_cIdleChildcareWorkers = 0; +/** Index of the last idle child careworker (just a hint). */ +static unsigned volatile g_idxLastChildcareWorker = 0; + +#ifdef WITH_RW_LOCK +/** RW lock for serializing kmkbuiltin_redirect and CreateProcess. */ +static SRWLOCK g_RWLock; +#endif + +/** The job object for this make instance, if we created/opened one. */ +static HANDLE g_hJob = NULL; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static void mkWinChildInitJobObjectAssociation(void); + +#if K_ARCH_BITS == 32 && !defined(_InterlockedCompareExchangePointer) +/** _InterlockedCompareExchangePointer is missing? (VS2010) */ +K_INLINE void *_InterlockedCompareExchangePointer(void * volatile *ppvDst, void *pvNew, void *pvOld) +{ + return (void *)_InterlockedCompareExchange((long volatile *)ppvDst, (intptr_t)pvNew, (intptr_t)pvOld); +} +#endif + + +/** + * Initializes the windows child module. + * + * @param cJobSlots The number of job slots. + */ +void MkWinChildInit(unsigned int cJobSlots) +{ + HMODULE hmod; + + /* + * Figure out how many childcare workers first. + */ + static unsigned int const s_cMaxWorkers = 4096; + unsigned cWorkers; + if (cJobSlots >= 1 && cJobSlots < s_cMaxWorkers) + cWorkers = cJobSlots; + else + cWorkers = s_cMaxWorkers; + + /* + * Allocate the array and the child completed event object. + */ + g_papChildCareworkers = (PWINCHILDCAREWORKER *)xcalloc(cWorkers * sizeof(g_papChildCareworkers[0])); + g_cChildCareworkersMax = cWorkers; + + g_hEvtWaitChildren = CreateEvent(NULL, FALSE /*fManualReset*/, FALSE /*fInitialState*/, NULL /*pszName*/); + if (!g_hEvtWaitChildren) + fatal(NILF, INTSTR_LENGTH, _("MkWinChildInit: CreateEvent failed: %u"), GetLastError()); + + /* + * NTDLL imports that we need. + */ + hmod = GetModuleHandleA("NTDLL.DLL"); + *(FARPROC *)&g_pfnNtQueryInformationProcess = GetProcAddress(hmod, "NtQueryInformationProcess"); + if (!g_pfnNtQueryInformationProcess) + fatal(NILF, 0, _("MkWinChildInit: NtQueryInformationProcess not found")); + +#if K_ARCH_BITS == 32 + /* + * Initialize g_f64BitHost. + */ + if (!IsWow64Process(GetCurrentProcess(), &g_f64BitHost)) + fatal(NILF, INTSTR_LENGTH, _("MkWinChildInit: IsWow64Process failed: %u"), GetLastError()); +#elif K_ARCH_BITS == 64 + assert(g_f64BitHost); +#else +# error "K_ARCH_BITS is bad/missing" +#endif + + /* + * Figure out how many processor groups there are. + * For that we need to first figure the windows version. + */ + if (!GetVersionExA(&g_VersionInfo)) + { + DWORD uRawVer = GetVersion(); + g_VersionInfo.dwMajorVersion = uRawVer & 0xff; + g_VersionInfo.dwMinorVersion = (uRawVer >> 8) & 0xff; + g_VersionInfo.dwBuildNumber = (uRawVer >> 16) & 0x7fff; + } + if (g_VersionInfo.dwMajorVersion >= 6) + { + hmod = GetModuleHandleA("KERNEL32.DLL"); + *(FARPROC *)&g_pfnGetActiveProcessorGroupCount = GetProcAddress(hmod, "GetActiveProcessorGroupCount"); + *(FARPROC *)&g_pfnGetActiveProcessorCount = GetProcAddress(hmod, "GetActiveProcessorCount"); + *(FARPROC *)&g_pfnSetThreadGroupAffinity = GetProcAddress(hmod, "SetThreadGroupAffinity"); + if ( g_pfnSetThreadGroupAffinity + && g_pfnGetActiveProcessorCount + && g_pfnGetActiveProcessorGroupCount) + { + unsigned int *pacProcessorsInGroup; + unsigned iGroup; + g_cProcessorGroups = g_pfnGetActiveProcessorGroupCount(); + if (g_cProcessorGroups == 0) + g_cProcessorGroups = 1; + + pacProcessorsInGroup = (unsigned int *)xmalloc(sizeof(g_pacProcessorsInGroup[0]) * g_cProcessorGroups); + g_pacProcessorsInGroup = pacProcessorsInGroup; + for (iGroup = 0; iGroup < g_cProcessorGroups; iGroup++) + pacProcessorsInGroup[iGroup] = g_pfnGetActiveProcessorCount(iGroup); + + MkWinChildInitCpuGroupAllocator(&g_ProcessorGroupAllocator); + } + else + { + g_pfnSetThreadGroupAffinity = NULL; + g_pfnGetActiveProcessorCount = NULL; + g_pfnGetActiveProcessorGroupCount = NULL; + } + } + +#ifdef WITH_RW_LOCK + /* + * For serializing with standard file handle manipulation (kmkbuiltin_redirect). + */ + InitializeSRWLock(&g_RWLock); +#endif + + /* + * Associate with a job object. + */ + mkWinChildInitJobObjectAssociation(); + + /* + * This is dead code that was thought to fix a problem observed doing + * `tcc.exe /c "kmk |& tee bld.log"` and leading to a crash in cl.exe + * when spawned with fInheritHandles = FALSE, see hStdErr=NULL in the + * child. However, it turns out this was probably caused by not clearing + * the CRT file descriptor and handle table in the startup info. + * Leaving the code here in case it comes in handy after all. + */ +#if 0 + { + struct + { + DWORD uStdHandle; + HANDLE hHandle; + } aHandles[3] = { { STD_INPUT_HANDLE, NULL }, { STD_OUTPUT_HANDLE, NULL }, { STD_ERROR_HANDLE, NULL } }; + int i; + + for (i = 0; i < 3; i++) + aHandles[i].hHandle = GetStdHandle(aHandles[i].uStdHandle); + + for (i = 0; i < 3; i++) + if ( aHandles[i].hHandle == NULL + || aHandles[i].hHandle == INVALID_HANDLE_VALUE) + { + int fd = open("nul", _O_RDWR); + if (fd >= 0) + { + if (_dup2(fd, i) >= 0) + { + assert((HANDLE)_get_osfhandle(i) != aHandles[i].hHandle); + assert((HANDLE)_get_osfhandle(i) == GetStdHandle(aHandles[i].uStdHandle)); + } + else + ONNNS(fatal, NILF, "_dup2(%d('nul'), %d) failed: %u (%s)", fd, i, errno, strerror(errno)); + if (fd != i) + close(fd); + } + else + ONNS(fatal, NILF, "open(nul,RW) failed: %u (%s)", i, errno, strerror(errno)); + } + else + { + int j; + for (j = i + 1; j < 3; j++) + if (aHandles[j].hHandle == aHandles[i].hHandle) + { + int fd = _dup(j); + if (fd >= 0) + { + if (_dup2(fd, j) >= 0) + { + aHandles[j].hHandle = (HANDLE)_get_osfhandle(j); + assert(aHandles[j].hHandle != aHandles[i].hHandle); + assert(aHandles[j].hHandle == GetStdHandle(aHandles[j].uStdHandle)); + } + else + ONNNS(fatal, NILF, "_dup2(%d, %d) failed: %u (%s)", fd, j, errno, strerror(errno)); + if (fd != j) + close(fd); + } + else + ONNS(fatal, NILF, "_dup(%d) failed: %u (%s)", j, errno, strerror(errno)); + } + } + } +#endif +} + +/** + * Create or open a job object for this make instance and its children. + * + * Depending on the --job-object=mode value, we typically create/open a job + * object here if we're the root make instance. The job object is then + * typically configured to kill all remaining processes when the root make + * terminates, so that there aren't any stuck processes around messing up + * subsequent builds. This is very handy on build servers. + * + * If we're it no-kill mode, the job object is pretty pointless for manual + * cleanup as the job object becomes invisible (or something) when the last + * handle to it closes, i.e. g_hJob. On windows 8 and later it looks + * like any orphaned children are immediately assigned to the parent job + * object. Too bad for kmk_kill and such. + * + * win_job_object_mode values: login, root, each, none + */ +static void mkWinChildInitJobObjectAssociation(void) +{ + BOOL fCreate = TRUE; + char szJobName[128]; + const char *pszJobName = win_job_object_name; + + /* Skip if disabled. */ + if (strcmp(win_job_object_mode, "none") == 0) + return; + + /* Skip if not root make instance, unless we're having one job object + per make instance. */ + if ( makelevel != 0 + && strcmp(win_job_object_mode, "each") != 0) + return; + + /* Format the the default job object name if --job-object-name + wasn't given. */ + if (!pszJobName || *pszJobName == '\0') + { + pszJobName = szJobName; + if (strcmp(win_job_object_mode, "login") == 0) + { + /* Use the AuthenticationId like mspdbsrv.exe does. */ + HANDLE hToken; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) + { + TOKEN_STATISTICS TokenStats; + DWORD cbRet = 0; + memset(&TokenStats, 0, sizeof(TokenStats)); + if (GetTokenInformation(hToken, TokenStatistics, &TokenStats, sizeof(TokenStats), &cbRet)) + snprintf(szJobName, sizeof(szJobName), "kmk-job-obj-login-%08x.%08x", + (unsigned)TokenStats.AuthenticationId.HighPart, (unsigned)TokenStats.AuthenticationId.LowPart); + else + { + ONN(message, 0, _("GetTokenInformation failed: %u (cbRet=%u)"), GetLastError(), cbRet); + return; + } + CloseHandle(hToken); + } + else + { + ON(message, 0, _("OpenProcessToken failed: %u"), GetLastError()); + return; + } + } + else + { + SYSTEMTIME Now = {0}; + GetSystemTime(&Now); + snprintf(szJobName, sizeof(szJobName), "kmk-job-obj-%04u-%02u-%02uT%02u-%02u-%02uZ%u", + Now.wYear, Now.wMonth, Now.wDay, Now.wHour, Now.wMinute, Now.wSecond, getpid()); + } + } + + /* In login mode and when given a job object name, we try open it first. */ + if ( win_job_object_name + || strcmp(win_job_object_mode, "login") == 0) + { + g_hJob = OpenJobObjectA(JOB_OBJECT_ASSIGN_PROCESS, win_job_object_no_kill /*bInheritHandle*/, pszJobName); + if (g_hJob) + fCreate = FALSE; + else + { + DWORD dwErr = GetLastError(); + if (dwErr != ERROR_PATH_NOT_FOUND && dwErr != ERROR_FILE_NOT_FOUND) + { + OSN(message, 0, _("OpenJobObjectA(,,%s) failed: %u"), pszJobName, GetLastError()); + return; + } + } + } + + if (fCreate) + { + SECURITY_ATTRIBUTES SecAttr = { sizeof(SecAttr), NULL, TRUE /*bInheritHandle*/ }; + g_hJob = CreateJobObjectA(win_job_object_no_kill ? &SecAttr : NULL, pszJobName); + if (g_hJob) + { + /* We need to set the BREAKAWAY_OK flag, as we don't want make CreateProcess + fail if someone tries to break way. Also set KILL_ON_JOB_CLOSE unless + --job-object-no-kill is given. */ + JOBOBJECT_EXTENDED_LIMIT_INFORMATION Info; + DWORD cbActual = 0; + memset(&Info, 0, sizeof(Info)); + if (QueryInformationJobObject(g_hJob, JobObjectExtendedLimitInformation, &Info, sizeof(Info), &cbActual)) + { + Info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK; + if (!win_job_object_no_kill) + Info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + else + Info.BasicLimitInformation.LimitFlags &= ~JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!SetInformationJobObject(g_hJob, JobObjectExtendedLimitInformation, &Info, sizeof(Info))) + OSSN(message, 0, _("SetInformationJobObject(%s,JobObjectExtendedLimitInformation,{%s},) failed: %u"), + pszJobName, win_job_object_mode, GetLastError()); + } + else + OSN(message, 0, _("QueryInformationJobObject(%s,JobObjectExtendedLimitInformation,,,) failed: %u"), + pszJobName, GetLastError()); + } + else + { + OSN(message, 0, _("CreateJobObjectA(NULL,%s) failed: %u"), pszJobName, GetLastError()); + return; + } + } + + /* Make it our job object. */ + if (!(AssignProcessToJobObject(g_hJob, GetCurrentProcess()))) + OSN(message, 0, _("AssignProcessToJobObject(%s, me) failed: %u"), pszJobName, GetLastError()); +} + +/** + * Used by mkWinChildcareWorkerThread() and MkWinChildWait() to get the head + * child from a lifo (g_pTailCompletedChildren, pTailTodoChildren). + * + * @returns Head child. + * @param ppTail Pointer to the child variable. + * @param pChild Tail child. + */ +static PWINCHILD mkWinChildDequeFromLifo(PWINCHILD volatile *ppTail, PWINCHILD pChild) +{ + if (pChild->pNext) + { + PWINCHILD pPrev; + do + { + pPrev = pChild; + pChild = pChild->pNext; + } while (pChild->pNext); + pPrev->pNext = NULL; + } + else + { + PWINCHILD const pWantedChild = pChild; + pChild = _InterlockedCompareExchangePointer(ppTail, NULL, pWantedChild); + if (pChild != pWantedChild) + { + PWINCHILD pPrev; + do + { + pPrev = pChild; + pChild = pChild->pNext; + } while (pChild->pNext); + pPrev->pNext = NULL; + assert(pChild == pWantedChild); + } + } + return pChild; +} + +/** + * Output error message while running on a worker thread. + * + * @returns -1 + * @param pWorker The calling worker. Mainly for getting the + * current child and its stderr output unit. Pass + * NULL if the output should go thru the child + * stderr buffering. + * @param iType The error type: + * - 0: more of a info directly to stdout, + * - 1: child related error, + * - 2: child related error for immedate release. + * @param pszFormat The message format string. + * @param ... Argument for the message. + */ +static int MkWinChildError(PWINCHILDCAREWORKER pWorker, int iType, const char *pszFormat, ...) +{ + /* + * Format the message into stack buffer. + */ + char szMsg[4096]; + int cchMsg; + int cchPrefix; + va_list va; + + /* Compose the prefix, being paranoid about it not exceeding the buffer in any way. */ + const char *pszInfix = iType == 0 ? "info: " : "error: "; + const char *pszProgram = program; + if (strlen(pszProgram) > 80) + { +#ifdef KMK + pszProgram = "kmk"; +#else + pszProgram = "gnumake"; +#endif + } + if (makelevel == 0) + cchPrefix = snprintf(szMsg, sizeof(szMsg) / 2, "%s: %s", pszProgram, pszInfix); + else + cchPrefix = snprintf(szMsg, sizeof(szMsg) / 2, "%s[%u]: %s", pszProgram, makelevel, pszInfix); + assert(cchPrefix < sizeof(szMsg) / 2 && cchPrefix > 0); + + /* Format the user specified message. */ + va_start(va, pszFormat); + cchMsg = vsnprintf(&szMsg[cchPrefix], sizeof(szMsg) - 2 - cchPrefix, pszFormat, va); + va_end(va); + szMsg[sizeof(szMsg) - 2] = '\0'; + cchMsg = strlen(szMsg); + + /* Make sure there's a newline at the end of it (we reserved space for that). */ + if (cchMsg <= 0 || szMsg[cchMsg - 1] != '\n') + { + szMsg[cchMsg++] = '\n'; + szMsg[cchMsg] = '\0'; + } + +#ifdef CONFIG_WITH_OUTPUT_IN_MEMORY + /* + * Try use the stderr of the current child of the worker. + */ + if ( iType != 0 + && iType != 3 + && pWorker) + { + PWINCHILD pChild = pWorker->pCurChild; + if (pChild) + { + struct child *pMkChild = pChild->pMkChild; + if (pMkChild) + { + output_write_text(&pMkChild->output, 1 /*is_err*/, szMsg, cchMsg); + return -1; + } + } + } +#endif + + /* + * Fallback to writing directly to stderr. + */ + maybe_con_fwrite(szMsg, cchMsg, 1, iType == 0 ? stdout : stderr); + return -1; +} + +/** + * Duplicates the given UTF-16 string. + * + * @returns 0 + * @param pwszSrc The UTF-16 string to duplicate. + * @param cwcSrc Length, may include the terminator. + * @param ppwszDst Where to return the duplicate. + */ +static int mkWinChildDuplicateUtf16String(const WCHAR *pwszSrc, size_t cwcSrc, WCHAR **ppwszDst) +{ + size_t cb = sizeof(WCHAR) * cwcSrc; + if (cwcSrc > 0 && pwszSrc[cwcSrc - 1] == L'\0') + *ppwszDst = (WCHAR *)memcpy(xmalloc(cb), pwszSrc, cb); + else + { + WCHAR *pwszDst = (WCHAR *)xmalloc(cb + sizeof(WCHAR)); + memcpy(pwszDst, pwszSrc, cb); + pwszDst[cwcSrc] = L'\0'; + *ppwszDst = pwszDst; + } + return 0; +} + + +/** + * Used to flush data we're read but not yet written at the termination of a + * process. + * + * @param pChild The child. + * @param pPipe The pipe. + */ +static void mkWinChildcareWorkerFlushUnwritten(PWINCHILD pChild, PWINCCWPIPE pPipe) +{ + DWORD cbUnwritten = pPipe->offPendingRead - pPipe->cbWritten; + assert(pPipe->cbWritten <= pPipe->cbBuffer - 16); + assert(pPipe->offPendingRead <= pPipe->cbBuffer - 16); + if (cbUnwritten) + { +#ifdef CONFIG_WITH_OUTPUT_IN_MEMORY + if (pChild && pChild->pMkChild) + { + output_write_bin(&pChild->pMkChild->output, pPipe->iWhich == 2, &pPipe->pbBuffer[pPipe->cbWritten], cbUnwritten); + pPipe->cbWritten += cbUnwritten; + } + else +#endif + { + DWORD cbWritten = 0; + if (WriteFile(GetStdHandle(pPipe->iWhich == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE), + &pPipe->pbBuffer[pPipe->cbWritten], cbUnwritten, &cbWritten, NULL)) + pPipe->cbWritten += cbWritten <= cbUnwritten ? cbWritten : cbUnwritten; /* paranoia */ + } + pPipe->fHaveWrittenOut = TRUE; + } +} + +/** + * This logic mirrors kwSandboxConsoleFlushAll. + * + * @returns TRUE if it looks like a CL.EXE source line, otherwise FALSE. + * @param pPipe The pipe. + * @param offStart The start of the output in the pipe buffer. + * @param offEnd The end of the output in the pipe buffer. + */ +static BOOL mkWinChildcareWorkerIsClExeSourceLine(PWINCCWPIPE pPipe, DWORD offStart, DWORD offEnd) +{ + if (offEnd < offStart + 2) + return FALSE; + if (offEnd - offStart > 80) + return FALSE; + + if ( pPipe->pbBuffer[offEnd - 2] != '\r' + || pPipe->pbBuffer[offEnd - 1] != '\n') + return FALSE; + + offEnd -= 2; + while (offEnd-- > offStart) + { + char ch = pPipe->pbBuffer[offEnd]; + if (isalnum(ch) || ch == '.' || ch == ' ' || ch == '_' || ch == '-') + { /* likely */ } + else + return FALSE; + } + + return TRUE; +} + +/** + * Adds output to the given standard output for the child. + * + * There is no pending read when this function is called, so we're free to + * reshuffle the buffer if desirable. + * + * @param pChild The child. Optional (kSubmit). + * @param iWhich Which standard descriptor number. + * @param cbNewData How much more output was caught. + */ +static void mkWinChildcareWorkerCaughtMoreOutput(PWINCHILD pChild, PWINCCWPIPE pPipe, DWORD cbNewData) +{ + DWORD offStart = pPipe->cbWritten; + assert(offStart <= pPipe->offPendingRead); + assert(offStart <= pPipe->cbBuffer - 16); + assert(pPipe->offPendingRead <= pPipe->cbBuffer - 16); + if (cbNewData > 0) + { + DWORD offRest; + + /* Move offPendingRead ahead by cbRead. */ + pPipe->offPendingRead += cbNewData; + assert(pPipe->offPendingRead <= pPipe->cbBuffer); + if (pPipe->offPendingRead > pPipe->cbBuffer) + pPipe->offPendingRead = pPipe->cbBuffer; + + /* Locate the last newline in the buffer. */ + offRest = pPipe->offPendingRead; + while (offRest > offStart && pPipe->pbBuffer[offRest - 1] != '\n') + offRest--; + + /* If none were found and we've less than 16 bytes left in the buffer, try + find a word boundrary to flush on instead. */ + if ( offRest <= offStart + && pPipe->cbBuffer - pPipe->offPendingRead + offStart < 16) + { + offRest = pPipe->offPendingRead; + while ( offRest > offStart + && isalnum(pPipe->pbBuffer[offRest - 1])) + offRest--; + if (offRest == offStart) + offRest = pPipe->offPendingRead; + } + /* If this is a potential CL.EXE process, we will keep the source + filename unflushed and maybe discard it at the end. */ + else if ( pChild + && pChild->fProbableClExe + && pPipe->iWhich == 1 + && offRest == pPipe->offPendingRead + && mkWinChildcareWorkerIsClExeSourceLine(pPipe, offStart, offRest)) + offRest = offStart; + + if (offRest > offStart) + { + /* Write out offStart..offRest. */ + DWORD cbToWrite = offRest - offStart; +#ifdef CONFIG_WITH_OUTPUT_IN_MEMORY + if (pChild && pChild->pMkChild) + { + output_write_bin(&pChild->pMkChild->output, pPipe->iWhich == 2, &pPipe->pbBuffer[offStart], cbToWrite); + offStart += cbToWrite; + pPipe->cbWritten = offStart; + } + else +#endif + { + DWORD cbWritten = 0; + if (WriteFile(GetStdHandle(pPipe->iWhich == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE), + &pPipe->pbBuffer[offStart], cbToWrite, &cbWritten, NULL)) + { + offStart += cbWritten <= cbToWrite ? cbWritten : cbToWrite; /* paranoia */ + pPipe->cbWritten = offStart; + } + } + pPipe->fHaveWrittenOut = TRUE; + } + } + + /* Shuffle the data to the front of the buffer. */ + if (offStart > 0) + { + DWORD cbUnwritten = pPipe->offPendingRead - offStart; + if (cbUnwritten > 0) + memmove(pPipe->pbBuffer, &pPipe->pbBuffer[offStart], cbUnwritten); + pPipe->offPendingRead -= pPipe->cbWritten; + pPipe->cbWritten = 0; + } +} + +/** + * Catches output from the given pipe. + * + * @param pChild The child. Optional (kSubmit). + * @param pPipe The pipe. + * @param fDraining Set if we're draining the pipe after the process + * terminated. + */ +static void mkWinChildcareWorkerCatchOutput(PWINCHILD pChild, PWINCCWPIPE pPipe, BOOL fDraining) +{ + /* + * Deal with already pending read. + */ + if (pPipe->fReadPending) + { + DWORD cbRead = 0; + if (GetOverlappedResult(pPipe->hPipeMine, &pPipe->Overlapped, &cbRead, !fDraining)) + { + mkWinChildcareWorkerCaughtMoreOutput(pChild, pPipe, cbRead); + pPipe->fReadPending = FALSE; + } + else if (fDraining && GetLastError() == ERROR_IO_INCOMPLETE) + return; + else + { + MkWinChildError(pChild ? pChild->pWorker : NULL, 2, "GetOverlappedResult failed: %u\n", GetLastError()); + pPipe->fReadPending = FALSE; + if (fDraining) + return; + } + } + + /* + * Read data till one becomes pending. + */ + for (;;) + { + DWORD cbRead; + + memset(&pPipe->Overlapped, 0, sizeof(pPipe->Overlapped)); + pPipe->Overlapped.hEvent = pPipe->hEvent; + ResetEvent(pPipe->hEvent); + + assert(pPipe->offPendingRead < pPipe->cbBuffer); + SetLastError(0); + cbRead = 0; + if (!ReadFile(pPipe->hPipeMine, &pPipe->pbBuffer[pPipe->offPendingRead], + pPipe->cbBuffer - pPipe->offPendingRead, &cbRead, &pPipe->Overlapped)) + { + DWORD dwErr = GetLastError(); + if (dwErr == ERROR_IO_PENDING) + pPipe->fReadPending = TRUE; + else + MkWinChildError(pChild ? pChild->pWorker : NULL, 2, + "ReadFile failed on standard %s: %u\n", + pPipe->iWhich == 1 ? "output" : "error", GetLastError()); + return; + } + + mkWinChildcareWorkerCaughtMoreOutput(pChild, pPipe, cbRead); + } +} + +/** + * Makes sure the output pipes are drained and pushed to output. + * + * @param pChild The child. Optional (kSubmit). + * @param pStdOut The standard output pipe structure. + * @param pStdErr The standard error pipe structure. + */ +void MkWinChildcareWorkerDrainPipes(PWINCHILD pChild, PWINCCWPIPE pStdOut, PWINCCWPIPE pStdErr) +{ + mkWinChildcareWorkerCatchOutput(pChild, pStdOut, TRUE /*fDraining*/); + mkWinChildcareWorkerCatchOutput(pChild, pStdErr, TRUE /*fDraining*/); + + /* Drop lone 'source.c' line from CL.exe, but only if no other output at all. */ + if ( pChild + && pChild->fProbableClExe + && !pStdOut->fHaveWrittenOut + && !pStdErr->fHaveWrittenOut + && pStdErr->cbWritten == pStdErr->offPendingRead + && pStdOut->cbWritten < pStdOut->offPendingRead + && mkWinChildcareWorkerIsClExeSourceLine(pStdOut, pStdOut->cbWritten, pStdOut->offPendingRead)) + { + if (!pStdOut->fReadPending) + pStdOut->cbWritten = pStdOut->offPendingRead = 0; + else + pStdOut->cbWritten = pStdOut->offPendingRead; + } + else + { + mkWinChildcareWorkerFlushUnwritten(pChild, pStdOut); + mkWinChildcareWorkerFlushUnwritten(pChild, pStdErr); + } +} + +/** + * Commmon worker for waiting on a child process and retrieving the exit code. + * + * @returns Child exit code. + * @param pWorker The worker. + * @param pChild The child. + * @param hProcess The process handle. + * @param pwszJob The job name. + * @param fCatchOutput Set if we need to work the output pipes + * associated with the worker. + */ +static int mkWinChildcareWorkerWaitForProcess(PWINCHILDCAREWORKER pWorker, PWINCHILD pChild, HANDLE hProcess, + WCHAR const *pwszJob, BOOL fCatchOutput) +{ + DWORD const msStart = GetTickCount(); + DWORD msNextMsg = msStart + 15000; + + /* Reset the written indicators on the pipes before we start loop. */ + pWorker->pStdOut->fHaveWrittenOut = FALSE; + pWorker->pStdErr->fHaveWrittenOut = FALSE; + + for (;;) + { + /* + * Do the waiting and output catching. + */ + DWORD dwStatus; + if (!fCatchOutput) + dwStatus = WaitForSingleObject(hProcess, 15001 /*ms*/); + else + { + HANDLE ahHandles[3] = { hProcess, pWorker->pStdOut->hEvent, pWorker->pStdErr->hEvent }; + dwStatus = WaitForMultipleObjects(3, ahHandles, FALSE /*fWaitAll*/, 1000 /*ms*/); + if (dwStatus == WAIT_OBJECT_0 + 1) + mkWinChildcareWorkerCatchOutput(pChild, pWorker->pStdOut, FALSE /*fDraining*/); + else if (dwStatus == WAIT_OBJECT_0 + 2) + mkWinChildcareWorkerCatchOutput(pChild, pWorker->pStdErr, FALSE /*fDraining*/); + } + assert(dwStatus != WAIT_FAILED); + + /* + * Get the exit code and return if the process was signalled as done. + */ + if (dwStatus == WAIT_OBJECT_0) + { + DWORD dwExitCode = -42; + if (GetExitCodeProcess(hProcess, &dwExitCode)) + { + pChild->iExitCode = (int)dwExitCode; + if (fCatchOutput) + MkWinChildcareWorkerDrainPipes(pChild, pWorker->pStdOut, pWorker->pStdErr); + return dwExitCode; + } + } + /* + * Loop again if just a timeout or pending output? + * Put out a message every 15 or 30 seconds if the job takes a while. + */ + else if ( dwStatus == WAIT_TIMEOUT + || dwStatus == WAIT_OBJECT_0 + 1 + || dwStatus == WAIT_OBJECT_0 + 2 + || dwStatus == WAIT_IO_COMPLETION) + { + DWORD msNow = GetTickCount(); + if (msNow >= msNextMsg) + { + if ( !pChild->pMkChild + || !pChild->pMkChild->recursive) /* ignore make recursions */ + { + if ( !pChild->pMkChild + || !pChild->pMkChild->file + || !pChild->pMkChild->file->name) + MkWinChildError(NULL, 0, "Pid %u ('%ls') still running after %u seconds\n", + GetProcessId(hProcess), pwszJob, (msNow - msStart) / 1000); + else + MkWinChildError(NULL, 0, "Target '%s' (pid %u) still running after %u seconds\n", + pChild->pMkChild->file->name, GetProcessId(hProcess), (msNow - msStart) / 1000); + } + + /* After 15s, 30s, 60s, 120s, 180s, ... */ + if (msNextMsg == msStart + 15000) + msNextMsg += 15000; + else + msNextMsg += 30000; + } + continue; + } + + /* Something failed. */ + pChild->iExitCode = GetLastError(); + if (pChild->iExitCode == 0) + pChild->iExitCode = -4242; + return pChild->iExitCode; + } +} + + +/** + * Closes standard handles that need closing before destruction. + * + * @param pChild The child (WINCHILDTYPE_PROCESS). + */ +static void mkWinChildcareWorkerCloseStandardHandles(PWINCHILD pChild) +{ + if ( pChild->u.Process.fCloseStdOut + && pChild->u.Process.hStdOut != INVALID_HANDLE_VALUE) + { + CloseHandle(pChild->u.Process.hStdOut); + pChild->u.Process.hStdOut = INVALID_HANDLE_VALUE; + pChild->u.Process.fCloseStdOut = FALSE; + } + if ( pChild->u.Process.fCloseStdErr + && pChild->u.Process.hStdErr != INVALID_HANDLE_VALUE) + { + CloseHandle(pChild->u.Process.hStdErr); + pChild->u.Process.hStdErr = INVALID_HANDLE_VALUE; + pChild->u.Process.fCloseStdErr = FALSE; + } +} + + +/** + * Does the actual process creation. + * + * @returns 0 if there is anything to wait on, otherwise non-zero windows error. + * @param pWorker The childcare worker. + * @param pChild The child. + * @param pwszImageName The image path. + * @param pwszCommandLine The command line. + * @param pwszzEnvironment The enviornment block. + */ +static int mkWinChildcareWorkerCreateProcess(PWINCHILDCAREWORKER pWorker, WCHAR const *pwszImageName, + WCHAR const *pwszCommandLine, WCHAR const *pwszzEnvironment, WCHAR const *pwszCwd, + BOOL pafReplace[3], HANDLE pahChild[3], BOOL fCatchOutput, HANDLE *phProcess) +{ + PROCESS_INFORMATION ProcInfo; + STARTUPINFOW StartupInfo; + DWORD fFlags = CREATE_UNICODE_ENVIRONMENT; + BOOL const fHaveHandles = pafReplace[0] | pafReplace[1] | pafReplace[2]; + BOOL fRet; + DWORD dwErr; +#ifdef KMK + extern int process_priority; +#endif + + /* + * Populate startup info. + * + * Turns out we can get away without passing TRUE for the inherit handles + * parameter to CreateProcess when we're not using STARTF_USESTDHANDLES. + * At least on NT, which is all worth caring about at this point + context IMO. + * + * Not inherting the handles is a good thing because it means we won't + * accidentally end up with a pipe handle or such intended for a different + * child process, potentially causing the EOF/HUP event to be delayed. + * + * Since the present handle inhertiance requirements only involves standard + * output and error, we'll never set the inherit handles flag and instead + * do manual handle duplication and planting. + */ + memset(&StartupInfo, 0, sizeof(StartupInfo)); + StartupInfo.cb = sizeof(StartupInfo); + GetStartupInfoW(&StartupInfo); + StartupInfo.lpReserved2 = 0; /* No CRT file handle + descriptor info possible, sorry. */ + StartupInfo.cbReserved2 = 0; + if ( !fHaveHandles + && !fCatchOutput) + StartupInfo.dwFlags &= ~STARTF_USESTDHANDLES; + else + { + fFlags |= CREATE_SUSPENDED; + StartupInfo.dwFlags &= ~STARTF_USESTDHANDLES; + } + + /* + * Flags. + */ +#ifdef KMK + switch (process_priority) + { + case 1: fFlags |= CREATE_SUSPENDED | IDLE_PRIORITY_CLASS; break; + case 2: fFlags |= CREATE_SUSPENDED | BELOW_NORMAL_PRIORITY_CLASS; break; + case 3: fFlags |= CREATE_SUSPENDED | NORMAL_PRIORITY_CLASS; break; + case 4: fFlags |= CREATE_SUSPENDED | HIGH_PRIORITY_CLASS; break; + case 5: fFlags |= CREATE_SUSPENDED | REALTIME_PRIORITY_CLASS; break; + } +#endif + if (g_cProcessorGroups > 1) + fFlags |= CREATE_SUSPENDED; + + /* + * Try create the process. + */ + DB(DB_JOBS, ("CreateProcessW(%ls, %ls,,, TRUE, %#x...)\n", pwszImageName, pwszCommandLine, fFlags)); + memset(&ProcInfo, 0, sizeof(ProcInfo)); +#ifdef WITH_RW_LOCK + AcquireSRWLockShared(&g_RWLock); +#endif + + fRet = CreateProcessW((WCHAR *)pwszImageName, (WCHAR *)pwszCommandLine, NULL /*pProcSecAttr*/, NULL /*pThreadSecAttr*/, + FALSE /*fInheritHandles*/, fFlags, (WCHAR *)pwszzEnvironment, pwszCwd, &StartupInfo, &ProcInfo); + dwErr = GetLastError(); + +#ifdef WITH_RW_LOCK + ReleaseSRWLockShared(&g_RWLock); +#endif + if (fRet) + *phProcess = ProcInfo.hProcess; + else + { + MkWinChildError(pWorker, 1, "CreateProcess(%ls) failed: %u\n", pwszImageName, dwErr); + return (int)dwErr; + } + + /* + * If the child is suspended, we've got some adjustment work to be done. + */ + dwErr = ERROR_SUCCESS; + if (fFlags & CREATE_SUSPENDED) + { + /* + * First do handle inhertiance as that's the most complicated. + */ + if (fHaveHandles || fCatchOutput) + { + char szErrMsg[128]; + if (fCatchOutput) + { + if (!pafReplace[1]) + { + pafReplace[1] = TRUE; + pahChild[1] = pWorker->pStdOut->hPipeChild; + } + if (!pafReplace[2]) + { + pafReplace[2] = TRUE; + pahChild[2] = pWorker->pStdErr->hPipeChild; + } + } + dwErr = nt_child_inject_standard_handles(ProcInfo.hProcess, pafReplace, pahChild, szErrMsg, sizeof(szErrMsg)); + if (dwErr != 0) + MkWinChildError(pWorker, 1, "%s\n", szErrMsg); + } + + /* + * Assign processor group (ignore failure). + */ +#ifdef MKWINCHILD_DO_SET_PROCESSOR_GROUP + if (g_cProcessorGroups > 1) + { + GROUP_AFFINITY Affinity = { 0 /* == all active apparently */, pWorker->iProcessorGroup, { 0, 0, 0 } }; + fRet = g_pfnSetThreadGroupAffinity(ProcInfo.hThread, &Affinity, NULL); + assert(fRet); + } +#endif + +#ifdef KMK + /* + * Set priority (ignore failure). + */ + switch (process_priority) + { + case 1: fRet = SetThreadPriority(ProcInfo.hThread, THREAD_PRIORITY_IDLE); break; + case 2: fRet = SetThreadPriority(ProcInfo.hThread, THREAD_PRIORITY_BELOW_NORMAL); break; + case 3: fRet = SetThreadPriority(ProcInfo.hThread, THREAD_PRIORITY_NORMAL); break; + case 4: fRet = SetThreadPriority(ProcInfo.hThread, THREAD_PRIORITY_HIGHEST); break; + case 5: fRet = SetThreadPriority(ProcInfo.hThread, THREAD_PRIORITY_TIME_CRITICAL); break; + default: fRet = TRUE; + } + assert(fRet); +#endif + + /* + * Inject the job object if we're in a non-killing mode, to postpone + * the closing of the job object and maybe make it more useful. + */ + if (win_job_object_no_kill && g_hJob) + { + HANDLE hWhatever = INVALID_HANDLE_VALUE; + DuplicateHandle(GetCurrentProcess(), g_hJob, ProcInfo.hProcess, &hWhatever, GENERIC_ALL, + TRUE /*bInheritHandle*/, DUPLICATE_SAME_ACCESS); + } + + /* + * Resume the thread if the adjustments succeeded, otherwise kill it. + */ + if (dwErr == ERROR_SUCCESS) + { + fRet = ResumeThread(ProcInfo.hThread); + assert(fRet); + if (!fRet) + { + dwErr = GetLastError(); + MkWinChildError(pWorker, 1, "ResumeThread failed on child process: %u\n", dwErr); + } + } + if (dwErr != ERROR_SUCCESS) + TerminateProcess(ProcInfo.hProcess, dwErr); + } + + /* + * Close unnecessary handles and cache the image. + */ + CloseHandle(ProcInfo.hThread); + kmk_cache_exec_image_w(pwszImageName); + return 0; +} + +/** + * Converts a argument vector that has already been quoted correctly. + * + * The argument vector is typically the result of quote_argv(). + * + * @returns 0 on success, non-zero on failure. + * @param pWorker The childcare worker. + * @param papszArgs The argument vector to convert. + * @param ppwszCommandLine Where to return the command line. + */ +static int mkWinChildcareWorkerConvertQuotedArgvToCommandline(PWINCHILDCAREWORKER pWorker, char **papszArgs, + WCHAR **ppwszCommandLine) +{ + WCHAR *pwszCmdLine; + WCHAR *pwszDst; + + /* + * Calc length the converted length. + */ + unsigned cwcNeeded = 1; + unsigned i = 0; + const char *pszSrc; + while ((pszSrc = papszArgs[i]) != NULL) + { + int cwcThis = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, -1, NULL, 0); + if (cwcThis > 0 || *pszSrc == '\0') + cwcNeeded += cwcThis + 1; + else + { + DWORD dwErr = GetLastError(); + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert argv[%u] (%s): %u\n"), i, pszSrc, dwErr); + return dwErr; + } + i++; + } + + /* + * Allocate and do the conversion. + */ + pwszCmdLine = pwszDst = (WCHAR *)xmalloc(cwcNeeded * sizeof(WCHAR)); + i = 0; + while ((pszSrc = papszArgs[i]) != NULL) + { + int cwcThis; + if (i > 0) + { + *pwszDst++ = ' '; + cwcNeeded--; + } + + cwcThis = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, -1, pwszDst, cwcNeeded); + if (!cwcThis && *pszSrc != '\0') + { + DWORD dwErr = GetLastError(); + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert argv[%u] (%s): %u\n"), i, pszSrc, dwErr); + free(pwszCmdLine); + return dwErr; + } + if (cwcThis > 0 && pwszDst[cwcThis - 1] == '\0') + cwcThis--; + pwszDst += cwcThis; + cwcNeeded -= cwcThis; + i++; + } + *pwszDst++ = '\0'; + + *ppwszCommandLine = pwszCmdLine; + return 0; +} + + +#define MKWCCWCMD_F_CYGWIN_SHELL 1 +#define MKWCCWCMD_F_MKS_SHELL 2 +#define MKWCCWCMD_F_HAVE_SH 4 +#define MKWCCWCMD_F_HAVE_KASH_C 8 /**< kmk_ash -c "..." */ + +/* + * @param pWorker The childcare worker if on one, otherwise NULL. + */ +static int mkWinChildcareWorkerConvertCommandline(PWINCHILDCAREWORKER pWorker, char **papszArgs, unsigned fFlags, + WCHAR **ppwszCommandLine) +{ + struct ARGINFO + { + size_t cchSrc; + size_t cwcDst; /**< converted size w/o terminator. */ + size_t cwcDstExtra : 24; /**< Only set with fSlowly. */ + size_t fSlowly : 1; + size_t fQuoteIt : 1; + size_t fEndSlashes : 1; /**< if escapes needed for trailing backslashes. */ + size_t fExtraSpace : 1; /**< if kash -c "" needs an extra space before the quote. */ + } *paArgInfo; + size_t cArgs; + size_t i; + size_t cwcNeeded; + WCHAR *pwszDst; + WCHAR *pwszCmdLine; + + /* + * Count them first so we can allocate an info array of the stack. + */ + cArgs = 0; + while (papszArgs[cArgs] != NULL) + cArgs++; + paArgInfo = (struct ARGINFO *)alloca(sizeof(paArgInfo[0]) * cArgs); + + /* + * Preprocess them and calculate the exact command line length. + */ + cwcNeeded = 1; + for (i = 0; i < cArgs; i++) + { + char *pszSrc = papszArgs[i]; + size_t cchSrc = strlen(pszSrc); + paArgInfo[i].cchSrc = cchSrc; + if (cchSrc == 0) + { + /* empty needs quoting. */ + paArgInfo[i].cwcDst = 2; + paArgInfo[i].cwcDstExtra = 0; + paArgInfo[i].fSlowly = 0; + paArgInfo[i].fQuoteIt = 1; + paArgInfo[i].fExtraSpace = 0; + paArgInfo[i].fEndSlashes = 0; + } + else + { + const char *pszSpace = memchr(pszSrc, ' ', cchSrc); + const char *pszTab = memchr(pszSrc, '\t', cchSrc); + const char *pszDQuote = memchr(pszSrc, '"', cchSrc); + const char *pszEscape = memchr(pszSrc, '\\', cchSrc); + int cwcDst = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, cchSrc + 1, NULL, 0); + if (cwcDst >= 0) + --cwcDst; + else + { + DWORD dwErr = GetLastError(); + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert argv[%u] (%s): %u\n"), i, pszSrc, dwErr); + return dwErr; + } +#if 0 + if (!pszSpace && !pszTab && !pszDQuote && !pszEscape) + { + /* no special handling needed. */ + paArgInfo[i].cwcDst = cwcDst; + paArgInfo[i].cwcDstExtra = 0; + paArgInfo[i].fSlowly = 0; + paArgInfo[i].fQuoteIt = 0; + paArgInfo[i].fExtraSpace = 0; + paArgInfo[i].fEndSlashes = 0; + } + else if (!pszDQuote && !pszEscape) + { + /* Just double quote it. */ + paArgInfo[i].cwcDst = cwcDst + 2; + paArgInfo[i].cwcDstExtra = 0; + paArgInfo[i].fSlowly = 0; + paArgInfo[i].fQuoteIt = 1; + paArgInfo[i].fExtraSpace = 0; + paArgInfo[i].fEndSlashes = 0; + } + else +#endif + { + /* Complicated, need to scan the string to figure out what to do. */ + size_t cwcDstExtra; + int cBackslashes; + char ch; + + paArgInfo[i].fQuoteIt = 0; + paArgInfo[i].fSlowly = 1; + paArgInfo[i].fExtraSpace = 0; + paArgInfo[i].fEndSlashes = 0; + + cwcDstExtra = 0; + cBackslashes = 0; + while ((ch = *pszSrc++) != '\0') + { + switch (ch) + { + default: + cBackslashes = 0; + break; + + case '\\': + cBackslashes++; + break; + + case '"': + if (fFlags & (MKWCCWCMD_F_CYGWIN_SHELL | MKWCCWCMD_F_MKS_SHELL)) + cwcDstExtra += 1; /* just an extra '"' */ + else + cwcDstExtra += 1 + cBackslashes; /* extra '\\' for the '"' and for each preceeding slash. */ + cBackslashes = 0; + break; + + case ' ': + case '\t': + if (!paArgInfo[i].fQuoteIt) + { + paArgInfo[i].fQuoteIt = 1; + cwcDstExtra += 2; + } + cBackslashes = 0; + break; + } + } + + /* If we're quoting the argument and it ends with trailing '\\', it/they must be escaped. */ + if ( cBackslashes > 0 + && paArgInfo[i].fQuoteIt + && !(fFlags & (MKWCCWCMD_F_CYGWIN_SHELL | MKWCCWCMD_F_MKS_SHELL))) + { + cwcDstExtra += cBackslashes; + paArgInfo[i].fEndSlashes = 1; + } + + paArgInfo[i].cwcDst = cwcDst + cwcDstExtra; + paArgInfo[i].cwcDstExtra = cwcDstExtra; + } + } + + if ( (fFlags & MKWCCWCMD_F_HAVE_KASH_C) + && paArgInfo[i].fQuoteIt) + { + paArgInfo[i].fExtraSpace = 1; + paArgInfo[i].cwcDst++; + paArgInfo[i].cwcDstExtra++; + } + + cwcNeeded += (i != 0) + paArgInfo[i].cwcDst; + } + + /* + * Allocate the result buffer and do the actual conversion. + */ + pwszDst = pwszCmdLine = (WCHAR *)xmalloc(sizeof(WCHAR) * cwcNeeded); + for (i = 0; i < cArgs; i++) + { + char *pszSrc = papszArgs[i]; + size_t cwcDst = paArgInfo[i].cwcDst; + + if (i != 0) + *pwszDst++ = L' '; + + if (paArgInfo[i].fQuoteIt) + { + *pwszDst++ = L'"'; + cwcDst -= 2; + } + + if (!paArgInfo[i].fSlowly) + { + int cwcDst2 = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, paArgInfo[i].cchSrc, pwszDst, cwcDst + 1); + assert(cwcDst2 >= 0); + pwszDst += cwcDst; + } + else + { + /* Do the conversion into the end of the output buffer, then move + it up to where it should be char by char. */ + int cBackslashes; + size_t cwcLeft = paArgInfo[i].cwcDst - paArgInfo[i].cwcDstExtra; + WCHAR volatile *pwchSlowSrc = pwszDst + paArgInfo[i].cwcDstExtra; + WCHAR volatile *pwchSlowDst = pwszDst; + int cwcDst2 = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, paArgInfo[i].cchSrc, + (WCHAR *)pwchSlowSrc, cwcLeft + 1); + assert(cwcDst2 >= 0); + + cBackslashes = 0; + while (cwcLeft-- > 0) + { + WCHAR wcSrc = *pwchSlowSrc++; + if (wcSrc != L'\\' && wcSrc != L'"') + cBackslashes = 0; + else if (wcSrc == L'\\') + cBackslashes++; + else if ( (fFlags & (MKWCCWCMD_F_CYGWIN_SHELL | MKWCCWCMD_F_HAVE_SH)) + == (MKWCCWCMD_F_CYGWIN_SHELL | MKWCCWCMD_F_HAVE_SH)) + { + *pwchSlowDst++ = L'"'; /* cygwin: '"' instead of '\\', no escaped slashes. */ + cBackslashes = 0; + } + else + { + if (!(fFlags & (MKWCCWCMD_F_CYGWIN_SHELL | MKWCCWCMD_F_MKS_SHELL))) + cBackslashes += 1; /* one extra escape the '"' and one for each preceeding slash. */ + while (cBackslashes > 0) + { + *pwchSlowDst++ = L'\\'; + cBackslashes--; + } + } + *pwchSlowDst++ = wcSrc; + assert((uintptr_t)pwchSlowDst <= (uintptr_t)pwchSlowSrc); + } + + if (paArgInfo[i].fEndSlashes) + while (cBackslashes-- > 0) + *pwchSlowDst++ = L'\\'; + + pwszDst += cwcDst; + assert(pwszDst == (WCHAR *)pwchSlowDst); + } + + if (paArgInfo[i].fExtraSpace) + *pwszDst++ = L' '; + if (paArgInfo[i].fQuoteIt) + *pwszDst++ = L'"'; + } + *pwszDst = L'\0'; + *ppwszCommandLine = pwszCmdLine; + return 0; +} + +static int mkWinChildcareWorkerConvertCommandlineWithShell(PWINCHILDCAREWORKER pWorker, const WCHAR *pwszShell, char **papszArgs, + WCHAR **ppwszCommandLine) +{ + MkWinChildError(pWorker, 1, "%s: not found!\n", papszArgs[0]); +//__debugbreak(); + return ERROR_FILE_NOT_FOUND; +} + +/** + * Searches the environment block for the PATH variable. + * + * @returns Pointer to the path in the block or "." in pwszPathFallback. + * @param pwszzEnv The UTF-16 environment block to search. + * @param pwszPathFallback Fallback. + */ +static const WCHAR *mkWinChildcareWorkerFindPathValue(const WCHAR *pwszzEnv, WCHAR pwszPathFallback[4]) +{ + while (*pwszzEnv) + { + size_t cwcVar = wcslen(pwszzEnv); + if (!IS_PATH_ENV_VAR(cwcVar, pwszzEnv)) + pwszzEnv += cwcVar + 1; + else if (cwcVar > 5) + return &pwszzEnv[5]; + else + break; + } + pwszPathFallback[0] = L'.'; + pwszPathFallback[1] = L'\0'; + return pwszPathFallback; +} + +/** + * Checks if we need to had this executable file to the shell. + * + * @returns TRUE if it's shell fooder, FALSE if we think windows can handle it. + * @param hFile Handle to the file in question + */ +static BOOL mkWinChildcareWorkerCheckIfNeedShell(HANDLE hFile) +{ + /* + * Read the first 512 bytes and check for an executable image header. + */ + union + { + DWORD dwSignature; + WORD wSignature; + BYTE ab[128]; + } uBuf; + DWORD cbRead; + uBuf.dwSignature = 0; + if ( ReadFile(hFile, &uBuf, sizeof(uBuf), &cbRead, NULL /*pOverlapped*/) + && cbRead == sizeof(uBuf)) + { + if (uBuf.wSignature == IMAGE_DOS_SIGNATURE) + return FALSE; + if (uBuf.dwSignature == IMAGE_NT_SIGNATURE) + return FALSE; + if ( uBuf.wSignature == IMAGE_OS2_SIGNATURE /* NE */ + || uBuf.wSignature == 0x5d4c /* LX */ + || uBuf.wSignature == IMAGE_OS2_SIGNATURE_LE /* LE */) + return FALSE; + } + return TRUE; +} + +/** + * Checks if the image path looks like microsoft CL.exe. + * + * @returns TRUE / FALSE. + * @param pwszImagePath The executable image path to evalutate. + * @param cwcImagePath The length of the image path. + */ +static BOOL mkWinChildIsProbableClExe(WCHAR const *pwszImagePath, size_t cwcImagePath) +{ + assert(pwszImagePath[cwcImagePath] == '\0'); + return cwcImagePath > 7 + && (pwszImagePath[cwcImagePath - 7] == L'/' || pwszImagePath[cwcImagePath - 7] == L'\\') + && (pwszImagePath[cwcImagePath - 6] == L'c' || pwszImagePath[cwcImagePath - 6] == L'C') + && (pwszImagePath[cwcImagePath - 5] == L'l' || pwszImagePath[cwcImagePath - 5] == L'L') + && pwszImagePath[cwcImagePath - 4] == L'.' + && (pwszImagePath[cwcImagePath - 3] == L'e' || pwszImagePath[cwcImagePath - 3] == L'E') + && (pwszImagePath[cwcImagePath - 2] == L'x' || pwszImagePath[cwcImagePath - 2] == L'X') + && (pwszImagePath[cwcImagePath - 1] == L'e' || pwszImagePath[cwcImagePath - 1] == L'E'); +} + +/** + * Temporary workaround for seemingly buggy kFsCache.c / dir-nt-bird.c. + * + * Something is not invalidated / updated correctly! + */ +static BOOL mkWinChildcareWorkerIsRegularFileW(PWINCHILDCAREWORKER pWorker, wchar_t const *pwszPath) +{ + BOOL fRet = FALSE; +#ifdef KMK + if (utf16_regular_file_p(pwszPath)) + fRet = TRUE; + else +#endif + { + /* Don't believe the cache. */ + DWORD dwAttr = GetFileAttributesW(pwszPath); + if (dwAttr != INVALID_FILE_ATTRIBUTES) + { + if (!(dwAttr & FILE_ATTRIBUTE_DIRECTORY)) + { +#ifdef KMK + extern void dir_cache_invalid_volatile(void); + dir_cache_invalid_volatile(); + if (utf16_regular_file_p(pwszPath)) + MkWinChildError(pWorker, 1, "kFsCache was out of sync! pwszPath=%S\n", pwszPath); + else + { + dir_cache_invalid_all(); + if (utf16_regular_file_p(pwszPath)) + MkWinChildError(pWorker, 1, "kFsCache was really out of sync! pwszPath=%S\n", pwszPath); + else + MkWinChildError(pWorker, 1, "kFsCache is really out of sync!! pwszPath=%S\n", pwszPath); + } +#endif + fRet = TRUE; + } + } + } + return fRet; +} + + +/** + * Tries to locate the image file, searching the path and maybe falling back on + * the shell in case it knows more (think cygwin with its own view of the file + * system). + * + * This will also check for shell script, falling back on the shell too to + * handle those. + * + * @returns 0 on success, windows error code on failure. + * @param pWorker The childcare worker. + * @param pszArg0 The first argument. + * @param pwszSearchPath In case mkWinChildcareWorkerConvertEnvironment + * had a chance of locating the search path already. + * @param pwszzEnv The environment block, in case we need to look for + * the path. + * @param pszShell The shell. + * @param ppwszImagePath Where to return the pointer to the image path. This + * could be the shell. + * @param pfNeedShell Where to return shell vs direct execution indicator. + * @param pfProbableClExe Where to return an indicator of probably CL.EXE. + */ +static int mkWinChildcareWorkerFindImage(PWINCHILDCAREWORKER pWorker, char const *pszArg0, WCHAR *pwszSearchPath, + WCHAR const *pwszzEnv, const char *pszShell, + WCHAR **ppwszImagePath, BOOL *pfNeedShell, BOOL *pfProbableClExe) +{ + /** @todo Slap a cache on this code. We usually end up executing the same + * stuff over and over again (e.g. compilers, linkers, etc). + * Hitting the file system is slow on windows. */ + + /* + * Convert pszArg0 to unicode so we can work directly on that. + */ + WCHAR wszArg0[MKWINCHILD_MAX_PATH + 4]; /* +4 for painless '.exe' appending */ + DWORD dwErr; + size_t cbArg0 = strlen(pszArg0) + 1; + int const cwcArg0 = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszArg0, cbArg0, wszArg0, MKWINCHILD_MAX_PATH); + if (cwcArg0 > 0) + { + HANDLE hFile = INVALID_HANDLE_VALUE; + WCHAR wszPathBuf[MKWINCHILD_MAX_PATH + 4]; /* +4 for painless '.exe' appending */ + int cwc; + + /* + * If there isn't an .exe suffix, we may have to add one. + * Also we ASSUME that .exe suffixes means no hash bang detection needed. + */ + int const fHasExeSuffix = cwcArg0 > CSTRLEN(".exe") + && wszArg0[cwcArg0 - 4] == '.' + && (wszArg0[cwcArg0 - 3] == L'e' || wszArg0[cwcArg0 - 3] == L'E') + && (wszArg0[cwcArg0 - 2] == L'x' || wszArg0[cwcArg0 - 2] == L'X') + && (wszArg0[cwcArg0 - 1] == L'e' || wszArg0[cwcArg0 - 1] == L'E'); + + /* + * If there isn't any path specified, we need to search the PATH env.var. + */ + int const fHasPath = wszArg0[1] == L':' + || wszArg0[0] == L'\\' + || wszArg0[0] == L'/' + || wmemchr(wszArg0, L'/', cwcArg0) + || wmemchr(wszArg0, L'\\', cwcArg0); + + /* Before we do anything, flip UNIX slashes to DOS ones. */ + WCHAR *pwc = wszArg0; + while ((pwc = wcschr(pwc, L'/')) != NULL) + *pwc++ = L'\\'; + + /* Don't need to set these all the time... */ + *pfNeedShell = FALSE; + *pfProbableClExe = FALSE; + + /* + * If any kind of path is specified in arg0, we will not search the + * PATH env.var and can limit ourselves to maybe slapping a .exe on to it. + */ + if (fHasPath) + { + /* + * If relative to a CWD, turn it into an absolute one. + */ + unsigned cwcPath = cwcArg0; + WCHAR *pwszPath = wszArg0; + if ( *pwszPath != L'\\' + && (pwszPath[1] != ':' || pwszPath[2] != L'\\') ) + { + DWORD cwcAbsPath = GetFullPathNameW(wszArg0, MKWINCHILD_MAX_PATH, wszPathBuf, NULL); + if (cwcAbsPath > 0) + { + cwcPath = cwcAbsPath + 1; /* include terminator, like MultiByteToWideChar does. */ + pwszPath = wszPathBuf; + } + } + + /* + * Check with .exe suffix first. + * We don't open .exe files and look for hash bang stuff, we just + * assume they are executable images that CreateProcess can deal with. + */ + if (!fHasExeSuffix) + { + pwszPath[cwcPath - 1] = L'.'; + pwszPath[cwcPath ] = L'e'; + pwszPath[cwcPath + 1] = L'x'; + pwszPath[cwcPath + 2] = L'e'; + pwszPath[cwcPath + 3] = L'\0'; + } + + if (mkWinChildcareWorkerIsRegularFileW(pWorker, pwszPath)) + { + *pfProbableClExe = mkWinChildIsProbableClExe(pwszPath, cwcPath + 4 - 1); + return mkWinChildDuplicateUtf16String(pwszPath, cwcPath + 4, ppwszImagePath); + } + + /* + * If no suffix was specified, try without .exe too, but now we need + * to see if it's for the shell or CreateProcess. + */ + if (!fHasExeSuffix) + { + pwszPath[cwcPath - 1] = L'\0'; +#ifdef KMK + if (mkWinChildcareWorkerIsRegularFileW(pWorker, pwszPath)) +#endif + { + hFile = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + NULL /*pSecAttr*/, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile != INVALID_HANDLE_VALUE) + { + *pfNeedShell = mkWinChildcareWorkerCheckIfNeedShell(hFile); + CloseHandle(hFile); + if (!*pfNeedShell) + { + *pfProbableClExe = mkWinChildIsProbableClExe(pwszPath, cwcPath - 1); + return mkWinChildDuplicateUtf16String(pwszPath, cwcPath, ppwszImagePath); + } + } + } + } + } + /* + * No path, need to search the PATH env.var. for the executable, maybe + * adding an .exe suffix while do so if that is missing. + */ + else + { + BOOL fSearchedCwd = FALSE; + WCHAR wszPathFallback[4]; + if (!pwszSearchPath) + pwszSearchPath = (WCHAR *)mkWinChildcareWorkerFindPathValue(pwszzEnv, wszPathFallback); + + for (;;) + { + size_t cwcCombined; + + /* + * Find the end of the current PATH component. + */ + size_t cwcSkip; + WCHAR wcEnd; + size_t cwcComponent = 0; + WCHAR wc; + while ((wc = pwszSearchPath[cwcComponent]) != L'\0') + { + if (wc != ';' && wc != ':') + { /* likely */ } + else if (wc == ';') + break; + else if (cwcComponent != (pwszSearchPath[cwcComponent] != L'"' ? 1 : 2)) + break; + cwcComponent++; + } + wcEnd = wc; + + /* Trim leading spaces and double quotes. */ + while ( cwcComponent > 0 + && ((wc = *pwszSearchPath) == L'"' || wc == L' ' || wc == L'\t')) + { + pwszSearchPath++; + cwcComponent--; + } + cwcSkip = cwcComponent; + + /* Trim trailing spaces & double quotes. */ + while ( cwcComponent > 0 + && ((wc = pwszSearchPath[cwcComponent - 1]) == L'"' || wc == L' ' || wc == L'\t')) + cwcComponent--; + + /* + * Skip empty components. Join the component and the filename, making sure to + * resolve any CWD relative stuff first. + */ + cwcCombined = cwcComponent + 1 + cwcArg0; + if (cwcComponent > 0 && cwcCombined <= MKWINCHILD_MAX_PATH) + { + /* Copy the component into wszPathBuf, maybe abspath'ing it. */ + DWORD cwcAbsPath = 0; + if ( *pwszSearchPath != L'\\' + && (pwszSearchPath[1] != ':' || pwszSearchPath[2] != L'\\') ) + { + /* To save an extra buffer + copying, we'll temporarily modify the PATH + value in our converted UTF-16 environment block. */ + WCHAR const wcSaved = pwszSearchPath[cwcComponent]; + pwszSearchPath[cwcComponent] = L'\0'; + cwcAbsPath = GetFullPathNameW(pwszSearchPath, MKWINCHILD_MAX_PATH, wszPathBuf, NULL); + pwszSearchPath[cwcComponent] = wcSaved; + if (cwcAbsPath > 0 && cwcAbsPath + 1 + cwcArg0 <= MKWINCHILD_MAX_PATH) + cwcCombined = cwcAbsPath + 1 + cwcArg0; + else + cwcAbsPath = 0; + } + if (cwcAbsPath == 0) + { + memcpy(wszPathBuf, pwszSearchPath, cwcComponent * sizeof(WCHAR)); + cwcAbsPath = cwcComponent; + } + + /* Append the filename. */ + if ((wc = wszPathBuf[cwcAbsPath - 1]) == L'\\' || wc == L'/' || wc == L':') + { + memcpy(&wszPathBuf[cwcAbsPath], wszArg0, cwcArg0 * sizeof(WCHAR)); + cwcCombined--; + } + else + { + wszPathBuf[cwcAbsPath] = L'\\'; + memcpy(&wszPathBuf[cwcAbsPath + 1], wszArg0, cwcArg0 * sizeof(WCHAR)); + } + assert(wszPathBuf[cwcCombined - 1] == L'\0'); + + /* DOS slash conversion */ + pwc = wszPathBuf; + while ((pwc = wcschr(pwc, L'/')) != NULL) + *pwc++ = L'\\'; + + /* + * Search with exe suffix first. + */ + if (!fHasExeSuffix) + { + wszPathBuf[cwcCombined - 1] = L'.'; + wszPathBuf[cwcCombined ] = L'e'; + wszPathBuf[cwcCombined + 1] = L'x'; + wszPathBuf[cwcCombined + 2] = L'e'; + wszPathBuf[cwcCombined + 3] = L'\0'; + } + if (mkWinChildcareWorkerIsRegularFileW(pWorker, wszPathBuf)) + { + *pfProbableClExe = mkWinChildIsProbableClExe(wszPathBuf, cwcCombined + (fHasExeSuffix ? 0 : 4) - 1); + return mkWinChildDuplicateUtf16String(wszPathBuf, cwcCombined + (fHasExeSuffix ? 0 : 4), ppwszImagePath); + } + if (!fHasExeSuffix) + { + wszPathBuf[cwcCombined - 1] = L'\0'; +#ifdef KMK + if (mkWinChildcareWorkerIsRegularFileW(pWorker, wszPathBuf)) +#endif + { + /* + * Check if the file exists w/o the added '.exe' suffix. If it does, + * we need to check if we can pass it to CreateProcess or need the shell. + */ + hFile = CreateFileW(wszPathBuf, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + NULL /*pSecAttr*/, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile != INVALID_HANDLE_VALUE) + { + *pfNeedShell = mkWinChildcareWorkerCheckIfNeedShell(hFile); + CloseHandle(hFile); + if (!*pfNeedShell) + { + *pfProbableClExe = mkWinChildIsProbableClExe(wszPathBuf, cwcCombined - 1); + return mkWinChildDuplicateUtf16String(wszPathBuf, cwcCombined, ppwszImagePath); + } + break; + } + } + } + } + + /* + * Advance to the next component. + */ + if (wcEnd != '\0') + pwszSearchPath += cwcSkip + 1; + else if (fSearchedCwd) + break; + else + { + fSearchedCwd = TRUE; + wszPathFallback[0] = L'.'; + wszPathFallback[1] = L'\0'; + pwszSearchPath = wszPathFallback; + } + } + } + + /* + * We need the shell. It will take care of finding/reporting missing + * image files and such. + */ + *pfNeedShell = TRUE; + if (pszShell) + { + cwc = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszShell, strlen(pszShell) + 1, wszPathBuf, MKWINCHILD_MAX_PATH); + if (cwc > 0) + return mkWinChildDuplicateUtf16String(wszPathBuf, cwc, ppwszImagePath); + dwErr = GetLastError(); + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert shell (%s): %u\n"), pszShell, dwErr); + } + else + { + MkWinChildError(pWorker, 1, "%s: not found!\n", pszArg0); + dwErr = ERROR_FILE_NOT_FOUND; + } + } + else + { + dwErr = GetLastError(); + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert argv[0] (%s): %u\n"), pszArg0, dwErr); + } + return dwErr == ERROR_INSUFFICIENT_BUFFER ? ERROR_FILENAME_EXCED_RANGE : dwErr; +} + +/** + * Creates the environment block. + * + * @returns 0 on success, windows error code on failure. + * @param pWorker The childcare worker if on one, otherwise NULL. + * @param papszEnv The environment vector to convert. + * @param cbEnvStrings The size of the environment strings, iff they are + * sequential in a block. Otherwise, zero. + * @param ppwszEnv Where to return the pointer to the environment + * block. + * @param ppwszSearchPath Where to return the pointer to the path value + * within the environment block. This will not be set + * if cbEnvStrings is non-zero, more efficient to let + * mkWinChildcareWorkerFindImage() search when needed. + */ +static int mkWinChildcareWorkerConvertEnvironment(PWINCHILDCAREWORKER pWorker, char **papszEnv, size_t cbEnvStrings, + WCHAR **ppwszEnv, WCHAR const **ppwszSearchPath) +{ + DWORD dwErr; + int cwcRc; + int cwcDst; + WCHAR *pwszzDst; + + *ppwszSearchPath = NULL; + + /* + * We've got a little optimization here with help from mkWinChildCopyStringArray. + */ + if (cbEnvStrings) + { + cwcDst = cbEnvStrings + 32; + pwszzDst = (WCHAR *)xmalloc(cwcDst * sizeof(WCHAR)); + cwcRc = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, papszEnv[0], cbEnvStrings, pwszzDst, cwcDst); + if (cwcRc != 0) + { + *ppwszEnv = pwszzDst; + return 0; + } + + /* Resize the allocation and try again. */ + dwErr = GetLastError(); + if (dwErr == ERROR_INSUFFICIENT_BUFFER) + { + cwcRc = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, papszEnv[0], cbEnvStrings, NULL, 0); + if (cwcRc > 0) + cwcDst = cwcRc + 32; + else + cwcDst *= 2; + pwszzDst = (WCHAR *)xrealloc(pwszzDst, cwcDst); + cwcRc = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, papszEnv[0], cbEnvStrings, pwszzDst, cwcDst); + if (cwcRc != 0) + { + *ppwszEnv = pwszzDst; + return 0; + } + dwErr = GetLastError(); + } + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert environment block: %u\n"), dwErr); + } + /* + * Need to convert it string by string. + */ + else + { + size_t offPathValue = ~(size_t)0; + size_t offDst; + + /* + * Estimate the size first. + */ + size_t cEnvVars; + size_t cwcDst = 32; + size_t iVar = 0; + const char *pszSrc; + while ((pszSrc = papszEnv[iVar]) != NULL) + { + cwcDst += strlen(pszSrc) + 1; + iVar++; + } + cEnvVars = iVar; + + /* Allocate estimated WCHARs and convert the variables one by one, reallocating + the block as needed. */ + pwszzDst = (WCHAR *)xmalloc(cwcDst * sizeof(WCHAR)); + cwcDst--; /* save one wchar for the terminating empty string. */ + offDst = 0; + for (iVar = 0; iVar < cEnvVars; iVar++) + { + size_t cwcLeft = cwcDst - offDst; + size_t const cbSrc = strlen(pszSrc = papszEnv[iVar]) + 1; + assert(cwcDst >= offDst); + + + cwcRc = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, cbSrc, &pwszzDst[offDst], cwcLeft); + if (cwcRc > 0) + { /* likely */ } + else + { + dwErr = GetLastError(); + if (dwErr == ERROR_INSUFFICIENT_BUFFER) + { + /* Need more space. So, calc exacly how much and resize the block accordingly. */ + size_t cbSrc2 = cbSrc; + size_t iVar2 = iVar; + cwcLeft = 1; + for (;;) + { + size_t cwcRc2 = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, cbSrc, NULL, 0); + if (cwcRc2 > 0) + cwcLeft += cwcRc2; + else + cwcLeft += cbSrc * 4; + + /* advance */ + iVar2++; + if (iVar2 >= cEnvVars) + break; + pszSrc = papszEnv[iVar2]; + cbSrc2 = strlen(pszSrc) + 1; + } + pszSrc = papszEnv[iVar]; + + /* Grow the allocation and repeat the conversion. */ + if (offDst + cwcLeft > cwcDst + 1) + { + cwcDst = offDst + cwcLeft; + pwszzDst = (WCHAR *)xrealloc(pwszzDst, cwcDst * sizeof(WCHAR)); + cwcDst--; /* save one wchar for the terminating empty string. */ + cwcRc = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszSrc, cbSrc, &pwszzDst[offDst], cwcLeft - 1); + if (cwcRc <= 0) + dwErr = GetLastError(); + } + } + if (cwcRc <= 0) + { + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert environment string #%u (%s): %u\n"), + iVar, pszSrc, dwErr); + free(pwszzDst); + return dwErr; + } + } + + /* Look for the PATH. */ + if ( offPathValue == ~(size_t)0 + && IS_PATH_ENV_VAR(cwcRc, &pwszzDst[offDst]) ) + offPathValue = offDst + 4 + 1; + + /* Advance. */ + offDst += cwcRc; + } + pwszzDst[offDst++] = '\0'; + + if (offPathValue != ~(size_t)0) + *ppwszSearchPath = &pwszzDst[offPathValue]; + *ppwszEnv = pwszzDst; + return 0; + } + free(pwszzDst); + return dwErr; +} + +/** + * Childcare worker: handle regular process. + * + * @param pWorker The worker. + * @param pChild The kSubmit child. + */ +static void mkWinChildcareWorkerThreadHandleProcess(PWINCHILDCAREWORKER pWorker, PWINCHILD pChild) +{ + WCHAR *pwszSearchPath = NULL; + WCHAR *pwszzEnvironment = NULL; + WCHAR *pwszCommandLine = NULL; + WCHAR *pwszImageName = NULL; + BOOL fNeedShell = FALSE; + BOOL fProbableClExe = FALSE; + int rc; + + /* + * First we convert the environment so we get the PATH we need to + * search for the executable. + */ + rc = mkWinChildcareWorkerConvertEnvironment(pWorker, pChild->u.Process.papszEnv ? pChild->u.Process.papszEnv : environ, + pChild->u.Process.cbEnvStrings, + &pwszzEnvironment, &pwszSearchPath); + /* + * Find the executable and maybe checking if it's a shell script, then + * convert it to a command line. + */ + if (rc == 0) + rc = mkWinChildcareWorkerFindImage(pWorker, pChild->u.Process.papszArgs[0], pwszSearchPath, pwszzEnvironment, + pChild->u.Process.pszShell, &pwszImageName, &fNeedShell, &pChild->fProbableClExe); + if (rc == 0) + { + if (!fNeedShell) + rc = mkWinChildcareWorkerConvertCommandline(pWorker, pChild->u.Process.papszArgs, 0 /*fFlags*/, &pwszCommandLine); + else + rc = mkWinChildcareWorkerConvertCommandlineWithShell(pWorker, pwszImageName, pChild->u.Process.papszArgs, + &pwszCommandLine); + + /* + * Create the child process. + */ + if (rc == 0) + { + BOOL afReplace[3] = { FALSE, pChild->u.Process.hStdOut != INVALID_HANDLE_VALUE, pChild->u.Process.hStdErr != INVALID_HANDLE_VALUE }; + HANDLE ahChild[3] = { INVALID_HANDLE_VALUE, pChild->u.Process.hStdOut, pChild->u.Process.hStdErr }; + rc = mkWinChildcareWorkerCreateProcess(pWorker, pwszImageName, pwszCommandLine, pwszzEnvironment, + NULL /*pwszCwd*/, afReplace, ahChild, pChild->u.Process.fCatchOutput, + &pChild->u.Process.hProcess); + mkWinChildcareWorkerCloseStandardHandles(pChild); + if (rc == 0) + { + /* + * Wait for the child to complete. + */ + mkWinChildcareWorkerWaitForProcess(pWorker, pChild, pChild->u.Process.hProcess, pwszImageName, + pChild->u.Process.fCatchOutput); + } + else + pChild->iExitCode = rc; + } + else + pChild->iExitCode = rc; + } + else + pChild->iExitCode = rc; + free(pwszCommandLine); + free(pwszImageName); + free(pwszzEnvironment); + + /* In case we failed, we must make sure the child end of pipes + used by $(shell no_such_command.exe) are closed, otherwise + the main thread will be stuck reading the parent end. */ + mkWinChildcareWorkerCloseStandardHandles(pChild); +} + +#ifdef KMK + +/** + * Childcare worker: handle builtin command. + * + * @param pWorker The worker. + * @param pChild The kSubmit child. + */ +static void mkWinChildcareWorkerThreadHandleBuiltIn(PWINCHILDCAREWORKER pWorker, PWINCHILD pChild) +{ + PCKMKBUILTINENTRY pBuiltIn = pChild->u.BuiltIn.pBuiltIn; + KMKBUILTINCTX Ctx = + { + pBuiltIn->uName.s.sz, + pChild->pMkChild ? &pChild->pMkChild->output : NULL, + pWorker, + }; + if (pBuiltIn->uFnSignature == FN_SIG_MAIN) + pChild->iExitCode = pBuiltIn->u.pfnMain(pChild->u.BuiltIn.cArgs, pChild->u.BuiltIn.papszArgs, + pChild->u.BuiltIn.papszEnv, &Ctx); + else if (pBuiltIn->uFnSignature == FN_SIG_MAIN_SPAWNS) + pChild->iExitCode = pBuiltIn->u.pfnMainSpawns(pChild->u.BuiltIn.cArgs, pChild->u.BuiltIn.papszArgs, + pChild->u.BuiltIn.papszEnv, &Ctx, pChild->pMkChild, NULL /*pPid*/); + else + { + assert(0); + pChild->iExitCode = 98; + } +} + +/** + * Childcare worker: handle append write-out. + * + * @param pWorker The worker. + * @param pChild The kSubmit child. + */ +static void mkWinChildcareWorkerThreadHandleAppend(PWINCHILDCAREWORKER pWorker, PWINCHILD pChild) +{ + int fd = open(pChild->u.Append.pszFilename, + pChild->u.Append.fTruncate + ? O_WRONLY | O_TRUNC | O_CREAT | _O_NOINHERIT | _O_BINARY + : O_WRONLY | O_APPEND | O_CREAT | _O_NOINHERIT | _O_BINARY, + 0666); + if (fd >= 0) + { + ssize_t cbWritten = write(fd, pChild->u.Append.pszAppend, pChild->u.Append.cbAppend); + if (cbWritten == (ssize_t)pChild->u.Append.cbAppend) + { + if (close(fd) >= 0) + { + pChild->iExitCode = 0; + return; + } + MkWinChildError(pWorker, 1, "kmk_builtin_append: close failed on '%s': %u (%s)\n", + pChild->u.Append.pszFilename, errno, strerror(errno)); + } + else + MkWinChildError(pWorker, 1, "kmk_builtin_append: error writing %lu bytes to on '%s': %u (%s)\n", + pChild->u.Append.cbAppend, pChild->u.Append.pszFilename, errno, strerror(errno)); + close(fd); + } + else + MkWinChildError(pWorker, 1, "kmk_builtin_append: error opening '%s': %u (%s)\n", + pChild->u.Append.pszFilename, errno, strerror(errno)); + pChild->iExitCode = 1; +} + +/** + * Childcare worker: handle kSubmit job. + * + * @param pWorker The worker. + * @param pChild The kSubmit child. + */ +static void mkWinChildcareWorkerThreadHandleSubmit(PWINCHILDCAREWORKER pWorker, PWINCHILD pChild) +{ + void *pvSubmitWorker = pChild->u.Submit.pvSubmitWorker; + + /* + * Prep the wait handles. + */ + HANDLE ahHandles[3] = { pChild->u.Submit.hEvent, NULL, NULL }; + DWORD cHandles = 1; + if (pChild->u.Submit.pStdOut) + { + assert(pChild->u.Submit.pStdErr); + pChild->u.Submit.pStdOut->fHaveWrittenOut = FALSE; + ahHandles[cHandles++] = pChild->u.Submit.pStdOut->hEvent; + pChild->u.Submit.pStdErr->fHaveWrittenOut = FALSE; + ahHandles[cHandles++] = pChild->u.Submit.pStdErr->hEvent; + } + + /* + * Wait loop. + */ + for (;;) + { + int iExitCode = -42; + int iSignal = -1; + DWORD dwStatus; + if (cHandles == 1) + dwStatus = WaitForSingleObject(ahHandles[0], INFINITE); + else + { + dwStatus = WaitForMultipleObjects(cHandles, ahHandles, FALSE /*fWaitAll*/, INFINITE); + assert(dwStatus != WAIT_FAILED); + if (dwStatus == WAIT_OBJECT_0 + 1) + mkWinChildcareWorkerCatchOutput(pChild, pChild->u.Submit.pStdOut, FALSE /*fDraining*/); + else if (dwStatus == WAIT_OBJECT_0 + 2) + mkWinChildcareWorkerCatchOutput(pChild, pChild->u.Submit.pStdErr, FALSE /*fDraining*/); + } + if (kSubmitSubProcGetResult((intptr_t)pvSubmitWorker, dwStatus == WAIT_OBJECT_0 /*fBlock*/, &iExitCode, &iSignal) == 0) + { + if (pChild->u.Submit.pStdOut) + MkWinChildcareWorkerDrainPipes(pChild, pChild->u.Submit.pStdOut, pChild->u.Submit.pStdErr); + + pChild->iExitCode = iExitCode; + pChild->iSignal = iSignal; + /* Cleanup must be done on the main thread. */ + return; + } + + if (pChild->iSignal != 0) + kSubmitSubProcKill((intptr_t)pvSubmitWorker, pChild->iSignal); + } +} + +/** + * Childcare worker: handle kmk_redirect process. + * + * @param pWorker The worker. + * @param pChild The redirect child. + */ +static void mkWinChildcareWorkerThreadHandleRedirect(PWINCHILDCAREWORKER pWorker, PWINCHILD pChild) +{ + mkWinChildcareWorkerWaitForProcess(pWorker, pChild, pChild->u.Redirect.hProcess, L"kmk_redirect", FALSE /*fCatchOutput*/); +} + +#endif /* KMK */ + +/** + * Childcare worker thread. + * + * @returns 0 + * @param pvUser The worker instance. + */ +static unsigned int __stdcall mkWinChildcareWorkerThread(void *pvUser) +{ + PWINCHILDCAREWORKER pWorker = (PWINCHILDCAREWORKER)pvUser; + assert(pWorker->uMagic == WINCHILDCAREWORKER_MAGIC); + +#ifdef MKWINCHILD_DO_SET_PROCESSOR_GROUP + /* + * Adjust process group if necessary. + * + * Note! It seems that setting the mask to zero means that we select all + * active processors. Couldn't find any alternative API for getting + * the correct active processor mask. + */ + if (g_cProcessorGroups > 1) + { + GROUP_AFFINITY Affinity = { 0 /* == all active apparently */, pWorker->iProcessorGroup, { 0, 0, 0 } }; + BOOL fRet = g_pfnSetThreadGroupAffinity(GetCurrentThread(), &Affinity, NULL); + assert(fRet); (void)fRet; +# ifndef NDEBUG + { + GROUP_AFFINITY ActualAffinity = { 0xbeefbeefU, 0xbeef, { 0xbeef, 0xbeef, 0xbeef } }; + fRet = GetThreadGroupAffinity(GetCurrentThread(), &ActualAffinity); + assert(fRet); (void)fRet; + assert(ActualAffinity.Group == pWorker->iProcessorGroup); + } +# endif + } +#endif + + /* + * Work loop. + */ + while (!g_fShutdown) + { + /* + * Try go idle. + */ + PWINCHILD pChild = pWorker->pTailTodoChildren; + if (!pChild) + { + _InterlockedExchange(&pWorker->fIdle, TRUE); + pChild = pWorker->pTailTodoChildren; + if (!pChild) + { + DWORD dwStatus; + + _InterlockedIncrement((long *)&g_cIdleChildcareWorkers); + _InterlockedExchange((long *)&g_idxLastChildcareWorker, pWorker->idxWorker); + dwStatus = WaitForSingleObject(pWorker->hEvtIdle, INFINITE); + _InterlockedExchange(&pWorker->fIdle, FALSE); + _InterlockedDecrement((long *)&g_cIdleChildcareWorkers); + + assert(dwStatus != WAIT_FAILED); + if (dwStatus == WAIT_FAILED) + Sleep(20); + + pChild = pWorker->pTailTodoChildren; + } + else + _InterlockedExchange(&pWorker->fIdle, FALSE); + } + if (pChild) + { + /* + * We got work to do. First job is to deque the job. + */ + pChild = mkWinChildDequeFromLifo(&pWorker->pTailTodoChildren, pChild); + assert(pChild); + if (pChild) + { + PWINCHILD pTailExpect; + + pChild->pWorker = pWorker; + pWorker->pCurChild = pChild; + switch (pChild->enmType) + { + case WINCHILDTYPE_PROCESS: + mkWinChildcareWorkerThreadHandleProcess(pWorker, pChild); + break; +#ifdef KMK + case WINCHILDTYPE_BUILT_IN: + mkWinChildcareWorkerThreadHandleBuiltIn(pWorker, pChild); + break; + case WINCHILDTYPE_APPEND: + mkWinChildcareWorkerThreadHandleAppend(pWorker, pChild); + break; + case WINCHILDTYPE_SUBMIT: + mkWinChildcareWorkerThreadHandleSubmit(pWorker, pChild); + break; + case WINCHILDTYPE_REDIRECT: + mkWinChildcareWorkerThreadHandleRedirect(pWorker, pChild); + break; +#endif + default: + assert(0); + } + pWorker->pCurChild = NULL; + pChild->pWorker = NULL; + + /* + * Move the child to the completed list. + */ + pTailExpect = NULL; + for (;;) + { + PWINCHILD pTailActual; + pChild->pNext = pTailExpect; + pTailActual = _InterlockedCompareExchangePointer(&g_pTailCompletedChildren, pChild, pTailExpect); + if (pTailActual != pTailExpect) + pTailExpect = pTailActual; + else + { + _InterlockedDecrement(&g_cPendingChildren); + if (pTailExpect) + break; + if (SetEvent(g_hEvtWaitChildren)) + break; + MkWinChildError(pWorker, 1, "SetEvent(g_hEvtWaitChildren=%p) failed: %u\n", + g_hEvtWaitChildren, GetLastError()); + break; + } + } + } + } + } + + _endthreadex(0); + return 0; +} + +/** + * Creates a pipe for catching child output. + * + * This is a custom CreatePipe implementation that allows for overlapped I/O on + * our end of the pipe. Silly that they don't offer an API that does this. + * + * @returns The pipe that was created. NULL on failure. + * @param pPipe The structure for the pipe. + * @param iWhich Which standard descriptor this is a pipe for. + * @param idxWorker The worker index. + */ +PWINCCWPIPE MkWinChildcareCreateWorkerPipe(unsigned iWhich, unsigned int idxWorker) +{ + /* + * We try generate a reasonably unique name from the get go, so this retry + * loop shouldn't really ever be needed. But you never know. + */ + static unsigned s_iSeqNo = 0; + DWORD const cMaxInstances = 1; + DWORD const cbPipe = 4096; + DWORD const cMsTimeout = 0; + unsigned cTries = 256; + while (cTries-- > 0) + { + /* Create the pipe (our end). */ + HANDLE hPipeRead; + DWORD fOpenMode = PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE; + DWORD fPipeMode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS; + WCHAR wszName[MAX_PATH]; + s_iSeqNo++; + _snwprintf(wszName, MAX_PATH, L"\\\\.\\pipe\\kmk-winchildren-%u-%u-%u-%s-%u-%u", + GetCurrentProcessId(), GetCurrentThreadId(), idxWorker, iWhich == 1 ? L"out" : L"err", s_iSeqNo, GetTickCount()); + hPipeRead = CreateNamedPipeW(wszName, fOpenMode, fPipeMode, cMaxInstances, cbPipe, cbPipe, cMsTimeout, NULL /*pSecAttr*/); + if (hPipeRead == INVALID_HANDLE_VALUE && GetLastError() == ERROR_INVALID_PARAMETER) + { + fOpenMode &= ~FILE_FLAG_FIRST_PIPE_INSTANCE; + fPipeMode &= ~PIPE_REJECT_REMOTE_CLIENTS; + hPipeRead = CreateNamedPipeW(wszName, fOpenMode, fPipeMode, cMaxInstances, cbPipe, cbPipe, cMsTimeout, NULL /*pSecAttr*/); + } + if (hPipeRead != INVALID_HANDLE_VALUE) + { + /* Connect the other end. */ + HANDLE hPipeWrite = CreateFileW(wszName, GENERIC_WRITE | FILE_READ_ATTRIBUTES, 0 /*fShareMode*/, NULL /*pSecAttr*/, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL /*hTemplateFile*/); + if (hPipeWrite != INVALID_HANDLE_VALUE) + { + /* + * Create the event object and we're done. + * + * It starts in signalled stated so we don't need special code + * for handing when we start waiting. + */ + HANDLE hEvent = CreateEventW(NULL /*pSecAttr*/, TRUE /*fManualReset*/, TRUE /*fInitialState*/, NULL /*pwszName*/); + if (hEvent != NULL) + { + PWINCCWPIPE pPipe = (PWINCCWPIPE)xcalloc(sizeof(*pPipe)); + pPipe->hPipeMine = hPipeRead; + pPipe->hPipeChild = hPipeWrite; + pPipe->hEvent = hEvent; + pPipe->iWhich = iWhich; + pPipe->fReadPending = FALSE; + pPipe->cbBuffer = cbPipe; + pPipe->pbBuffer = xcalloc(cbPipe); + return pPipe; + } + + CloseHandle(hPipeWrite); + CloseHandle(hPipeRead); + return NULL; + } + CloseHandle(hPipeRead); + } + } + return NULL; +} + +/** + * Destroys a childcare worker pipe. + * + * @param pPipe The pipe. + */ +void MkWinChildcareDeleteWorkerPipe(PWINCCWPIPE pPipe) +{ + if (pPipe->hPipeChild != NULL) + { + CloseHandle(pPipe->hPipeChild); + pPipe->hPipeChild = NULL; + } + + if (pPipe->hPipeMine != NULL) + { + if (pPipe->fReadPending) + if (!CancelIo(pPipe->hPipeMine)) + WaitForSingleObject(pPipe->hEvent, INFINITE); + CloseHandle(pPipe->hPipeMine); + pPipe->hPipeMine = NULL; + } + + if (pPipe->hEvent != NULL) + { + CloseHandle(pPipe->hEvent); + pPipe->hEvent = NULL; + } + + if (pPipe->pbBuffer) + { + free(pPipe->pbBuffer); + pPipe->pbBuffer = NULL; + } +} + +/** + * Initializes the processor group allocator. + * + * @param pState The allocator to initialize. + */ +void MkWinChildInitCpuGroupAllocator(PMKWINCHILDCPUGROUPALLOCSTATE pState) +{ + /* We shift the starting group with the make nesting level as part of + our very simple distribution strategy. */ + pState->idxGroup = makelevel; + pState->idxProcessorInGroup = 0; +} + +/** + * Allocate CPU group for the next child process. + * + * @returns CPU group. + * @param pState The allocator state. Must be initialized by + * MkWinChildInitCpuGroupAllocator(). + */ +unsigned int MkWinChildAllocateCpuGroup(PMKWINCHILDCPUGROUPALLOCSTATE pState) +{ + unsigned int iGroup = 0; + if (g_cProcessorGroups > 1) + { + unsigned int cMaxInGroup; + unsigned int cInGroup; + + iGroup = pState->idxGroup % g_cProcessorGroups; + + /* Advance. We employ a very simple strategy that does 50% in + each group for each group cycle. Odd processor counts are + caught in odd group cycles. The init function selects the + starting group based on make nesting level to avoid stressing + out the first group. */ + cInGroup = ++pState->idxProcessorInGroup; + cMaxInGroup = g_pacProcessorsInGroup[iGroup]; + if ( !(cMaxInGroup & 1) + || !((pState->idxGroup / g_cProcessorGroups) & 1)) + cMaxInGroup /= 2; + else + cMaxInGroup = cMaxInGroup / 2 + 1; + if (cInGroup >= cMaxInGroup) + { + pState->idxProcessorInGroup = 0; + pState->idxGroup++; + } + } + return iGroup; +} + +/** + * Creates another childcare worker. + * + * @returns The new worker, if we succeeded. + */ +static PWINCHILDCAREWORKER mkWinChildcareCreateWorker(void) +{ + PWINCHILDCAREWORKER pWorker = (PWINCHILDCAREWORKER)xcalloc(sizeof(*pWorker)); + pWorker->uMagic = WINCHILDCAREWORKER_MAGIC; + pWorker->idxWorker = g_cChildCareworkers; + pWorker->hEvtIdle = CreateEventW(NULL, FALSE /*fManualReset*/, FALSE /*fInitialState*/, NULL /*pwszName*/); + if (pWorker->hEvtIdle) + { + pWorker->pStdOut = MkWinChildcareCreateWorkerPipe(1, pWorker->idxWorker); + if (pWorker->pStdOut) + { + pWorker->pStdErr = MkWinChildcareCreateWorkerPipe(2, pWorker->idxWorker); + if (pWorker->pStdErr) + { + /* Before we start the thread, assign it to a processor group. */ + pWorker->iProcessorGroup = MkWinChildAllocateCpuGroup(&g_ProcessorGroupAllocator); + + /* Try start the thread. */ + pWorker->hThread = (HANDLE)_beginthreadex(NULL, 0 /*cbStack*/, mkWinChildcareWorkerThread, pWorker, + 0, &pWorker->tid); + if (pWorker->hThread != NULL) + { + pWorker->idxWorker = g_cChildCareworkers++; /* paranoia */ + g_papChildCareworkers[pWorker->idxWorker] = pWorker; + return pWorker; + } + + /* Bail out! */ + ONS (error, NILF, "_beginthreadex failed: %u (%s)\n", errno, strerror(errno)); + MkWinChildcareDeleteWorkerPipe(pWorker->pStdErr); + } + else + ON (error, NILF, "Failed to create stderr pipe: %u\n", GetLastError()); + MkWinChildcareDeleteWorkerPipe(pWorker->pStdOut); + } + else + ON (error, NILF, "Failed to create stdout pipe: %u\n", GetLastError()); + CloseHandle(pWorker->hEvtIdle); + } + else + ON (error, NILF, "CreateEvent failed: %u\n", GetLastError()); + pWorker->uMagic = ~WINCHILDCAREWORKER_MAGIC; + free(pWorker); + return NULL; +} + +/** + * Helper for copying argument and environment vectors. + * + * @returns Single alloc block copy. + * @param papszSrc The source vector. + * @param pcbStrings Where to return the size of the strings & terminator. + */ +static char **mkWinChildCopyStringArray(char **papszSrc, size_t *pcbStrings) +{ + const char *psz; + char **papszDstArray; + char *pszDstStr; + size_t i; + + /* Calc sizes first. */ + size_t cbStrings = 1; /* (one extra for terminator string) */ + size_t cStrings = 0; + while ((psz = papszSrc[cStrings]) != NULL) + { + cbStrings += strlen(psz) + 1; + cStrings++; + } + *pcbStrings = cbStrings; + + /* Allocate destination. */ + papszDstArray = (char **)xmalloc(cbStrings + (cStrings + 1) * sizeof(papszDstArray[0])); + pszDstStr = (char *)&papszDstArray[cStrings + 1]; + + /* Copy it. */ + for (i = 0; i < cStrings; i++) + { + const char *pszSource = papszSrc[i]; + size_t cchString = strlen(pszSource); + papszDstArray[i] = pszDstStr; + memcpy(pszDstStr, pszSource, cchString); + pszDstStr += cchString; + *pszDstStr++ = '\0'; + } + *pszDstStr = '\0'; + assert(&pszDstStr[1] - papszDstArray[0] == cbStrings); + papszDstArray[i] = NULL; + return papszDstArray; +} + +/** + * Allocate and init a WINCHILD. + * + * @returns The new windows child structure. + * @param enmType The child type. + */ +static PWINCHILD mkWinChildNew(WINCHILDTYPE enmType) +{ + PWINCHILD pChild = xcalloc(sizeof(*pChild)); + pChild->enmType = enmType; + pChild->fCoreDumped = 0; + pChild->iSignal = 0; + pChild->iExitCode = 222222; + pChild->uMagic = WINCHILD_MAGIC; + pChild->pid = (intptr_t)pChild; + return pChild; +} + +/** + * Destructor for WINCHILD. + * + * @param pChild The child structure to destroy. + */ +static void mkWinChildDelete(PWINCHILD pChild) +{ + assert(pChild->uMagic == WINCHILD_MAGIC); + pChild->uMagic = ~WINCHILD_MAGIC; + + switch (pChild->enmType) + { + case WINCHILDTYPE_PROCESS: + { + if (pChild->u.Process.papszArgs) + { + free(pChild->u.Process.papszArgs); + pChild->u.Process.papszArgs = NULL; + } + if (pChild->u.Process.cbEnvStrings && pChild->u.Process.papszEnv) + { + free(pChild->u.Process.papszEnv); + pChild->u.Process.papszEnv = NULL; + } + if (pChild->u.Process.pszShell) + { + free(pChild->u.Process.pszShell); + pChild->u.Process.pszShell = NULL; + } + if (pChild->u.Process.hProcess) + { + CloseHandle(pChild->u.Process.hProcess); + pChild->u.Process.hProcess = NULL; + } + mkWinChildcareWorkerCloseStandardHandles(pChild); + break; + } + +#ifdef KMK + case WINCHILDTYPE_BUILT_IN: + if (pChild->u.BuiltIn.papszArgs) + { + free(pChild->u.BuiltIn.papszArgs); + pChild->u.BuiltIn.papszArgs = NULL; + } + if (pChild->u.BuiltIn.papszEnv) + { + free(pChild->u.BuiltIn.papszEnv); + pChild->u.BuiltIn.papszEnv = NULL; + } + break; + + case WINCHILDTYPE_APPEND: + if (pChild->u.Append.pszFilename) + { + free(pChild->u.Append.pszFilename); + pChild->u.Append.pszFilename = NULL; + } + if (pChild->u.Append.pszAppend) + { + free(pChild->u.Append.pszAppend); + pChild->u.Append.pszAppend = NULL; + } + break; + + case WINCHILDTYPE_SUBMIT: + if (pChild->u.Submit.pvSubmitWorker) + { + kSubmitSubProcCleanup((intptr_t)pChild->u.Submit.pvSubmitWorker); + pChild->u.Submit.pvSubmitWorker = NULL; + } + break; + + case WINCHILDTYPE_REDIRECT: + if (pChild->u.Redirect.hProcess) + { + CloseHandle(pChild->u.Redirect.hProcess); + pChild->u.Redirect.hProcess = NULL; + } + break; +#endif /* KMK */ + + default: + assert(0); + } + + free(pChild); +} + +/** + * Queues the child with a worker, creating new workers if necessary. + * + * @returns 0 on success, windows error code on failure (child destroyed). + * @param pChild The child. + * @param pPid Where to return the PID (optional). + */ +static int mkWinChildPushToCareWorker(PWINCHILD pChild, pid_t *pPid) +{ + PWINCHILDCAREWORKER pWorker = NULL; + PWINCHILD pOldChild; + PWINCHILD pCurChild; + + /* + * There are usually idle workers around, except for at the start. + */ + if (g_cIdleChildcareWorkers > 0) + { + /* + * Try the idle hint first and move forward from it. + */ + unsigned int const cWorkers = g_cChildCareworkers; + unsigned int iHint = g_idxLastChildcareWorker; + unsigned int i; + for (i = iHint; i < cWorkers; i++) + { + PWINCHILDCAREWORKER pPossibleWorker = g_papChildCareworkers[i]; + if (pPossibleWorker->fIdle) + { + pWorker = pPossibleWorker; + break; + } + } + if (!pWorker) + { + /* Scan from the start. */ + if (iHint > cWorkers) + iHint = cWorkers; + for (i = 0; i < iHint; i++) + { + PWINCHILDCAREWORKER pPossibleWorker = g_papChildCareworkers[i]; + if (pPossibleWorker->fIdle) + { + pWorker = pPossibleWorker; + break; + } + } + } + } + if (!pWorker) + { + /* + * Try create more workers if we haven't reached the max yet. + */ + if (g_cChildCareworkers < g_cChildCareworkersMax) + pWorker = mkWinChildcareCreateWorker(); + + /* + * Queue it with an existing worker. Look for one without anthing extra scheduled. + */ + if (!pWorker) + { + unsigned int i = g_cChildCareworkers; + if (i == 0) + fatal(NILF, 0, _("Failed to create worker threads for managing child processes!\n")); + pWorker = g_papChildCareworkers[--i]; + if (pWorker->pTailTodoChildren) + while (i-- > 0) + { + PWINCHILDCAREWORKER pPossibleWorker = g_papChildCareworkers[i]; + if (!pPossibleWorker->pTailTodoChildren) + { + pWorker = pPossibleWorker; + break; + } + } + } + } + + /* + * Do the queueing. + */ + pOldChild = NULL; + for (;;) + { + pChild->pNext = pOldChild; + pCurChild = _InterlockedCompareExchangePointer((void **)&pWorker->pTailTodoChildren, pChild, pOldChild); + if (pCurChild == pOldChild) + { + DWORD volatile dwErr; + _InterlockedIncrement(&g_cPendingChildren); + if ( !pWorker->fIdle + || SetEvent(pWorker->hEvtIdle)) + { + *pPid = pChild->pid; + return 0; + } + + _InterlockedDecrement(&g_cPendingChildren); + dwErr = GetLastError(); + assert(0); + mkWinChildDelete(pChild); + return dwErr ? dwErr : -20; + } + pOldChild = pCurChild; + } +} + +/** + * Creates a regular child process (job.c). + * + * Will copy the information and push it to a childcare thread that does the + * actual process creation. + * + * @returns 0 on success, windows status code on failure. + * @param papszArgs The arguments. + * @param papszEnv The environment (optional). + * @param pszShell The SHELL variable value (optional). + * @param pMkChild The make child structure (optional). + * @param pPid Where to return the pid. + */ +int MkWinChildCreate(char **papszArgs, char **papszEnv, const char *pszShell, struct child *pMkChild, pid_t *pPid) +{ + PWINCHILD pChild = mkWinChildNew(WINCHILDTYPE_PROCESS); + pChild->pMkChild = pMkChild; + + pChild->u.Process.papszArgs = mkWinChildCopyStringArray(papszArgs, &pChild->u.Process.cbArgsStrings); + if ( !papszEnv + || !pMkChild + || pMkChild->environment == papszEnv) + { + pChild->u.Process.cbEnvStrings = 0; + pChild->u.Process.papszEnv = papszEnv; + } + else + pChild->u.Process.papszEnv = mkWinChildCopyStringArray(papszEnv, &pChild->u.Process.cbEnvStrings); + if (pszShell) + pChild->u.Process.pszShell = xstrdup(pszShell); + pChild->u.Process.hStdOut = INVALID_HANDLE_VALUE; + pChild->u.Process.hStdErr = INVALID_HANDLE_VALUE; + + /* We always catch the output in order to prevent character soups courtesy + of the microsoft CRT and/or linkers writing character by character to the + console. Always try write whole lines, even when --output-sync is none. */ + pChild->u.Process.fCatchOutput = TRUE; + + return mkWinChildPushToCareWorker(pChild, pPid); +} + +/** + * Creates a chile process with a pipe hooked up to stdout. + * + * @returns 0 on success, non-zero on failure. + * @param papszArgs The argument vector. + * @param papszEnv The environment vector (optional). + * @param fdErr File descriptor to hook up to stderr. + * @param pPid Where to return the pid. + * @param pfdReadPipe Where to return the read end of the pipe. + */ +int MkWinChildCreateWithStdOutPipe(char **papszArgs, char **papszEnv, int fdErr, pid_t *pPid, int *pfdReadPipe) +{ + /* + * Create the pipe. + */ + HANDLE hReadPipe; + HANDLE hWritePipe; + if (CreatePipe(&hReadPipe, &hWritePipe, NULL, 0 /* default size */)) + { + //if (SetHandleInformation(hWritePipe, HANDLE_FLAG_INHERIT /* clear */ , HANDLE_FLAG_INHERIT /*set*/)) + { + int fdReadPipe = _open_osfhandle((intptr_t)hReadPipe, O_RDONLY); + if (fdReadPipe >= 0) + { + PWINCHILD pChild; + int rc; + + /* + * Get a handle for fdErr. Ignore failure. + */ + HANDLE hStdErr = INVALID_HANDLE_VALUE; + if (fdErr >= 0) + { + HANDLE hNative = (HANDLE)_get_osfhandle(fdErr); + if (!DuplicateHandle(GetCurrentProcess(), hNative, GetCurrentProcess(), + &hStdErr, 0 /*DesiredAccess*/, TRUE /*fInherit*/, DUPLICATE_SAME_ACCESS)) + { + ONN(error, NILF, _("DuplicateHandle failed on stderr descriptor (%u): %u\n"), fdErr, GetLastError()); + hStdErr = INVALID_HANDLE_VALUE; + } + } + + /* + * Push it off to the worker thread. + */ + pChild = mkWinChildNew(WINCHILDTYPE_PROCESS); + pChild->u.Process.papszArgs = mkWinChildCopyStringArray(papszArgs, &pChild->u.Process.cbArgsStrings); + pChild->u.Process.papszEnv = mkWinChildCopyStringArray(papszEnv ? papszEnv : environ, + &pChild->u.Process.cbEnvStrings); + //if (pszShell) + // pChild->u.Process.pszShell = xstrdup(pszShell); + pChild->u.Process.hStdOut = hWritePipe; + pChild->u.Process.hStdErr = hStdErr; + pChild->u.Process.fCloseStdErr = TRUE; + pChild->u.Process.fCloseStdOut = TRUE; + + rc = mkWinChildPushToCareWorker(pChild, pPid); + if (rc == 0) + *pfdReadPipe = fdReadPipe; + else + { + ON(error, NILF, _("mkWinChildPushToCareWorker failed on pipe: %d\n"), rc); + close(fdReadPipe); + *pfdReadPipe = -1; + *pPid = -1; + } + return rc; + } + + ON(error, NILF, _("_open_osfhandle failed on pipe: %u\n"), errno); + } + //else + // ON(error, NILF, _("SetHandleInformation failed on pipe: %u\n"), GetLastError()); + if (hReadPipe != INVALID_HANDLE_VALUE) + CloseHandle(hReadPipe); + CloseHandle(hWritePipe); + } + else + ON(error, NILF, _("CreatePipe failed: %u\n"), GetLastError()); + *pfdReadPipe = -1; + *pPid = -1; + return -1; +} + +#ifdef KMK + +/** + * Interface used by kmkbuiltin.c for executing builtin commands on threads. + * + * @returns 0 on success, windows status code on failure. + * @param pBuiltIn The kmk built-in command entry. + * @param cArgs The number of arguments in papszArgs. + * @param papszArgs The argument vector. + * @param papszEnv The environment vector, optional. + * @param pMkChild The make child structure. + * @param pPid Where to return the pid. + */ +int MkWinChildCreateBuiltIn(PCKMKBUILTINENTRY pBuiltIn, int cArgs, char **papszArgs, char **papszEnv, + struct child *pMkChild, pid_t *pPid) +{ + size_t cbIgnored; + PWINCHILD pChild = mkWinChildNew(WINCHILDTYPE_BUILT_IN); + pChild->pMkChild = pMkChild; + pChild->u.BuiltIn.pBuiltIn = pBuiltIn; + pChild->u.BuiltIn.cArgs = cArgs; + pChild->u.BuiltIn.papszArgs = mkWinChildCopyStringArray(papszArgs, &cbIgnored); + pChild->u.BuiltIn.papszEnv = papszEnv ? mkWinChildCopyStringArray(papszEnv, &cbIgnored) : NULL; + return mkWinChildPushToCareWorker(pChild, pPid); +} + +/** + * Interface used by append.c for do the slow file system part. + * + * This will append the given buffer to the specified file and free the buffer. + * + * @returns 0 on success, windows status code on failure. + * + * @param pszFilename The name of the file to append to. + * @param ppszAppend What to append. The pointer pointed to is set to + * NULL once we've taken ownership of the buffer and + * promise to free it. + * @param cbAppend How much to append. + * @param fTruncate Whether to truncate the file before appending to it. + * @param pMkChild The make child structure. + * @param pPid Where to return the pid. + */ +int MkWinChildCreateAppend(const char *pszFilename, char **ppszAppend, size_t cbAppend, int fTruncate, + struct child *pMkChild, pid_t *pPid) +{ + size_t cbFilename = strlen(pszFilename) + 1; + PWINCHILD pChild = mkWinChildNew(WINCHILDTYPE_APPEND); + pChild->pMkChild = pMkChild; + pChild->u.Append.fTruncate = fTruncate; + pChild->u.Append.pszAppend = *ppszAppend; + pChild->u.Append.cbAppend = cbAppend; + pChild->u.Append.pszFilename = (char *)memcpy(xmalloc(cbFilename), pszFilename, cbFilename); + *ppszAppend = NULL; + return mkWinChildPushToCareWorker(pChild, pPid); +} + +/** + * Interface used by kSubmit.c for registering stuff to wait on. + * + * @returns 0 on success, windows status code on failure. + * @param hEvent The event object handle to wait on. + * @param pvSubmitWorker The argument to pass back to kSubmit to clean up. + * @param pStdOut Standard output pipe for the worker. Optional. + * @param pStdErr Standard error pipe for the worker. Optional. + * @param pMkChild The make child structure. + * @param pPid Where to return the pid. + */ +int MkWinChildCreateSubmit(intptr_t hEvent, void *pvSubmitWorker, PWINCCWPIPE pStdOut, PWINCCWPIPE pStdErr, + struct child *pMkChild, pid_t *pPid) +{ + PWINCHILD pChild = mkWinChildNew(WINCHILDTYPE_SUBMIT); + pChild->pMkChild = pMkChild; + pChild->u.Submit.hEvent = (HANDLE)hEvent; + pChild->u.Submit.pvSubmitWorker = pvSubmitWorker; + pChild->u.Submit.pStdOut = pStdOut; + pChild->u.Submit.pStdErr = pStdErr; + return mkWinChildPushToCareWorker(pChild, pPid); +} + +/** + * Interface used by redirect.c for registering stuff to wait on. + * + * @returns 0 on success, windows status code on failure. + * @param hProcess The process object to wait on. + * @param pPid Where to return the pid. + */ +int MkWinChildCreateRedirect(intptr_t hProcess, pid_t *pPid) +{ + PWINCHILD pChild = mkWinChildNew(WINCHILDTYPE_REDIRECT); + pChild->u.Redirect.hProcess = (HANDLE)hProcess; + return mkWinChildPushToCareWorker(pChild, pPid); +} + + +/** + * New interface used by redirect.c for spawning and waitin on a child. + * + * This interface is only used when kmk_builtin_redirect is already running on + * a worker thread. + * + * @returns exit status. + * @param pvWorker The worker instance. + * @param pszExecutable The executable image to run. + * @param papszArgs Argument vector. + * @param fQuotedArgv Whether the argument vector is already quoted and + * just need some space to be turned into a command + * line. + * @param papszEnvVars Environment vector. + * @param pszCwd The working directory of the child. Optional. + * @param pafReplace Which standard handles to replace. Maybe modified! + * @param pahReplace The replacement handles. Maybe modified! + * + */ +int MkWinChildBuiltInExecChild(void *pvWorker, const char *pszExecutable, char **papszArgs, BOOL fQuotedArgv, + char **papszEnvVars, const char *pszCwd, BOOL pafReplace[3], HANDLE pahReplace[3]) +{ + PWINCHILDCAREWORKER pWorker = (PWINCHILDCAREWORKER)pvWorker; + WCHAR *pwszSearchPath = NULL; + WCHAR *pwszzEnvironment = NULL; + WCHAR *pwszCommandLine = NULL; + WCHAR *pwszImageName = NULL; + WCHAR *pwszCwd = NULL; + BOOL fNeedShell = FALSE; + PWINCHILD pChild; + int rc; + assert(pWorker->uMagic == WINCHILDCAREWORKER_MAGIC); + pChild = pWorker->pCurChild; + assert(pChild != NULL && pChild->uMagic == WINCHILD_MAGIC); + + /* + * Convert the CWD first since it's optional and we don't need to clean + * up anything here if it fails. + */ + if (pszCwd) + { + size_t cchCwd = strlen(pszCwd); + int cwcCwd = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszCwd, cchCwd + 1, NULL, 0); + pwszCwd = xmalloc((cwcCwd + 1) * sizeof(WCHAR)); /* (+1 in case cwcCwd is 0) */ + cwcCwd = MultiByteToWideChar(CP_ACP, 0 /*fFlags*/, pszCwd, cchCwd + 1, pwszCwd, cwcCwd + 1); + if (!cwcCwd) + { + rc = GetLastError(); + MkWinChildError(pWorker, 1, _("MultiByteToWideChar failed to convert CWD (%s): %u\n"), pszCwd, (unsigned)rc); + return rc; + } + } + + /* + * Before we search for the image, we convert the environment so we don't + * have to traverse it twice to find the PATH. + */ + rc = mkWinChildcareWorkerConvertEnvironment(pWorker, papszEnvVars ? papszEnvVars : environ, 0/*cbEnvStrings*/, + &pwszzEnvironment, &pwszSearchPath); + /* + * Find the executable and maybe checking if it's a shell script, then + * convert it to a command line. + */ + if (rc == 0) + rc = mkWinChildcareWorkerFindImage(pWorker, pszExecutable, pwszSearchPath, pwszzEnvironment, NULL /*pszShell*/, + &pwszImageName, &fNeedShell, &pChild->fProbableClExe); + if (rc == 0) + { + assert(!fNeedShell); + if (!fQuotedArgv) + rc = mkWinChildcareWorkerConvertCommandline(pWorker, papszArgs, 0 /*fFlags*/, &pwszCommandLine); + else + rc = mkWinChildcareWorkerConvertQuotedArgvToCommandline(pWorker, papszArgs, &pwszCommandLine); + + /* + * Create the child process. + */ + if (rc == 0) + { + HANDLE hProcess; + rc = mkWinChildcareWorkerCreateProcess(pWorker, pwszImageName, pwszCommandLine, pwszzEnvironment, + pwszCwd, pafReplace, pahReplace, TRUE /*fCatchOutput*/, &hProcess); + if (rc == 0) + { + /* + * Wait for the child to complete. + */ + rc = mkWinChildcareWorkerWaitForProcess(pWorker, pChild, hProcess, pwszImageName, TRUE /*fCatchOutput*/); + CloseHandle(hProcess); + } + } + } + + free(pwszCwd); + free(pwszCommandLine); + free(pwszImageName); + free(pwszzEnvironment); + + return rc; +} + +#endif /* CONFIG_NEW_WIN_CHILDREN */ + +/** + * Interface used to kill process when processing Ctrl-C and fatal errors. + * + * @returns 0 on success, -1 & errno on error. + * @param pid The process to kill (PWINCHILD). + * @param iSignal What to kill it with. + * @param pMkChild The make child structure for validation. + */ +int MkWinChildKill(pid_t pid, int iSignal, struct child *pMkChild) +{ + PWINCHILD pChild = (PWINCHILD)pid; + if (pChild) + { + assert(pChild->uMagic == WINCHILD_MAGIC); + if (pChild->uMagic == WINCHILD_MAGIC) + { + switch (pChild->enmType) + { + case WINCHILDTYPE_PROCESS: + assert(pChild->pMkChild == pMkChild); + TerminateProcess(pChild->u.Process.hProcess, DBG_TERMINATE_PROCESS); + pChild->iSignal = iSignal; + break; +#ifdef KMK + case WINCHILDTYPE_SUBMIT: + { + pChild->iSignal = iSignal; + SetEvent(pChild->u.Submit.hEvent); + break; + } + + case WINCHILDTYPE_REDIRECT: + TerminateProcess(pChild->u.Redirect.hProcess, DBG_TERMINATE_PROCESS); + pChild->iSignal = iSignal; + break; + + case WINCHILDTYPE_BUILT_IN: + break; + +#endif /* KMK */ + default: + assert(0); + } + } + } + return -1; +} + +/** + * Wait for a child process to complete + * + * @returns 0 on success, windows error code on failure. + * @param fBlock Whether to block. + * @param pPid Where to return the pid if a child process + * completed. This is set to zero if none. + * @param piExitCode Where to return the exit code. + * @param piSignal Where to return the exit signal number. + * @param pfCoreDumped Where to return the core dumped indicator. + * @param ppMkChild Where to return the associated struct child pointer. + */ +int MkWinChildWait(int fBlock, pid_t *pPid, int *piExitCode, int *piSignal, int *pfCoreDumped, struct child **ppMkChild) +{ + PWINCHILD pChild; + + *pPid = 0; + *piExitCode = -222222; + *pfCoreDumped = 0; + *ppMkChild = NULL; + + /* + * Wait if necessary. + */ + if (fBlock && !g_pTailCompletedChildren && g_cPendingChildren > 0) + { + DWORD dwStatus = WaitForSingleObject(g_hEvtWaitChildren, INFINITE); + if (dwStatus == WAIT_FAILED) + return (int)GetLastError(); + } + + /* + * Try unlink the last child in the LIFO. + */ + pChild = g_pTailCompletedChildren; + if (!pChild) + return 0; + pChild = mkWinChildDequeFromLifo(&g_pTailCompletedChildren, pChild); + assert(pChild); + + /* + * Set return values and ditch the child structure. + */ + *pPid = pChild->pid; + *piExitCode = pChild->iExitCode; + *pfCoreDumped = pChild->fCoreDumped; + *ppMkChild = pChild->pMkChild; + switch (pChild->enmType) + { + case WINCHILDTYPE_PROCESS: + break; +#ifdef KMK + case WINCHILDTYPE_BUILT_IN: + case WINCHILDTYPE_APPEND: + case WINCHILDTYPE_SUBMIT: + case WINCHILDTYPE_REDIRECT: + break; +#endif /* KMK */ + default: + assert(0); + } + mkWinChildDelete(pChild); + +#ifdef KMK + /* Flush the volatile directory cache. */ + dir_cache_invalid_after_job(); +#endif + return 0; +} + +/** + * Get the child completed event handle. + * + * Needed when w32os.c is waiting for a job token to become available, given + * that completed children is the typical source of these tokens (esp. for kmk). + * + * @returns Zero if no active children, event handle if waiting is required. + */ +intptr_t MkWinChildGetCompleteEventHandle(void) +{ + /* We don't return the handle if we've got completed children. This + is a safe guard against being called twice in a row without any + MkWinChildWait call inbetween. */ + if (!g_pTailCompletedChildren) + return (intptr_t)g_hEvtWaitChildren; + return 0; +} + +/** + * Emulate execv() for restarting kmk after one or more makefiles has been made. + * + * Does not return. + * + * @param papszArgs The arguments. + * @param papszEnv The environment. + */ +void MkWinChildReExecMake(char **papszArgs, char **papszEnv) +{ + PROCESS_INFORMATION ProcInfo; + STARTUPINFOW StartupInfo; + WCHAR *pwszCommandLine; + WCHAR *pwszzEnvironment; + WCHAR *pwszPathIgnored; + int rc; + + /* + * Get the executable name. + */ + WCHAR wszImageName[MKWINCHILD_MAX_PATH]; + DWORD cwcImageName = GetModuleFileNameW(GetModuleHandle(NULL), wszImageName, MKWINCHILD_MAX_PATH); + if (cwcImageName == 0) + ON(fatal, NILF, _("MkWinChildReExecMake: GetModuleFileName failed: %u\n"), GetLastError()); + + /* + * Create the command line and environment. + */ + rc = mkWinChildcareWorkerConvertCommandline(NULL, papszArgs, 0 /*fFlags*/, &pwszCommandLine); + if (rc != 0) + ON(fatal, NILF, _("MkWinChildReExecMake: mkWinChildcareWorkerConvertCommandline failed: %u\n"), rc); + + rc = mkWinChildcareWorkerConvertEnvironment(NULL, papszEnv ? papszEnv : environ, 0 /*cbEnvStrings*/, + &pwszzEnvironment, &pwszPathIgnored); + if (rc != 0) + ON(fatal, NILF, _("MkWinChildReExecMake: mkWinChildcareWorkerConvertEnvironment failed: %u\n"), rc); + +#ifdef KMK + /* + * Flush the file system cache to avoid messing up tools fetching + * going on in the "exec'ed" make by keeping directories open. + */ + dir_cache_invalid_all_and_close_dirs(1); +#endif + + /* + * Fill out the startup info and try create the process. + */ + memset(&ProcInfo, 0, sizeof(ProcInfo)); + memset(&StartupInfo, 0, sizeof(StartupInfo)); + StartupInfo.cb = sizeof(StartupInfo); + GetStartupInfoW(&StartupInfo); + if (!CreateProcessW(wszImageName, pwszCommandLine, NULL /*pProcSecAttr*/, NULL /*pThreadSecAttr*/, + TRUE /*fInheritHandles*/, CREATE_UNICODE_ENVIRONMENT, pwszzEnvironment, NULL /*pwsz*/, + &StartupInfo, &ProcInfo)) + ON(fatal, NILF, _("MkWinChildReExecMake: CreateProcessW failed: %u\n"), GetLastError()); + CloseHandle(ProcInfo.hThread); + + /* + * Wait for it to complete and forward the status code to our parent. + */ + for (;;) + { + DWORD dwExitCode = -2222; + DWORD dwStatus = WaitForSingleObject(ProcInfo.hProcess, INFINITE); + if ( dwStatus == WAIT_IO_COMPLETION + || dwStatus == WAIT_TIMEOUT /* whatever */) + continue; /* however unlikely, these aren't fatal. */ + + /* Get the status code and terminate. */ + if (dwStatus == WAIT_OBJECT_0) + { + if (!GetExitCodeProcess(ProcInfo.hProcess, &dwExitCode)) + { + ON(fatal, NILF, _("MkWinChildReExecMake: GetExitCodeProcess failed: %u\n"), GetLastError()); + dwExitCode = -2222; + } + } + else if (dwStatus) + dwExitCode = dwStatus; + + CloseHandle(ProcInfo.hProcess); + for (;;) + exit(dwExitCode); + } +} + +#ifdef WITH_RW_LOCK +/** Serialization with kmkbuiltin_redirect. */ +void MkWinChildExclusiveAcquire(void) +{ + AcquireSRWLockExclusive(&g_RWLock); +} + +/** Serialization with kmkbuiltin_redirect. */ +void MkWinChildExclusiveRelease(void) +{ + ReleaseSRWLockExclusive(&g_RWLock); +} +#endif /* WITH_RW_LOCK */ + +/** + * Implementation of the CLOSE_ON_EXEC macro. + * + * @returns errno value. + * @param fd The file descriptor to hide from children. + */ +int MkWinChildUnrelatedCloseOnExec(int fd) +{ + if (fd >= 0) + { + HANDLE hNative = (HANDLE)_get_osfhandle(fd); + if (hNative != INVALID_HANDLE_VALUE && hNative != NULL) + { + if (SetHandleInformation(hNative, HANDLE_FLAG_INHERIT /*clear*/ , 0 /*set*/)) + return 0; + } + return errno; + } + return EINVAL; +} + |