Hart.gif (5380 bytes)Win32 System Programming

Return to Addison-Wesley Home Page  


Return to Top Page

ERRATA that apply to Printing 1
(most are corrected in Printing 2)

    See copyright page in front of book for printing number information.

Last modified: July 16, 1999

Please send comments and suggestions directly to me: jmhart@world.std.com


Change severity ratings

    A: Important factual error or significant program defect.
    B: Clarification, better explanation, program improvement or minor defect.
    C: Minor change.

SEVERITY A CHANGES

p. 82

The last 4 lines of the sample code should be modified to read as follows, so that the __except section is outside of the __try section:

        }
        __except (filter-expression) {
                /* Exception handler. */
        }

p. 87-89

Revise the two paragraphs between the list of 1 to 5 on p. 87 and the "Example" section on p. 88, where the TRUE and FALSE values were inverted:

The signal handler can perform cleanup operations just as an exception or completion handler would. The signal handler should return TRUE to indicate that the function handled the signal. If the signal handler returns FALSE, then the next handler function in the list is executed. The signal handlers are executed in the reverse order from the way they were set so that the most recently set handler is executed first and the system handler will be executed last.

The final three "returns" in Program 5-4 should be return TRUE

p. 129-130

There have been changes to Program 7-1. This is the new version, which reflects the changes to the InitUnixSA function. It now requires the address of a heap handle. (This is discussed below.) This revision accounts for several changes.

NEW PROGRAM LISTING AT THE END.

p. 131-133

There have been changes to Program 7-3. This is the new version, which creates a heap and allocates all the security structures in the heap. The calling program should eventually destroy this heap (see the changes to chmod). The original (Printing 1) version of this program allocated the SIDs on the function's heap, even though they were implicitly required by the calling program. There is an associated change in Support.h, noted below on p. 310. There is also a change on p. 192, listed under the C severity changes.

This defect never showed up in my testing, but a reader detected and diagnosed the problem. My fix illustrates heap usage; there are other techniques to this problem that you might prefer.

NEW PROGRAM LISTING AT THE END.

p. 133

Add a new first bulleted item:

  • Several memory allocations are required to hold information such as the SIDs. They are created in a dedicated heap, which is eventually destroyed by the calling program.

p. 137

There is a revised Program 7-5.

NEW PROGRAM LISTING AT THE END.

p. 164

In Table 8-2, the second item down the "Read" column should be:

Succeeds. It is not necessary for the calling process to own a lock on the file region.

p. 165

The second bullet down should say:

  • Assume that process A has a shared lock on the file and that process B attempts to read the file without obtaining a shared lock first. The read will still succeed, even though the reading process does not own any lock on the file, because the read operation does not conflict with the existing shared lock.

p. 233

The statement regarding critical section performance is not entirely correct; CSs can be slower than mutexes in some circumstances. See details.

p. 279

In Figure 13-2, ExitProcess and SleepEx are each one word, and SleepEx should have the value INFINITE, not 0 (there are two calls to SleepEx):

SleepEx (INFINITE)

p. 310

In the 13th line down, add LPHANDLE as the last item in LPSECURITY_ATTRIBUTES

This is necessary for the changes in Programs 7-1 and 7-3.

SEVERITY B CHANGES

p. 37

13th line of Program 3-1 (first line inside while loop) should be:

MsgLen = _tcslen (pMsg);

p. 40

10th line down, move the } down another 3 lines so that it comes after HeapFree

p. 42

Change the 8th line of Program 3-4 from HANDLE hOutFile; to

TCHAR YNResp [3] = _T ("y");

See atou for additional atou performance comparisons that modify and extend some of the conclusions in the text.

p. 45

Change the second sentence at the top of the page:

Also, Windows 95/98 does not usefully implement MoveFileEx; it will just return a FALSE, indicating an error.

p. 61

Add an open bracket at the end of the else for statement at the very end of the page.

p. 62

Then put

ok = ... SetCurrentDirectory ... }

for the first three lines on Page 62 (adding the close bracket required by the open bracket on the preceding page).

p. 62

In Program 4-2, replace lines 16 to 20 (if SearchHandle ... FALSE; }) with:

       if (SearchHandle == INVALID_HANDLE_VALUE)
                 /* A deleted file will not be found on 2nd pass. */
            return iPass == 2;

p. 88

