Leaked source code of windows server 2003
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1053 lines
26 KiB

/*++
Copyright (c) 1994-1998 Microsoft Corporation
Module Name:
tlist.c
Abstract:
This module implements a task list application.
Author:
Wesley Witt (wesw) 20-May-1994
Mike Sartain (mikesart) 28-Oct-1994 Added detailed task information
Julian Jiggins (julianj) 19-Mar-1998 Added list processes using specific module feature
Shaun Cox (shaunco) 9-Jul-1998 Display services running in processes
Environment:
User Mode
--*/
#include "pch.h"
#pragma hdrstop
#include <dbghelp.h>
#include "psapi.h"
#include <tchar.h>
#define BAD_PID ((DWORD)-1)
DWORD numTasks;
TASK_LIST tlist[MAX_TASKS];
BOOL fShowServices;
BOOL fShowMtsPackages;
BOOL fShowSessionIds;
BOOL fShowCommandLines;
const char *Blanks = " ";
VOID Usage(VOID);
VOID PrintThreadInfo(PTASK_LIST pTaskList);
BOOL FMatchTaskName(LPTSTR szPN, LPTSTR szWindowTitle, LPTSTR szProcessName);
VOID GetFirstPidWithName(LPTSTR szTask);
VOID
PrintTask(
DWORD i
)
{
BOOL NameShown = FALSE;
if (fShowSessionIds) {
printf("%2d ", tlist[i].SessionId);
}
printf( "%4d %-15s ", tlist[i].dwProcessId, tlist[i].ProcessName );
if (fShowServices && tlist[i].ServiceNames[0]) {
printf( "Svcs: %s", tlist[i].ServiceNames);
NameShown = TRUE;
}
if (fShowMtsPackages && tlist[i].MtsPackageNames[0]) {
printf( "%sMts: %s", NameShown ? " " : "",
tlist[i].MtsPackageNames);
NameShown = TRUE;
}
if (!NameShown && tlist[i].hwnd) {
if (fShowServices || fShowMtsPackages) {
printf( "Title: %s", tlist[i].WindowTitle );
}
else {
printf( " %s", tlist[i].WindowTitle );
}
}
printf( "\n" );
if (fShowCommandLines) {
printf(" Command Line: %s\n", tlist[i].CommandLine);
}
}
VOID
PrintTaskTree(
DWORD level,
DWORD id
)
{
DWORD i;
DetectOrphans( tlist, numTasks );
for (i=0; i<numTasks; i++) {
if (tlist[i].flags) {
continue;
}
// NOTE: The format of the output below should stay fixed forever. There are tools
// at MS that depend on it.
if (level == 0 || tlist[i].dwInheritedFromProcessId == id) {
printf( "%.*s", level*2, Blanks );
printf( "%s (%d)", tlist[i].ProcessName, tlist[i].dwProcessId );
if (tlist[i].hwnd) {
printf( " %s", tlist[i].WindowTitle );
}
printf( "\n" );
tlist[i].flags = TRUE;
if (tlist[i].dwProcessId != 0) {
PrintTaskTree( level+1, tlist[i].dwProcessId );
}
}
}
}
int __cdecl
main(
int argc,
char *argv[]
)
/*++
Routine Description:
Main entrypoint for the TLIST application. This app prints
a task list to stdout. The task list include the process id,
task name, ant the window title.
Arguments:
argc - argument count
argv - array of pointers to arguments
Return Value:
0 - success
--*/
{
DWORD i;
TASK_LIST_ENUM te;
BOOL fTree;
BOOL fFindTasksUsingModule;
BOOL fPidOnly = FALSE;
DWORD cchPN = 0;
LPSTR szPN = NULL;
DWORD dwPID = BAD_PID;
DWORD dwNumServices = 0;
LPENUM_SERVICE_STATUS_PROCESS pServiceInfo = NULL;
if (argc > 1 && (argv[1][0] == '-' || argv[1][0] == '/') && argv[1][1] == '?') {
Usage();
}
fTree = FALSE;
fFindTasksUsingModule = FALSE;
if (argc > 1) {
if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 't' || argv[1][1] == 'T')) {
fTree = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 's' || argv[1][1] == 'S')) {
fShowServices = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'k' || argv[1][1] == 'K')) {
fShowMtsPackages = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'e' || argv[1][1] == 'E')) {
fShowSessionIds = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'c' || argv[1][1] == 'C')) {
fShowCommandLines = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'v' || argv[1][1] == 'V')) {
fShowServices = TRUE;
fShowMtsPackages = TRUE;
fShowSessionIds = TRUE;
fShowCommandLines = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'p' || argv[1][1] == 'P') && argc == 3) {
_strlwr(argv[2]);
if (!strcmp(argv[2], "system process")) {
printf("0\n");
return 0;
}
fPidOnly = TRUE;
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'p' || argv[1][1] == 'P') && argc == 4) {
_strlwr(argv[2]);
if (!strcmp(argv[2], "system")) {
_strlwr(argv[3]);
if (!strcmp(argv[3], "process")) {
printf("0\n");
return 0;
}
}
Usage();
} else if ((argv[1][0] == '-' || argv[1][0] == '/') &&
(argv[1][1] == 'm' || argv[1][1] == 'M') && argc == 3) {
fFindTasksUsingModule = TRUE;
} else {
szPN = argv[1];
if (!(dwPID = atol(szPN)) && szPN[0] != '0' && szPN[1] != 0) {
dwPID = BAD_PID;
cchPN = strlen(szPN);
_strupr(szPN);
}
}
}
//
// lets be god
//
EnableDebugPriv();
//
// Include 32bit modules in enumeration
//
{
DWORD SymOpt = SymGetOptions();
SymOpt |= SYMOPT_INCLUDE_32BIT_MODULES;
SymSetOptions(SYMOPT_INCLUDE_32BIT_MODULES);
}
//
// get the task list for the system
//
//
// Get the process information for all active Win32 services.
// This allows us to print the service names next to the processes
// that host them.
//
dwNumServices = GetServiceProcessInfo( &pServiceInfo );
numTasks = GetTaskListEx(
tlist,
MAX_TASKS,
cchPN || (dwPID != BAD_PID),
dwNumServices,
pServiceInfo);
free( pServiceInfo );
if (fShowMtsPackages) {
AddMtsPackageNames(tlist, numTasks);
}
if (fShowCommandLines) {
AddCommandLines(tlist, numTasks);
}
//
// enumerate all windows and try to get the window
// titles for each task
//
te.tlist = tlist;
te.numtasks = numTasks;
GetWindowTitles( &te );
//
// print the task list
//
if (fTree) {
PrintTaskTree( 0, 0 );
} else if (fFindTasksUsingModule) {
PrintTasksUsingModule(argv[2]);
} else if (fPidOnly) {
GetFirstPidWithName(argv[2]);
} else {
for (i=0; i<numTasks; i++) {
if ((dwPID == BAD_PID) && (!cchPN)) {
PrintTask( i );
}
else
if ((dwPID == tlist[i].dwProcessId) ||
(cchPN && FMatchTaskName(szPN, tlist[i].WindowTitle, tlist[i].ProcessName))) {
PrintTask( i );
PrintThreadInfo(tlist + i);
}
if (tlist[i].pThreadInfo) {
free(tlist[i].pThreadInfo);
}
}
}
//
// end of program
//
return 0;
}
VOID
GetFirstPidWithName(
LPTSTR szTask
)
/*++
Routine Description:
Returns the PID of the first task with a Name matching the specified
Name. IF no task is found -1 is returned
Arguments:
szTask - module name to search for
--*/
{
DWORD i;
TCHAR szPName[PROCESS_SIZE + 1];
TCHAR szNameWExe[PROCESS_SIZE + 1] = {0};
_tcsncat(szNameWExe, szTask, (sizeof(szNameWExe)/sizeof(szNameWExe[0]))-1);
_tcsncat(szNameWExe, ".exe", (sizeof(szNameWExe)/sizeof(szNameWExe[0]))-_tcslen(szNameWExe)-1);
for (i=0; i<numTasks; i++) {
ZeroMemory(szPName, sizeof(szPName));
_tcsncat(szPName, tlist[i].ProcessName, (sizeof(szPName)/sizeof(szPName[0]))-1);
_tcslwr(szPName);
if ((!_tcscmp(szPName, szTask))||(!_tcscmp(szPName, szNameWExe))) {
if (tlist[i].dwProcessId != 0) {
printf("%d\n", tlist[i].dwProcessId);
return;
}
}
}
printf("-1\n");
}
VOID
Usage(
VOID
)
/*++
Routine Description:
Prints usage text for this tool.
Arguments:
None.
Return Value:
None.
--*/
{
fprintf( stderr,
"Microsoft (R) Windows NT (TM) Version 5.1 TLIST\n"
VER_LEGALCOPYRIGHT_STR
"\n\n"
"usage: TLIST"
" <<-m <pattern>> | <-t> | <pid> | <pattern> | <-p <processname>>> | <-k> | <-s>\n"
" [options]:\n"
" -t\n"
" Print Task Tree\n\n"
" <pid>\n"
" List module information for this task.\n\n"
" <pattern>\n"
" The pattern can be a complete task\n"
" name or a regular expression pattern\n"
" to use as a match. Tlist matches the\n"
" supplied pattern against the task names\n"
" and the window titles.\n\n"
" -c\n"
" Show command lines for each process\n\n"
" -e\n"
" Show session IDs for each process\n\n"
" -k\n"
" Show MTS packages active in each process.\n\n"
" -m <pattern>\n"
" Lists all tasks that have DLL modules loaded\n"
" in them that match the given pattern name\n\n"
" -s\n"
" Show services active in each process.\n\n"
" -p <processname>\n"
" Returns the PID of the process specified or -1\n"
" if the specified process doesn't exist. If there\n"
" are multiple instances of the process running only\n"
" the instance with the first PID value is returned.\n\n"
" -v\n"
" Show all process information\n\n"
);
ExitProcess(0);
}
//
// Routines used to list all processes that have a specific module in use
//
BOOL
FindSpecificModuleCallback(
LPSTR Name,
DWORD_PTR Base,
DWORD Size,
PVOID Context
)
/*++
Routine Description:
Callback function for module enumeration to find a specific module
Arguments:
Name - Module name
Base - Base address
Size - Size of image
Context - User context pointer
Return Value:
TRUE - Continue enumeration
FALSE - Stop enumeration
--*/
{
PFIND_MODULE_INFO pFindModuleInfo;
pFindModuleInfo = (PFIND_MODULE_INFO)Context;
if (MatchPattern(Name, pFindModuleInfo->szModuleToFind))
{
pFindModuleInfo->fFound = TRUE;
strcpy(pFindModuleInfo->szMatchingModuleName, Name);
return FALSE; // Found Module so stop enumerating
}
return TRUE;
}
BOOL
IsTaskUsingModule(
PTASK_LIST pTask,
LPTSTR szModuleName,
LPTSTR szMatchingModuleName
)
/*++
Routine Description:
Checks if the given task has the given module loaded
Arguments:
pTaskList - task to search for module
szModule - module name to search for
Return Value:
TRUE - if the module is loaded in the task
FALSE - if the module is not loaded in the task
--*/
{
FIND_MODULE_INFO FindModuleInfo;
FindModuleInfo.fFound = FALSE;
FindModuleInfo.szModuleToFind = szModuleName;
FindModuleInfo.szMatchingModuleName = szMatchingModuleName;
EnumerateLoadedModules(
(HANDLE) UlongToPtr(pTask->dwProcessId),
FindSpecificModuleCallback,
&FindModuleInfo
);
return FindModuleInfo.fFound;
}
void
PrintTasksUsingModule(
LPTSTR szModuleName
)
/*++
Routine Description:
Enumerates through all the tasks in the system looking for those that
have loaded modules of the given name.
Arguments:
szModule - module name to search for
Return Value:
None
--*/
{
BOOL fUsed = FALSE;
DWORD i;
CHAR szMatchingModuleName[64];
_strupr(szModuleName); // Needed for wildcarding
for (i=0; i<numTasks; i++) {
if (IsTaskUsingModule(tlist + i, szModuleName, szMatchingModuleName)) {
printf("%s - ", szMatchingModuleName);
PrintTask( i );
fUsed = TRUE;
}
}
if (!fUsed) {
printf( "No tasks found using %s\n", szModuleName );
}
}
BOOL
GetVersionStuff(
LPTSTR szFileName,
VS_FIXEDFILEINFO *pvsRet
)
/*++
Routine Description:
Get fixedfileinfo for szFileName.
Arguments:
szFileName - name of file
pvsRet - fixedfileinfo return struct
Return Value:
TRUE - success
FALSE - failure
--*/
{
DWORD dwHandle;
DWORD dwLength;
BOOL fRet = FALSE;
LPVOID lpvData = NULL;
if (!(dwLength = GetFileVersionInfoSize(szFileName, &dwHandle))) {
goto err;
}
if (lpvData = malloc(dwLength)) {
if (GetFileVersionInfo(szFileName, 0, dwLength, lpvData)) {
UINT uLen;
VS_FIXEDFILEINFO *pvs;
DWORD *pdwTranslation;
DWORD dwDefLang = 0x409;
if (!VerQueryValue(lpvData, "\\VarFileInfo\\Translation",
&pdwTranslation, &uLen)) {
// if we can't get the langid, default to usa
pdwTranslation = &dwDefLang;
uLen = sizeof(DWORD);
}
if (VerQueryValue(lpvData, "\\", (LPVOID *)&pvs, &uLen)) {
*pvsRet = *pvs;
fRet = TRUE;
}
}
}
err:
if (lpvData)
free(lpvData);
return fRet;
}
BOOL
EnumLoadedModulesCallback(
LPSTR Name,
DWORD_PTR Base,
DWORD Size,
PVOID Context
)
/*++
Routine Description:
Callback function for module enumeration
Arguments:
Name - Module name
Base - Base address
Size - Size of image
Context - User context pointer
Return Value:
TRUE - Continue enumeration
FALSE - Stop enumeration
--*/
{
VS_FIXEDFILEINFO vs;
CHAR szBuffer[100];
szBuffer[0] = 0;
if (GetVersionStuff( Name, &vs )) {
sprintf( szBuffer, "%u.%u.%u.%u %s",
HIWORD(vs.dwFileVersionMS),
LOWORD(vs.dwFileVersionMS),
HIWORD(vs.dwFileVersionLS),
LOWORD(vs.dwFileVersionLS),
vs.dwFileFlags & VS_FF_DEBUG ? "dbg" : "shp"
);
}
printf( " %18.18s 0x%p %s\n", szBuffer, (void *)Base, Name );
return TRUE;
}
BOOL
PrintModuleList(
ULONG ProcessId
)
/*++
Routine Description:
Prints list of modules in ProcessId
Arguments:
ProcessID - process id
Return Value:
TRUE - success
FALSE - failure
--*/
{
EnumerateLoadedModules(
(HANDLE) UlongToPtr(ProcessId),
EnumLoadedModulesCallback,
NULL
);
return TRUE;
}
DWORD
GetWin32StartAddress(
HANDLE hThread
)
/*++
Routine Description:
Get starting address for thread
Arguments:
hThread
Return Value:
Starting Thread address or 0
--*/
{
NTSTATUS Status;
DWORD ThreadInformation;
// make sure we have a handle
if (!hThread)
return 0;
// get the threadinfo
Status = NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress,
&ThreadInformation, sizeof(ThreadInformation), NULL);
if (!NT_SUCCESS(Status))
return 0;
return ThreadInformation;
}
ULONG
GetLastThreadErr(
HANDLE hThread
)
/*++
Routine Description:
Get Last Error for a Thread
Arguments:
hThread
Return Value:
LastError or 0
--*/
{
TEB Teb;
NTSTATUS Status;
HANDLE hProcess;
ULONG LastErrorValue;
THREAD_BASIC_INFORMATION ThreadInformation;
// make sure we have a handle
if (!hThread)
return 0;
// query for basic thread info
Status = NtQueryInformationThread(hThread, ThreadBasicInformation,
&ThreadInformation, sizeof(ThreadInformation), NULL);
if (!NT_SUCCESS(Status))
return 0;
// get handle to process
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
(DWORD)(DWORD_PTR)ThreadInformation.ClientId.UniqueProcess))) {
return 0;
}
__try {
// read the TEB from the process and get the last error value
if (ReadProcessMemory(hProcess,
ThreadInformation.TebBaseAddress, &Teb, sizeof(TEB), NULL)) {
LastErrorValue = Teb.LastErrorValue;
}
}
__except(EXCEPTION_EXECUTE_HANDLER) {
}
// close the hProcess
CloseHandle(hProcess);
return LastErrorValue;
}
BOOL
FPrintPEBInfo(
HANDLE hProcess
)
/*++
Routine Description:
Prints cmdline and cwd of hProcess
Arguments:
hProcess.
Return Value:
TRUE - success
FALSE - failure
--*/
{
PEB Peb;
NTSTATUS Status;
PROCESS_BASIC_INFORMATION BasicInfo;
BOOL fRet = FALSE;
WCHAR szT[MAX_PATH * 2];
RTL_USER_PROCESS_PARAMETERS ProcessParameters;
Status = NtQueryInformationProcess(hProcess, ProcessBasicInformation,
&BasicInfo, sizeof(BasicInfo), NULL);
if (!NT_SUCCESS(Status)) {
SetLastError(RtlNtStatusToDosError(Status));
return fRet;
}
__try {
// get the PEB
if (ReadProcessMemory(hProcess, BasicInfo.PebBaseAddress, &Peb,
sizeof(PEB), NULL)) {
// get the processparameters
if (ReadProcessMemory(hProcess, Peb.ProcessParameters,
&ProcessParameters, sizeof(ProcessParameters), NULL)) {
// get the CWD
if (ReadProcessMemory(hProcess,
ProcessParameters.CurrentDirectory.DosPath.Buffer, szT,
sizeof(szT), NULL)) {
wprintf(L" CWD: %s\n", szT);
}
// get cmdline
if (ReadProcessMemory(hProcess, ProcessParameters.CommandLine.Buffer,
szT, sizeof(szT), NULL)) {
wprintf(L" CmdLine: %s\n", szT);
}
fRet = TRUE;
}
}
}
__except(EXCEPTION_EXECUTE_HANDLER) {
}
return fRet;
}
// copied from the win32 API code since we need to run on NT 4 and this is a
// new API to NT 5
HANDLE
TlistOpenThread(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwThreadId
)
/*++
Routine Description:
A handle to a thread object may be created using OpenThread.
Opening a thread creates a handle to the specified thread.
Associated with the thread handle is a set of access rights that
may be performed using the thread handle. The caller specifies the
desired access to the thread using the DesiredAccess parameter.
Arguments:
mDesiredAccess - Supplies the desired access to the thread object.
For NT/Win32, this access is checked against any security
descriptor on the target thread. The following object type
specific access flags can be specified in addition to the
STANDARD_RIGHTS_REQUIRED access flags.
DesiredAccess Flags:
THREAD_TERMINATE - This access is required to terminate the
thread using TerminateThread.
THREAD_SUSPEND_RESUME - This access is required to suspend and
resume the thread using SuspendThread and ResumeThread.
THREAD_GET_CONTEXT - This access is required to use the
GetThreadContext API on a thread object.
THREAD_SET_CONTEXT - This access is required to use the
SetThreadContext API on a thread object.
THREAD_SET_INFORMATION - This access is required to set certain
information in the thread object.
THREAD_SET_THREAD_TOKEN - This access is required to set the
thread token using SetTokenInformation.
THREAD_QUERY_INFORMATION - This access is required to read
certain information from the thread object.
SYNCHRONIZE - This access is required to wait on a thread object.
THREAD_ALL_ACCESS - This set of access flags specifies all of the
possible access flags for a thread object.
bInheritHandle - Supplies a flag that indicates whether or not the
returned handle is to be inherited by a new process during
process creation. A value of TRUE indicates that the new
process will inherit the handle.
dwThreadId - Supplies the thread id of the thread to open.
Return Value:
NON-NULL - Returns an open handle to the specified thread. The
handle may be used by the calling process in any API that
requires a handle to a thread. If the open is successful, the
handle is granted access to the thread object only to the
extent that it requested access through the DesiredAccess
parameter.
NULL - The operation failed. Extended error status is available
using GetLastError.
--*/
{
NTSTATUS Status;
OBJECT_ATTRIBUTES Obja;
HANDLE Handle;
CLIENT_ID ClientId;
ClientId.UniqueThread = (HANDLE)LongToHandle(dwThreadId);
ClientId.UniqueProcess = (HANDLE)NULL;
InitializeObjectAttributes(
&Obja,
NULL,
(bInheritHandle ? OBJ_INHERIT : 0),
NULL,
NULL
);
Status = NtOpenThread(
&Handle,
(ACCESS_MASK)dwDesiredAccess,
&Obja,
&ClientId
);
if ( NT_SUCCESS(Status) ) {
return Handle;
}
else {
return NULL;
}
}
VOID
PrintThreadInfo(
PTASK_LIST pTaskList
)
/*++
Routine Description:
Prints all kinds of info about a task
Arguments:
PTASK_LIST of task to print
Return Value:
None.
--*/
{
UINT nThread;
HANDLE hProcess;
// from \\kernel\razzle2\src\ntos\inc\ke.h
#define MAX_THREADSTATE (sizeof(szThreadState) / sizeof(TCHAR *))
static const TCHAR *szThreadState[] = {
"Initialized",
"Ready ",
"Running ",
"Standby ",
"Terminated",
"Waiting ",
"Transition",
"??? " };
// get a handle to the process
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pTaskList->dwProcessId);
if (!hProcess)
return;
// print the CWD and CmdLine
FPrintPEBInfo(hProcess);
printf( " VirtualSize: %6ld KB"
" PeakVirtualSize: %6ld KB\n",
pTaskList->VirtualSize / 1024,
pTaskList->PeakVirtualSize / 1024);
printf( " WorkingSetSize:%6ld KB"
" PeakWorkingSetSize:%6ld KB\n",
pTaskList->WorkingSetSize / 1024,
pTaskList->PeakWorkingSetSize / 1024);
printf( " NumberOfThreads: %ld\n",
pTaskList->NumberOfThreads);
// if we got any threadinfo, spit it out
if (pTaskList->pThreadInfo) {
for (nThread = 0; nThread < pTaskList->NumberOfThreads; nThread++) {
PTHREAD_INFO pThreadInfo = &pTaskList->pThreadInfo[nThread];
HANDLE hThread = TlistOpenThread(THREAD_QUERY_INFORMATION, FALSE,
(DWORD)(DWORD_PTR)pThreadInfo->UniqueThread);
printf(" %4d Win32StartAddr:0x%08x LastErr:0x%08x State:%s\n",
HandleToUlong(pThreadInfo->UniqueThread),
GetWin32StartAddress(hThread),
GetLastThreadErr(hThread),
szThreadState[min(pThreadInfo->ThreadState, MAX_THREADSTATE - 1)]);
if (hThread)
NtClose(hThread);
}
}
// print the modules
PrintModuleList( pTaskList->dwProcessId );
// close the hProcess
CloseHandle(hProcess);
}
BOOL
FMatchTaskName(
LPTSTR szPN,
LPTSTR szWindowTitle,
LPTSTR szProcessName
)
{
LPTSTR szT;
TCHAR szTName[PROCESS_SIZE]= {0};
_tcsncat( szTName, szProcessName, (sizeof(szTName)/sizeof(szTName[0]))-1 );
if (szT = _tcschr( szTName, _T('.') ))
szT[0] = _T('\0');
if (MatchPattern( szTName, szPN ) ||
MatchPattern( szProcessName, szPN ) ||
MatchPattern( szWindowTitle, szPN )) {
return TRUE;
}
return FALSE;
}