commit dbbf0b2f8453b92b54fb5cd0e4f68fc89ba25afa Author: Anton Kovalenko Date: Thu Apr 23 21:07:28 2020 +0300 Initial release diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cab1a8 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +## PIJ - Run process tree in a job and wait for completion. + +## Usage +`pij my-program.exe arguments...` + +## Purpose +Use it when you want to run `my-program.exe` in a batch file, but +my-program.exe starts another process(es) and returns before they +complete, which is not what you want: batch file should continue only +when all additional processes also exit. PIJ runs initial process in a +job and waits for this job to become empty, before exiting. + +## Result code (ERRORLEVEL) +PIJ terminates with non-zero exit code if there's some problem during +initial process creation or waiting phase. Zero exit code is returned +when the last process in the job exits. + +## Restrictions +All arguments are passed to CreateProcess'es lpCommandLine, hence some +restrictions on what you can start: it should be a real EXE file +(please start cmd.exe if you want to run another batch file), standard +directories and PATH are used, registry AppPaths aren't. + +## Author +Written by Anton Kovalenko . My code is public domain. +Uses some code by Raymond Chen. diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..81527fe --- /dev/null +++ b/build.cmd @@ -0,0 +1 @@ +x86_64-w64-mingw32-gcc -O2 -s pij.c pijhelp.cc -o pij.exe -DUNICODE diff --git a/pij.c b/pij.c new file mode 100644 index 0000000..3037b17 --- /dev/null +++ b/pij.c @@ -0,0 +1,161 @@ +#ifndef UNICODE +#define UNICODE +#endif + +#include +#include +#include + +// Skip initial argument in a command line. Adapted from my winserv +// http://www.sw4me.com/wiki/Winserv +static LPTSTR skip_arg(LPTSTR p) +{ + int inquote, slashes; + + // skip spaces + while (isspace(*p)) { p++; } + if (*p == TEXT('\0')) { return p; } + inquote = 0; slashes = 0; + // we found where the argument begins + for (;;) { + while (*p == TEXT('\\')) { + slashes++; + p++; + } + if (*p == TEXT('"')) { + if ((slashes & 1) == 0) { + if ((inquote) && (p[1] == TEXT('"'))) { + p++; + } else { + inquote = !inquote; + } + } + } + slashes=0; + if ((*p == TEXT('\0')) || (!inquote && isspace(*p))) { + while (isspace(*p)) + p++; + return p; + } + p++; + } +} + +// Terminate unsuccessfully with an error message +static void Fail(const char *desc) { + fprintf(stderr, "Error %u in %s\n", GetLastError(), desc); + exit(1); +} + +// Raymond Chen's CreateProcessInJob +// see https://devblogs.microsoft.com/oldnewthing/20131209-00/?p=2433 +BOOL CreateProcessInJob( + HANDLE hJob, + LPCTSTR lpApplicationName, + LPTSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + LPVOID lpEnvironment, + LPCTSTR lpCurrentDirectory, + LPSTARTUPINFO lpStartupInfo, + LPPROCESS_INFORMATION ppi) +{ + BOOL fRc = CreateProcess( + lpApplicationName, + lpCommandLine, + lpProcessAttributes, + lpThreadAttributes, + bInheritHandles, + dwCreationFlags | CREATE_SUSPENDED, + lpEnvironment, + lpCurrentDirectory, + lpStartupInfo, + ppi); + if (fRc) { + fRc = AssignProcessToJobObject(hJob, ppi->hProcess); + if (fRc && !(dwCreationFlags & CREATE_SUSPENDED)) { + fRc = ResumeThread(ppi->hThread) != (DWORD)-1; + } + if (!fRc) { + TerminateProcess(ppi->hProcess, 0); + CloseHandle(ppi->hProcess); + CloseHandle(ppi->hThread); + ppi->hProcess = ppi->hThread = NULL; + } + } + return fRc; +} + +// defined in pijhelp.cc, so I can use C++ raw literals +extern const char *Help; + + +int main(int argc, char*argv[]) +{ + if (argc <= 1) { + printf("%s",Help); + exit(0); + } + LPTSTR cmdline = GetCommandLineW(); + LPTSTR restCmdLine = skip_arg(cmdline); + HANDLE hJob = CreateJobObject(NULL,NULL); + if (hJob == NULL) { + Fail("CreateJobObject"); + } + HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1); + if (hPort == NULL) { + Fail("CreateIoCompletionPort"); + } + + // see https://devblogs.microsoft.com/oldnewthing/20130405-00/?p=4743 + JOBOBJECT_ASSOCIATE_COMPLETION_PORT jacp; + jacp.CompletionKey = hJob; + jacp.CompletionPort = hPort; + if (!SetInformationJobObject(hJob, + JobObjectAssociateCompletionPortInformation, + &jacp, sizeof(jacp))) { + Fail("SetInformationJobObject(JobObjectAssociateCompletionPortInformation)"); + } + + STARTUPINFO si = { sizeof si }; + PROCESS_INFORMATION pi; + if (!CreateProcessInJob(hJob, + NULL, + restCmdLine, + NULL, + NULL, + FALSE, + 0, + NULL, + NULL, + &si, + &pi)) { + Fail("CreateProcess"); + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + + DWORD completionCode; + ULONG_PTR completionKey; + LPOVERLAPPED overlapped; + + for (;;) { + BOOL rc = GetQueuedCompletionStatus(hPort, + &completionCode, + &completionKey, + &overlapped, + INFINITE); + if (!rc) { + Fail("GetQueuedCompletionStatus"); + } + if ((HANDLE)completionKey == hJob && + completionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) { + break; + } + } + CloseHandle(hJob); + return 0; +} diff --git a/pijhelp.cc b/pijhelp.cc new file mode 100644 index 0000000..785657f --- /dev/null +++ b/pijhelp.cc @@ -0,0 +1,24 @@ +const char *Help = R"HELP( +PIJ - Run process tree in a job and wait for completion. + +Usage: + pij my-program.exe arguments... + +Use it when you want to run my-program.exe in a batch file, but +my-program.exe starts another process(es) and returns before they +complete, which is not what you want: batch file should continue only +when all additional processes also exit. PIJ runs initial process in a +job and waits for this job to become empty, before exiting. + +PIJ terminates with non-zero exit code if there's some problem during +initial process creation or waiting phase. Zero exit code is returned +when the last process in the job exits. + +All arguments are passed to CreateProcess'es lpCommandLine, hence some +restrictions on what you can start: it should be a real EXE file +(please start cmd.exe if you want to run another batch file), standard +directories and PATH are used, registry AppPaths aren't. + +Written by Anton Kovalenko . Public domain. +)HELP"; +