The declaration of the variable Exit should be:

volatile static BOOL Exit = FALSE;

This is required as the console control handler runs in a separate thread; see Chapter 11 for a more complete explanation. The code on the disc has this fix.

p. 154

In the second bullet at the bottom of the page, 4th line from the bottom, it should say:

... for any of a group of processes to terminate.

(not all)

p. 160

Revise the last sentence before Program 8-2 to:

Program 8-2 will run under either Windows NT or Windows 95/98, and it uses the GetVersionEx function to determine the OS version. With Windows 95/98, only the elapsed time is available.

p. 161

There is a new Program 8-2. The preprocessor variable has been replaced by a run-time determination of the operating system type.

NEW PROGRAM LISTING AT THE END.

p. 228

See the last paragraph before the Example. You can test a critical section with the TryCriticalSection function (this function is available starting with Microsoft Visual C++ Version 5.0), which will take ownership of the critical section if possible, but will return a FALSE if a different thread owns the critical section. Thus, you can poll a critical section, but you cannot time out.

p. 233

Here is an additional comparison of mutexes with CRITICAL_SECTION objects. Mutex handles can participate in a WaitForMultipleObjects call (the handles in the array can be a mix of any synchronization object handles). This is desirable at times, and careful use of WaitForMultipleObjects can help to avoid the deadlocks that might occur if you waited for the individual objects sequentially. You can only wait for one single CRITICAL_SECTION object at a time, however, and you must use EnterCriticalSection.

p. 234

The remarks on p. 232 regarding names apply to semaphores as well. Creating a semaphore with the name of an existing semaphore means that the initial and maximum counts are ignored.

This applies to CreateMutex, etc.

p. 237

The remarks on p. 232 regarding names applies to events (and semaphores) as well, but, of course, the initial state and the auto/manual reset properties cannot be changed.

p. 246

case CS_RQCOMPLETE is missing a closing bracket } for the if statement. It should go just before the closing bracket for the _try statement and have its own line.

p. 248

The utilization computation on the seventh line is inaccurate because of the way in which it is parenthesized. As it is, the utilization will stay at zero. The fix will allow the multiplication by 100 to precede the division. Alternatively, you could cast the operands to floating point. Here is an improved expression for the utilization.

    Utilization = (LONG)(100 *
        (TotKerTim.li - OldKerTim +
        TotUsrTim.li - OldUsrTim) / (10000 * CS_TIMEOUT));

p. 293

Eliminate the phrase at the end of the first sentence of Paragraph 4, so that the first sentence now reads:

Obtain the names of subkeys by specifying a key to RegEnumKeyEx.

Likewise, remove the last sentence in the second to last paragraph. The sentence to be removed currently reads "RegistryQueryInfoKey is ... index." Notice that there is a function RegQueryInfoKey, but its job is to retrieve information regarding a specified key; among other things, it will find the number of subkeys.

Appendix C -- p. 338

See atou for additional atou performance comparisons that modify and extend some of the conclusions in the text.

SEVERITY C CHANGES

p. 25

Middle of page, last of 5 bullets, fdwShareMode should be fdwAccess.

p. 29

At the end of the very last line, replace _tstrcpy with _tcscpy.

p. 31

In the third line after "The Generic Main Function" heading, change UNICODE to _UNICODE

p. 39

In the 1st line of the 4th paragraph, change GetErrorNumber () to

The value returned by GetLastError ()

p. 45

Change the third bullet down on the page to:

Since Windows 95/98 does not implement MoveFileEx, you

p. 60

The fourth line of the function should be LPTSTR lpszTempFile (not LPCTSTR)

p. 60

The function GetTempFileName is type UINT (not UNIT).

p. 60

At end of second paragraph under Parameters, the flag name should be FILE_FLAG_DELETE_ON_CLOSE

p. 61

In next to last line on page (Program 4-2) replace STAR with _T ("*")

p. 65

For more information on the C Run-Time Library, see Win32 vs. The C Run-Time Library for File I/O. comp.os.ms-windows.programmer.win32 is also an excellent place to go for additional information or to get answers to your questions.

p. 68

In Exercise 4-13, FILETIME uses 64 bits, not 63.

p. 73

Between the two gray boxes, the structure is EXCEPTION_POINTERS (not EXCEPTION_RECORD)

p. 82

