// ----------------------------------------------------------------------------
//
// Program: RunAll
//
// Purpose: Run all processes simultaneously with optional fatal error
//          on any process failure or timeout.  This is intended to be
//          used by the automated build process to wrapper running
//          bootd and the kernel debugger side-by-side.
//
// Copyright (c) Microsoft Corporation. All Rights Reserved.
//
// ----------------------------------------------------------------------------

#define _WIN32_WINNT 0x0500

#include <winlean.h>

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

static BOOL g_bVerbose = FALSE;

#define tprintf if (g_bVerbose) printf

static BOOL WINAPI CtrlHandler(DWORD dwCtrlType)
{
    printf("Caught control event %I32u", dwCtrlType);
    switch (dwCtrlType) {
      case CTRL_C_EVENT:
      case CTRL_BREAK_EVENT:
        GenerateConsoleCtrlEvent(dwCtrlType, 0);
      default:
        break;
    }
    ExitProcess((UINT)-2);
}

static void
ErrorReport(char *function)
{
    VOID *message;
    DWORD error = GetLastError();

    FormatMessage(
                  FORMAT_MESSAGE_ALLOCATE_BUFFER |
                  FORMAT_MESSAGE_FROM_SYSTEM,
                  NULL,
                  error,
                  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                  (LPTSTR) &message,
                  0, NULL );

    printf("runall: %s failed with error %d: %s",
           function, error, message);

    LocalFree(message);
}

void usage()
{
    printf("Usage: runall [options] cmd [cmd1] ...\n\n");
    printf("Options:\n");
    printf("  /L:logdir     - Write output from processes to logdir (must exist).\n");
    printf("  /T:seconds    - Set timeout in seconds.\n");
    printf("\nRuns specified commands in parallel and wait for all to terminate successfully.\n\nThe program returns -1 if any process fails to launch, or exits with a non zero error-code, or the option timeout expires.");
}

static BOOL
ParseInt64(LPCSTR lpszValue, PINT64 pValue)
{
    PCHAR pszStop;

    errno = 0;
    *pValue = _strtoi64(lpszValue, &pszStop, 10);

    // Return no overflow and all characters parsed.
    return errno == 0 && *pszStop == '\0';
}

static HANDLE
OpenLogFile(LPCSTR lpszLogDir, int fileNumber)
{
    HANDLE hLogFile;
    CHAR lpszLogPath[_MAX_PATH];

    _snprintf_s(lpszLogPath, sizeof(lpszLogPath), _TRUNCATE,
                "%s\\%d.log", lpszLogDir, fileNumber);
    lpszLogPath[_MAX_PATH - 1] = '0';

    hLogFile = CreateFile(lpszLogPath, GENERIC_WRITE | GENERIC_READ,
                          FILE_SHARE_READ, NULL,
                          CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    if (INVALID_HANDLE_VALUE == hLogFile) {
        ErrorReport("CreateFile (for log)");
    }

    return hLogFile;
}

static int
RunAndWait(int                   nProcesses,
           LPPROCESS_INFORMATION lpProcessInfo,
           LPCSTR                lpszCommandLine[],
           time_t                endTime)
{
    HANDLE hProcesses[MAXIMUM_WAIT_OBJECTS];
    int    nProcessOrdinal[MAXIMUM_WAIT_OBJECTS];
    int    i;

    for (i = 0; i < nProcesses; i++) {
        if (ResumeThread(lpProcessInfo[i].hThread) == -1) {
            ErrorReport("ResumeThread");
            return -1;
        }
        hProcesses[i]      = lpProcessInfo[i].hProcess;
        nProcessOrdinal[i] = i;
    }

    while (nProcesses > 0) {
        DWORD  dwMilliseconds;
        DWORD  dwResult;
        time_t t;

        time(&t);
        if (t >= endTime) {
            printf("runall: timeout\n");
            return -1;
        }

        t = (endTime - t) * 1000;

        // Note INFINITE - 1 so we don't sleep forever.
        dwMilliseconds = (DWORD)((t < INFINITE) ? t : INFINITE - 1);

        dwResult = WaitForMultipleObjects(nProcesses,
                                          hProcesses,
                                          FALSE,
                                          dwMilliseconds);
        tprintf("Next timeout = %d ms\n", dwMilliseconds);
        if (dwResult == WAIT_TIMEOUT) {
            tprintf("runall: WAIT_TIMEOUT\n");
        }
        else if (dwResult >= WAIT_ABANDONED_0 &&
                 dwResult <= WAIT_ABANDONED_0 + nProcesses) {
            printf("runall: wait abandoned (process %d)\n",
                   dwResult - WAIT_ABANDONED_0);
            return -1;
        }
        else {
            DWORD dwProcess = dwResult - WAIT_OBJECT_0;
            DWORD dwExitCode;

            hProcesses[dwProcess] = hProcesses[--nProcesses];
            nProcessOrdinal[dwProcess] = nProcessOrdinal[nProcesses];

            if (GetExitCodeProcess(hProcesses[dwProcess], &dwExitCode) == 0) {
                ErrorReport("GetExitCodeProcess");
                return -1;
            }

            printf("runall: Command \"%s\" => exited %d\n",
                   lpszCommandLine[nProcessOrdinal[dwProcess]],
                   dwExitCode);

            if (dwExitCode != 0) {
                return -1;
            }
        }
    }
    return 0;
}

static int
RunAll(int nProcesses,
       LPCSTR lpszCommandLine[],
       LPCSTR logDir,
       time_t endTime)
{
    PROCESS_INFORMATION pi[MAXIMUM_WAIT_OBJECTS];
    STARTUPINFO         si;

    int exitCode = -1;
    int i;

    for (i = 0; i < nProcesses; i++) {
        DWORD dwCreationFlags = CREATE_SUSPENDED | CREATE_NO_WINDOW;
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);

        if (NULL != logDir) {
            si.dwFlags = STARTF_USESTDHANDLES;
            si.hStdOutput = OpenLogFile(logDir, i);
            if (INVALID_HANDLE_VALUE == si.hStdOutput) {
                break;
            }
            SetHandleInformation(si.hStdOutput,
                                 HANDLE_FLAG_INHERIT,
                                 HANDLE_FLAG_INHERIT);
            si.hStdError  = si.hStdOutput;
        }

        if (!CreateProcess(NULL, (LPSTR)lpszCommandLine[i], NULL, NULL, TRUE,
                           dwCreationFlags, NULL, NULL, &si, &pi[i])) {
            ErrorReport("CreateProcess");
            break;
        }

        tprintf("runall: Created PID %u: %s\n",
                pi[i].dwProcessId, lpszCommandLine[i]);
    }

    if (i == nProcesses) {
        exitCode = RunAndWait(nProcesses, pi, lpszCommandLine, endTime);
    }

    while (--i >= 0) {
        if (TerminateProcess(pi[i].hProcess, -1)) {
            tprintf("runall: Terminated PID %u: %s\n",
                    pi[i].dwProcessId, lpszCommandLine[i]);
        }
    }
    return exitCode;
}

