Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
468 lines (388 sloc)
15.7 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @file lsof/lsof.c | |
* | |
* Yori determine which processes are keeping files open. | |
* | |
* Copyright (c) 2018-2021 Malcolm J. Smith | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in | |
* all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
* THE SOFTWARE. | |
*/ | |
#include <yoripch.h> | |
#include <yorilib.h> | |
/** | |
Help text to display to the user. | |
*/ | |
const | |
CHAR strLsofHelpText[] = | |
"\n" | |
"Determine which processes are keeping files open.\n" | |
"\n" | |
"LSOF [-license] [-b] [-s] <file>...\n" | |
"\n" | |
" -b Use basic search criteria for files only\n" | |
" -s Process files from all subdirectories\n"; | |
/** | |
Display usage text to the user. | |
*/ | |
BOOL | |
LsofHelp(VOID) | |
{ | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Lsof %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR); | |
#if YORI_BUILD_ID | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T(" Build %i\n"), YORI_BUILD_ID); | |
#endif | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strLsofHelpText); | |
return TRUE; | |
} | |
/** | |
Context passed to the callback which is invoked for each file found. | |
*/ | |
typedef struct _LSOF_CONTEXT { | |
/** | |
Counts the number of files processed in an enumerate. If this is zero, | |
the program assumes the request is to create a new file. | |
*/ | |
DWORD FilesFoundThisArg; | |
/** | |
The number of bytes in the Buffer below. | |
*/ | |
DWORD BufferLength; | |
/** | |
A buffer that is populated with the array of process IDs using a given | |
file. | |
*/ | |
PFILE_PROCESS_IDS_USING_FILE_INFORMATION Buffer; | |
} LSOF_CONTEXT, *PLSOF_CONTEXT; | |
/** | |
A callback that is invoked when a file is found that matches a search criteria | |
specified in the set of strings to enumerate. | |
@param FilePath Pointer to the file path that was found. | |
@param FileInfo Information about the file. Note in this application this can | |
be NULL when it is operating on files that do not yet exist. | |
@param Depth Specifies the recursion depth. Ignored in this application. | |
@param Context Pointer to the lsof context structure indicating the | |
action to perform and populated with the file and line count found. | |
@return TRUE to continute enumerating, FALSE to abort. | |
*/ | |
BOOL | |
LsofFileFoundCallback( | |
__in PYORI_STRING FilePath, | |
__in_opt PWIN32_FIND_DATA FileInfo, | |
__in DWORD Depth, | |
__in PVOID Context | |
) | |
{ | |
HANDLE FileHandle; | |
PLSOF_CONTEXT LsofContext = (PLSOF_CONTEXT)Context; | |
IO_STATUS_BLOCK IoStatus; | |
DWORD Index; | |
LONG Status; | |
UNREFERENCED_PARAMETER(Depth); | |
UNREFERENCED_PARAMETER(FileInfo); | |
ASSERT(YoriLibIsStringNullTerminated(FilePath)); | |
LsofContext->FilesFoundThisArg++; | |
FileHandle = CreateFile(FilePath->StartOfString, | |
FILE_READ_ATTRIBUTES, | |
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | |
NULL, | |
OPEN_EXISTING, | |
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, | |
NULL); | |
if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) { | |
DWORD LastError = GetLastError(); | |
if (LastError == ERROR_ACCESS_DENIED && | |
DllNtDll.pRtlGetLastNtStatus != NULL && | |
DllNtDll.pRtlGetLastNtStatus() == (LONG)0xC0000056) { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: open of %y failed: the file is delete pending\n"), FilePath); | |
} else { | |
LPTSTR ErrText = YoriLibGetWinErrorText(LastError); | |
YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: open of %y failed: %s"), FilePath, ErrText); | |
YoriLibFreeWinErrorText(ErrText); | |
} | |
return TRUE; | |
} | |
Status = DllNtDll.pNtQueryInformationFile(FileHandle, &IoStatus, LsofContext->Buffer, LsofContext->BufferLength, FileProcessIdsUsingFileInformation); | |
if (Status == 0) { | |
for (Index = 0; Index < LsofContext->Buffer->NumberOfProcesses; Index++) { | |
HANDLE ProcessHandle; | |
TCHAR ProcessName[300]; | |
DWORD ProcessNameSize; | |
ProcessName[0] = '\0'; | |
ProcessNameSize = sizeof(ProcessName)/sizeof(ProcessName[0]); | |
ProcessHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)LsofContext->Buffer->ProcessIds[Index]); | |
if (ProcessHandle != NULL) { | |
DllKernel32.pQueryFullProcessImageNameW(ProcessHandle, 0, ProcessName, &ProcessNameSize); | |
} | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%10i %s\n"), LsofContext->Buffer->ProcessIds[Index], ProcessName); | |
} | |
} else { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: query of %y failed: %08x"), FilePath, Status); | |
} | |
CloseHandle(FileHandle); | |
return TRUE; | |
} | |
/** | |
Display information about handles opened for all processes. | |
@return TRUE to indicate success, FALSE to indicate failure. | |
*/ | |
BOOLEAN | |
LsofDumpHandles(VOID) | |
{ | |
PYORI_SYSTEM_HANDLE_INFORMATION_EX Handles; | |
DWORD_PTR Index; | |
PYORI_SYSTEM_HANDLE_ENTRY_EX ThisHandle; | |
HANDLE LocalProcessHandle; | |
PYORI_OBJECT_NAME_INFORMATION ObjectName; | |
PYORI_OBJECT_TYPE_INFORMATION ObjectType; | |
YORI_STRING ModuleNameString; | |
YORI_STRING ObjectNameString; | |
YORI_STRING ObjectTypeString; | |
DWORD ObjectNameLength; | |
DWORD ObjectTypeLength; | |
DWORD LengthReturned; | |
HANDLE ProcessHandle; | |
DWORD LastPid; | |
ProcessHandle = INVALID_HANDLE_VALUE; | |
LastPid = 0; | |
YoriLibLoadPsapiFunctions(); | |
if (!YoriLibGetSystemHandlesList(&Handles)) { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: Error getting system handle list\n")); | |
return FALSE; | |
} | |
ObjectNameLength = 0x10000; | |
ObjectName = YoriLibMalloc(ObjectNameLength); | |
if (ObjectName == NULL) { | |
YoriLibFree(Handles); | |
return FALSE; | |
} | |
ObjectTypeLength = 0x1000; | |
ObjectType = YoriLibMalloc(ObjectTypeLength); | |
if (ObjectName == NULL) { | |
YoriLibFree(ObjectName); | |
YoriLibFree(Handles); | |
return FALSE; | |
} | |
if (!YoriLibAllocateString(&ModuleNameString, 0x8000)) { | |
YoriLibFree(ObjectType); | |
YoriLibFree(ObjectName); | |
YoriLibFree(Handles); | |
return FALSE; | |
} | |
for (Index = 0; Index < Handles->NumberOfHandles; Index++) { | |
ThisHandle = &Handles->Handles[Index]; | |
if (ProcessHandle == INVALID_HANDLE_VALUE || | |
LastPid != ThisHandle->ProcessId) { | |
if (ProcessHandle != INVALID_HANDLE_VALUE && | |
ProcessHandle != NULL) { | |
CloseHandle(ProcessHandle); | |
} | |
// | |
// This open may fail. If it does, we can't get information | |
// about this process, which makes displaying numeric values | |
// pointless. Leaving this value as NULL is used to suppress | |
// processing this process, but when we get to the next | |
// process (LastPid != ThisHandle->ProcessId), the next | |
// process will be opened. | |
// | |
ProcessHandle = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, (DWORD)ThisHandle->ProcessId); | |
if (ProcessHandle == NULL) { | |
ProcessHandle = OpenProcess(PROCESS_DUP_HANDLE, FALSE, (DWORD)ThisHandle->ProcessId); | |
} | |
LastPid = (DWORD)ThisHandle->ProcessId; | |
ModuleNameString.LengthInChars = 0; | |
if (DllPsapi.pGetModuleFileNameExW != NULL && | |
ProcessHandle != NULL) { | |
ModuleNameString.LengthInChars = DllPsapi.pGetModuleFileNameExW(ProcessHandle, NULL, ModuleNameString.StartOfString, ModuleNameString.LengthAllocated); | |
} | |
if (ProcessHandle == NULL) { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Process %i ** NO ACCESS **\n"), LastPid); | |
} else { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Process %i %y\n"), LastPid, &ModuleNameString); | |
} | |
} | |
if (ProcessHandle != NULL) { | |
ObjectName->Name.LengthInBytes = 0; | |
ObjectType->TypeName.LengthInBytes = 0; | |
// | |
// Get a local instance of the handle and see what information | |
// can be extracted from them | |
// | |
LocalProcessHandle = INVALID_HANDLE_VALUE; | |
if (DuplicateHandle(ProcessHandle, (HANDLE)ThisHandle->HandleValue, GetCurrentProcess(), &LocalProcessHandle, 0, FALSE, DUPLICATE_SAME_ACCESS)) { | |
DllNtDll.pNtQueryObject(LocalProcessHandle, 1, ObjectName, ObjectNameLength, &LengthReturned); | |
DllNtDll.pNtQueryObject(LocalProcessHandle, 2, ObjectType, ObjectTypeLength, &LengthReturned); | |
} else { | |
LocalProcessHandle = INVALID_HANDLE_VALUE; | |
} | |
// | |
// Convert any output into strings for display | |
// | |
YoriLibInitEmptyString(&ObjectNameString); | |
if (ObjectName->Name.LengthInBytes > 0) { | |
ObjectNameString.LengthInChars = ObjectName->Name.LengthInBytes / sizeof(WCHAR); | |
ObjectNameString.StartOfString = ObjectName->Name.Buffer; | |
} | |
YoriLibInitEmptyString(&ObjectTypeString); | |
if (ObjectType->TypeName.LengthInBytes > 0) { | |
ObjectTypeString.LengthInChars = ObjectType->TypeName.LengthInBytes / sizeof(WCHAR); | |
ObjectTypeString.StartOfString = ObjectType->TypeName.Buffer; | |
} | |
// | |
// Only display files, since that's part of the point of the | |
// program. | |
// | |
if (YoriLibCompareStringWithLiteralInsensitive(&ObjectTypeString, _T("File")) == 0) { | |
PYORI_STRING NameToDisplay; | |
NameToDisplay = &ObjectNameString; | |
// | |
// If it's possible to get a Win32 path name, display that. | |
// Otherwise, use what we have. | |
// | |
ModuleNameString.LengthInChars = 0; | |
if (DllKernel32.pGetFinalPathNameByHandleW != NULL) { | |
ModuleNameString.LengthInChars = DllKernel32.pGetFinalPathNameByHandleW(LocalProcessHandle, ModuleNameString.StartOfString, ModuleNameString.LengthAllocated, 0); | |
if (ModuleNameString.LengthInChars > 0 && | |
ModuleNameString.LengthInChars < ModuleNameString.LengthAllocated) { | |
NameToDisplay = &ModuleNameString; | |
} | |
} | |
YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T(" Handle %lli Object %p %y\n"), ThisHandle->HandleValue, ThisHandle->Object, NameToDisplay); | |
} | |
if (LocalProcessHandle != INVALID_HANDLE_VALUE) { | |
CloseHandle(LocalProcessHandle); | |
} | |
} | |
} | |
YoriLibFreeStringContents(&ModuleNameString); | |
YoriLibFree(ObjectType); | |
YoriLibFree(ObjectName); | |
YoriLibFree(Handles); | |
return TRUE; | |
} | |
#ifdef YORI_BUILTIN | |
/** | |
The main entrypoint for the lsof builtin command. | |
*/ | |
#define ENTRYPOINT YoriCmd_LSOF | |
#else | |
/** | |
The main entrypoint for the lsof standalone application. | |
*/ | |
#define ENTRYPOINT ymain | |
#endif | |
/** | |
The main entrypoint for the lsof cmdlet. | |
@param ArgC The number of arguments. | |
@param ArgV An array of arguments. | |
@return Exit code of the child process on success, or failure if the child | |
could not be launched. | |
*/ | |
DWORD | |
ENTRYPOINT( | |
__in DWORD ArgC, | |
__in YORI_STRING ArgV[] | |
) | |
{ | |
BOOL ArgumentUnderstood; | |
DWORD i; | |
DWORD StartArg = 0; | |
DWORD MatchFlags; | |
BOOL Recursive = FALSE; | |
BOOL BasicEnumeration = FALSE; | |
LSOF_CONTEXT LsofContext; | |
YORI_STRING Arg; | |
ZeroMemory(&LsofContext, sizeof(LsofContext)); | |
for (i = 1; i < ArgC; i++) { | |
ArgumentUnderstood = FALSE; | |
ASSERT(YoriLibIsStringNullTerminated(&ArgV[i])); | |
if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) { | |
if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) { | |
LsofHelp(); | |
return EXIT_SUCCESS; | |
} else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) { | |
YoriLibDisplayMitLicense(_T("2018-2021")); | |
return EXIT_SUCCESS; | |
} else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) { | |
BasicEnumeration = TRUE; | |
ArgumentUnderstood = TRUE; | |
} else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) { | |
Recursive = TRUE; | |
ArgumentUnderstood = TRUE; | |
} else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) { | |
StartArg = i + 1; | |
ArgumentUnderstood = TRUE; | |
break; | |
} | |
} else { | |
ArgumentUnderstood = TRUE; | |
StartArg = i; | |
break; | |
} | |
if (!ArgumentUnderstood) { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]); | |
} | |
} | |
if (StartArg == 0 || StartArg == ArgC) { | |
if (!LsofDumpHandles()) { | |
return EXIT_FAILURE; | |
} | |
} else { | |
if (DllNtDll.pNtQueryInformationFile == NULL || | |
DllKernel32.pQueryFullProcessImageNameW == NULL) { | |
YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: OS support not present\n")); | |
return EXIT_FAILURE; | |
} | |
// | |
// Attempt to enable backup privilege so an administrator can access | |
// more objects successfully. | |
// | |
YoriLibEnableBackupPrivilege(); | |
LsofContext.BufferLength = 16 * 1024; | |
LsofContext.Buffer = YoriLibMalloc(LsofContext.BufferLength); | |
if (LsofContext.Buffer == NULL) { | |
return EXIT_FAILURE; | |
} | |
// | |
// If no file name is specified, use stdin; otherwise open | |
// the file and use that | |
// | |
MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_RETURN_DIRECTORIES; | |
if (Recursive) { | |
MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD; | |
} | |
if (BasicEnumeration) { | |
MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION; | |
} | |
for (i = StartArg; i < ArgC; i++) { | |
LsofContext.FilesFoundThisArg = 0; | |
YoriLibForEachStream(&ArgV[i], MatchFlags, 0, LsofFileFoundCallback, NULL, &LsofContext); | |
if (LsofContext.FilesFoundThisArg == 0) { | |
YORI_STRING FullPath; | |
YoriLibInitEmptyString(&FullPath); | |
if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) { | |
LsofFileFoundCallback(&FullPath, NULL, 0, &LsofContext); | |
YoriLibFreeStringContents(&FullPath); | |
} | |
} | |
} | |
YoriLibFree(LsofContext.Buffer); | |
} | |
return EXIT_SUCCESS; | |
} | |
// vim:sw=4:ts=4:et: |