The first sentence of the second paragraph after "Global and Local Unwinds" should be modified to read as follows:

For example...of the preceding section before the floating-point exceptions are enabled.

p. 98

The final parameter, lpMem, has type LPCVOID, not LPSTR as shown.

p. 108

The allocation granularity may not necessarily be 64K, contrary to what is stated. The actual granularity is the dwAllocationGranularity member of the SYSTEM_INFO structure, which you can read using the GetSystemInfo() function.

p. 126

In the first gray box, change the 3rd and 5th items to type LPTSTR:

LPTSTR lpszAccount,

LPTSTR lpszReferencedDomain,

p. 126

In the second sentence after this first gray box, change "Obtain the thread's" to:

Obtain the process's

p. 136

Under the heading "Example: Reading File Permissions," the first sentence should begin, "Proram 7-4..." (not 7-3).

p. 145

5th line from the bottom of the page should be LPCTSTR:

LPCTSTR lpszCurDir,

p. 146

Revise the second sentence of the third bullet:

All processes in a group receive a console control signal (Ctrl-c or Ctrl-break) if they all share the same console.

p. 147

Change the second sentence of the UNIX section at the bottom of the page to:

First, Win32 has no equivalent to the UNIX fork() function, which makes a copy of the parent, including the parent's data space, heap, and stack.

p. 155

Add an additional return value, WAIT_FAILED, for the wait functions. This value, of course, indicates that the call failed.

p. 156

In the "Process Security" section, the access right CREATE_PROCESS should be PROCESS_CREATE_PROCESS. DUPLICATE_HANDLE should be PROCESS_DUP_HANDLE, and PROCESS_CREATE_THREAD instead of CREATE_THREAD (Use the PROCESS_ prefix on all process access rights).

p. 162

In the second line, the name of Program 8-1 should be grepMP.

p. 165

In the first sentence of the "UNIX" section, the reference should be to Table 8-1, not Figure 8-1.

p. 178

There is material added to the end of Exercise 8-7:

...so that the ID will not be reused. Another technique would be to include the process start time in the job management file. This time should be the same as the process start time of the process obtained from the process ID. This technique will not work with Windows 95/98, however, as the process start time is not available.

p. 185

Italicize [path] near the top of the page.

p. 185

In the second group of bulleted items in the middle of the page, change the first bullet into two:

  • PIPE_TYPE_BYTE and PIPE_TYPE_MESSAGE, which are mutually exclusive, indicate whether data is written to the pipe as a stream of bytes or messages. Use the same type value for all pipe instances.
  • PIPE_READMODE_BYTE and PIPE_READMODE_MESSAGE indicate whether data is read as a stream of bytes or messages. PIPE_READMODE_MESSAGE requires PIPE_TYPE_MESSAGE.

p. 186

Italicize [path] in two places at the bottom of the page.

p. 187

Windows 95/98 systems cannot be named pipe servers, although this fact is not well documented. Furthermore, under W95, CreateNamedPipe actually returns a valid handle, but a client CreateFile fails to find the named pipe. The new example program reimplementing the pipe program (Program 9-1) with named pipes demonstrates this behavior. [Go to pipeNP.c program.]

p. 189

Change the beginning of the page to:

  • NMPWAIT_USE_DEFAULT_WAIT, which uses the default time-out period specified by CreateNamedPipe.

The next function also allows...

p. 192

In Program 9-3, there are two additions to reflect the changes to InitUnixSA. In the 9th row down, add hSecHeap

HANDLE hNamedPipe, hTempFile, hSecHeap;

in the last line on this page, add &hSecHeap:

pNPSA = InitializeUnixSA (0440, argv[1], argv[2], AceMasks, &hSecHeap);

p. 201

At the end of Exercise 9-3, it should say:

Also, confirm the correct operation...

p. 206

In the ExitThread box, it should say VOID ExitThread (DWORD dwExitCode)

9 lines up from the bottom of the page should say, "Windows NT Version 3.51 did not allow ..."

p. 213

The comment in the 13th line of Program 10-2 should be:

/* Thread number: 0, 1, 2, ... */

p. 216

The last sentence of the first paragraph under the heading "Thread Local Storage" should reference Program 10-1.

p. 217

The last line on the page should be REALTIME_PRIORITY_CLASS (there is no underbar in REALTIME).

p. 222