static double
FileTimeToSeconds(FILETIME ftValue)
{
    PINT64 pV = (PINT64)&ftValue;
    double d = (*pV) * 1e-7;
    return d;
}

static void
DisplayProcessTimes()
{
    FILETIME ftCreation;
    FILETIME ftExitTime;
    FILETIME ftKernelTime;
    FILETIME ftUserTime;

    GetProcessTimes((HANDLE)-1,
                    &ftCreation,
                    &ftExitTime,
                    &ftKernelTime,
                    &ftUserTime);
    printf("runall: Process Time Information\n");
    printf("runall:   Kernel time: %.2f\n", FileTimeToSeconds(ftKernelTime));
    printf("runall:   User time:   %.2f\n", FileTimeToSeconds(ftUserTime));
}

int
main(int argc, LPCSTR argv[])
{
    BOOL   bAccounting    = FALSE;
    BOOL   bLaunchFailure = FALSE;
    time_t endTime        = _I64_MAX;
    INT64  timeout        = 0;
    LPCSTR lpszLogDir     = NULL;
    INT32  exitCode;

    argc--;
    argv++;

    while (argc != 0 && (argv[0][0] == '/' || argv[0][0] == '-')) {
        switch (tolower(argv[0][1])) {
            case 'a':
                bAccounting = TRUE;
                break;
            case 't':
                if (argv[0][2] == ':' && ParseInt64(&argv[0][3], &timeout)) {
                    time(&endTime);
                    endTime += (time_t)timeout;
                }
                else {
                    printf("Bad timeout argument.");
                    return -1;
                }
                break;
            case 'l':
                if (argv[0][2] == ':') {
                    lpszLogDir = argv[0] + 3;
                }
                else {
                    printf("Bad log dir argument.");
                    return -1;
                }
                break;
            case 'v':
                g_bVerbose = TRUE;
                break;
            default:
                usage();
                return -1;
        }
        argc --;
        argv ++;
    }

    if (argc == 0) {
        usage();
        return -1;
    } else if (argc > MAXIMUM_WAIT_OBJECTS) {
        printf("Supported process limit exceeded.");
        return -1;
    }

    SetConsoleCtrlHandler(CtrlHandler, TRUE);

    exitCode = RunAll(argc, argv, lpszLogDir, endTime);

    if (bAccounting) {
        DisplayProcessTimes();
    }

    return exitCode;
}
