Initial release

This commit is contained in:
Anton Kovalenko 2020-04-23 21:07:28 +03:00
commit dbbf0b2f84
4 changed files with 212 additions and 0 deletions

26
README.md Normal file

@ -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 <anton@sw4me.com>. My code is public domain.
Uses some code by Raymond Chen.

1
build.cmd Normal file

@ -0,0 +1 @@
x86_64-w64-mingw32-gcc -O2 -s pij.c pijhelp.cc -o pij.exe -DUNICODE

161
pij.c Normal file

@ -0,0 +1,161 @@
#ifndef UNICODE
#define UNICODE
#endif
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
// 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;
}

24
pijhelp.cc Normal file

@ -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 <anton@sw4me.com>. Public domain.
)HELP";