Exercise 10-7, as stated, does not make sense as TLS cannot pass data between threads.

p. 230

The reference to Chapter 7 at the bottom should be to Chapter 5:

Completion handlers (Chapter 5) can be...

p. 231

In the sixth line down, _finally { should begin with a small f (not a capital "F").

p. 231

Text has been added to the end of the "Heap Synchronization" section:

...the HEAP_NO_SERIALIZE flag or when it is necessary for a thread to have exclusive access to a heap.

p. 231

Change last sentence at bottom of page to:

This feature would be useful for restricting access to a recursive function or in an application that implements nested transactions.

p. 232

  • First paragraph: see the comment for p. 228.
  • See the end of the paragraph discussing lpszMutexName (middle of the page). A CreateMutex using the name of a mutex that already exists will succeed, although the fInitialOwner flag is ignored. Furthermore, MUTEX_ALL_ACCESS access is requested. If the name refers to an existing event, semaphore, or file-mapping object, the call will fail. If you want to find out whether or not a mutex with a particular name exists, use OpenMutex.

p. 245

In the middle of the page, 4th line down after "Maintaining and Reporting Server Statistics," there is a word missing:

...this code shows some of the essential processing...

p. 288-289

All references to SwitchThreadToFiber should be to SwitchToFiber. There are three references, as well as the index.

p. 306

The very last line (after item 3, Select...) should be:

This technique will define _MT on the command line generated to invoke the compiler.

p. 334

Change items 2 and 3 under "Host Systems" to:

  1. A 66MHz 486 with 40MB of RAM, Windows NT 4.0, and a FAT file system.
  2. The same 66MHz 486 system using an NTFS file system on a separate, but identical, drive.

Bibliography

Custer's book has been updated by David A Solomon as a second edition of Inside Windows NT. The edition is updated and expanded to include file system and Version 4.0 information. The ISBN is 1-572-31677-2.

Index

Add new terms:

PIPE_TYPE_MESSAGE flag p. 185

PIPE_READMODE_BYTE flag p. 185

Change LPSYSTEM_INFO on p. 92 to SYSTEM_INFO

Add p. 58 for FILETIME


NEW PROGRAM LISTINGS


Program 7-1 the chmod Command

/* Chapter 7. chmod command. */
/* chmod [options] mode file [GroupName]
        Update access rights of the named file.
        Options:
                -f Force - do not complain if unable to change.
                -c Create the file if it does not exist.
                        The optional group name is after the file name. */
#include "EvryThng.h"
int _tmain (int argc, LPTSTR argv [])
{
        HANDLE hFile, hSecHeap;
        BOOL Force, CreateNew, Change, Exists;
        DWORD Mode, DecMode, UsrCnt = ACCT_NAME_SIZE;
        TCHAR UsrNam [ACCT_NAME_SIZE];
        int FileIndex, GrpIndex, ModeIndex;
                /* Array of rights settings in "UNIX order". */
        DWORD AceMasks [] = {GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE};
        LPSECURITY_ATTRIBUTES pSa = NULL;
        ModeIndex = Options (argc, argv, _T ("fc"), &Force, &CreateNew, NULL);
        GrpIndex = ModeIndex + 2;
        FileIndex = ModeIndex + 1;
        DecMode = _ttoi (argv [ModeIndex]);
                        /* The security mode is in octal (base 8). */
        Mode = ((DecMode / 100) % 10) * 64 /* Decimal conversion. */
                + ((DecMode / 10) % 10) * 8 + (DecMode % 10);
        Exists = (_taccess (argv [FileIndex], 0) == 0);
        if (!Exists && CreateNew) {
                        /* File does not exist; create a new one. */
                GetUserName (UsrNam, &UsrCnt);
                pSa = InitializeUnixSA (Mode, UsrNam, argv [GrpIndex],
                AceMasks, &hSecHeap);
                hFile = CreateFile (argv [FileIndex], 0, 0, pSa,
                                CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
                CloseHandle (hFile);
                HeapDestroy (hSecHeap); /* Release security structures. */
        }
        else if (Exists)
        { /* File does exist; change permissions. */
                Change = ChangeFilePermissions (Mode, argv [FileIndex], AceMasks);
        }
        return 0;
}

Program 7-3 InitUnFp: Initializing Security Attributes

/* Set UNIX-style permissions as ACEs in a Win32
        (Windows NT/NTFS only) SECURITY_ATTRIBUTES structure. */
#include "EvryThng.h" 
#define ACL_SIZE 1024
#define INIT_EXCEPTION 0x3
#define CHANGE_EXCEPTION 0x4
#define SID_SIZE LUSIZE
#define DOM_SIZE LUSIZE
LPSECURITY_ATTRIBUTES InitializeUnixSA (DWORD UnixPerms,
        LPCTSTR UsrNam, LPCTSTR GrpNam, LPDWORD AceMasks, LPHANDLE pHeap)
{
        HANDLE SAHeap = HeapCreate (HEAP_GENERATE_EXCEPTIONS, 0, 0);
        LPSECURITY_ATTRIBUTES pSA = NULL;
        PSECURITY_DESCRIPTOR pSD = NULL;
        PACL pAcl = NULL;
        BOOL Success;
        DWORD iBit, iSid, UsrCnt = ACCT_NAME_SIZE;
                        /* Tables of User, Group, and Everyone Names, SIDs,
                        etc. for LookupAccountName and SID creation. */
        LPCTSTR pGrpNms [3] = {EMPTY, EMPTY, _T ("Everyone")};
        PSID pSidTable [3] = {NULL, NULL, NULL};
        SID_NAME_USE sNamUse [3] =
                        {SidTypeUser, SidTypeGroup, SidTypeWellKnownGroup};
        TCHAR RefDomain [3] [DOM_SIZE];
        DWORD RefDomCnt [3] = {DOM_SIZE, DOM_SIZE, DOM_SIZE};
        DWORD SidCnt [3] = {SID_SIZE, SID_SIZE, SID_SIZE};
__try { /* Try-except block for memory allocation failures. */
        *pHeap = SAHeap;
        pSA = HeapAlloc (SAHeap, 0, sizeof (SECURITY_ATTRIBUTES));
        pSA->nLength = sizeof (SECURITY_ATTRIBUTES);
        pSA->bInheritHandle = FALSE;
                        /* Programmer can set this later. */
        pSD = HeapAlloc (SAHeap, 0, sizeof (SECURITY_DESCRIPTOR));
        pSA->lpSecurityDescriptor = pSD;
        InitializeSecurityDescriptor (pSD, SECURITY_DESCRIPTOR_REVISION); 
                        /* Get a SID for User, Group, and Everyone. */
        pGrpNms [0] = UsrNam; pGrpNms [1] = GrpNam;
        for (iSid = 0; iSid < 3; iSid++) {
                pSidTable [iSid] = HeapAlloc (SAHeap, 0, SID_SIZE);
                LookupAccountName (NULL, pGrpNms [iSid],
                                pSidTable [iSid], &SidCnt [iSid],
                                RefDomain [iSid], &RefDomCnt [iSid],
                                &sNamUse [iSid]);
        }
        SetSecurityDescriptorOwner (pSD, pSidTable [0], FALSE);
        SetSecurityDescriptorGroup (pSD, pSidTable [1], FALSE);
        pAcl = HeapAlloc (ProcHeap, HEAP_GENERATE_EXCEPTIONS, ACL_SIZE);
        InitializeAcl (pAcl, ACL_SIZE, ACL_REVISION);
                        /* Add all the access allowed/denied ACEs. */
        for (iBit = 0; iBit < 9; iBit++) {
                if ((UnixPerms >> (8 - iBit) & 0x1) != 0 &&
                                AceMasks[iBit%3] != 0)
                        AddAccessAllowedAce (pAcl, ACL_REVISION,
                                AceMasks [iBit%3], pSidTable [iBit/3]);
                else if (AceMasks[iBit%3] != 0)
                        AddAccessDeniedAce (pAcl, ACL_REVISION,
                                AceMasks [iBit%3], pSidTable [iBit/3]);
        }
                        /* Associate ACL with the security descriptor. */
        SetSecurityDescriptorDacl (pSD, TRUE, pAcl, FALSE);
        return pSA;
} /* End of __try-except block. */
__except (EXCEPTION_EXECUTE_HANDLER) { /* Free all resources. */
        if (SAHeap != NULL)
                HeapDestroy (SAHeap);
        pSA = NULL;
}
return pSA;
}

Program 7-5 ChangeFilePermissions: Changing Security Attributes

BOOL ChangeFilePermissions (DWORD fPm, LPCTSTR FNm, LPDWORD AceMsk)
        /* Change permissions in existing file. Group is left unchanged. */
{
        TCHAR UsrNm [ACCT_NAME_SIZE], GrpNm [ACCT_NAME_SIZE];
        DWORD OldfPerm;
        LPSECURITY_ATTRIBUTES pSA;
        PSECURITY_DESCRIPTOR pSD = NULL;
        HANDLE hSecHeap;
        if (_taccess (FNm, 0) != 0) return FALSE;
        OldfPerm = ReadFilePermissions (FNm, UsrNm, GrpNm);
        pSA = InitializeUnixSA (fPm, UsrNm, GrpNm, AceMsk, &hSecHeap);
        pSD = pSA->lpSecurityDescriptor;
        SetFileSecurity (FileName, DACL_SECURITY_INFORMATION, pSD);
        HeapDestroy (hSecHeap); return TRUE;
}

Program 8-2 timep: Process Times

/* Chapter 8. timep. Version for Windows 95/98 & NT. */
#include "EvryThng.h"
int _tmain (int argc, LPTSTR argv [])
{
        STARTUPINFO StartUp;
        PROCESS_INFORMATION ProcInfo;
        union { /* Structure required for file time arithmetic. */
                LONGLONG li;
                FILETIME ft;
        } CreateTime, ExitTime, ElapsedTime;
        FILETIME KernelTime, UserTime;
        SYSTEMTIME ElTiSys, KeTiSys, UsTiSys, StartTimeSys, ExitTimeSys;
        LPTSTR targv = SkipArg (GetCommandLine ());
        OSVERSIONINFO OSVer;
        OSVer.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
        GetVersionEx (&OSVer); /* WNT or W95? */
        GetStartupInfo (&StartUp);
        GetSystemTime (&StartTimeSys);
                /* Execute command line and wait for the process to complete. */
        CreateProcess (NULL, targv, NULL, NULL, TRUE,
                        NORMAL_PRIORITY_CLASS, NULL, NULL, &StartUp, &ProcInfo)
        WaitForSingleObject (ProcInfo.hProcess, INFINITE);
        GetSystemTime (&ExitTimeSys);
        if (OSVer.dwPlatformId == VER_PLATFORM_WIN32_NT) {
                        /* Windows NT. Elapsed, Kernel, & System times. */
                GetProcessTimes (ProcInfo.hProcess, &CreateTime.ft,
                                &ExitTime.ft, &KernelTime, &UserTime);
                ElapsedTime.li = ExitTime.li - CreateTime.li;
                FileTimeToSystemTime (&ElapsedTime.ft, &ElTiSys);
                FileTimeToSystemTime (&KernelTime, &KeTiSys);
                FileTimeToSystemTime (&UserTime, &UsTiSys);
                _tprintf (_T ("\nReal Time: %02d:%02d:%02d:%03d\n"),
                                ElTiSys.wHour, ElTiSys.wMinute, ElTiSys.wSecond,
                                ElTiSys.wMilliseconds);
                _tprintf (_T ("User Time: %02d:%02d:%02d:%03d\n"),
                                UsTiSys.wHour, UsTiSys.wMinute, UsTiSys.wSecond,
                                UsTiSys.wMilliseconds);
                _tprintf (_T ("Sys Time: %02d:%02d:%02d:%03d\n"),
                                KeTiSys.wHour, KeTiSys.wMinute, KeTiSys.wSecond,
                                KeTiSys.wMilliseconds);
        } else {
                        /* Windows 95/98. Elapsed time only. */
                SystemTimeToFileTime (&StartTimeSys, &CreateTime.ft);
                SystemTimeToFileTime (&ExitTimeSys, &ExitTime.ft);
                ElapsedTime.li = ExitTime.li - CreateTime.li;
                FileTimeToSystemTime (&ElapsedTime.ft, &ElTiSys);
                _tprintf (_T ("\nReal Time: %02d:%02d:%02d:%03d\n"),
                                ElTiSys.wHour, ElTiSys.wMinute, ElTiSys.wSecond,
                                ElTiSys.wMilliseconds);
        }
        CloseHandle (ProcInfo.hThread);
        CloseHandle (ProcInfo.hProcess);
        return 0;
}