diff --git a/LogMonitor/docs/README.md b/LogMonitor/docs/README.md index 820233d..52aa090 100644 --- a/LogMonitor/docs/README.md +++ b/LogMonitor/docs/README.md @@ -214,6 +214,33 @@ This will monitor any changes in log files matching a specified filter, given th - `type` (required): `"File"` - `directory` (required): set to the directory containing the files to be monitored. + > :grey_exclamation:**NOTE:** Only works with absolute paths. + > + > To support *long file name* functionality, we prepend "\\?\" to the path. This approach extends the MAX_PATH limit from 260 characters to 32,767 wide characters. For more details, see [Maximum Path Length Limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation). + > + > Due to this modification, the path **must** be an [absolute path](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#traditional-dos-paths), beginning with a disk designator with a backslash, for example "C:\" or "d:\". + > + > [UNC paths](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths) and [DOS device paths](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths) are not supported. + > + > Ensure you [identify the type of path](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#identify-the-path) and the path is correctly formatted to avoid issues. + > + > | Example | Path Type | Allowed | + > |------------------------------------------------------- |------------------|--------------------| + > | "c:" | Absolute | :white_check_mark: | + > | "c:\\" | Absolute | :white_check_mark: | + > | "c:\temp" | Absolute | :white_check_mark: | + > | "\tempdir" | Absolute | :x: | + > | "C:tempdir" | Relative | :x: | + > | "\\\\.\Volume\{b75e2c83-0000-0000-0000-602f00000000}\Test" | Volume GUID path | :x: | + > | "\\\\.\c:\temp" | DOS Device Path | :x: | + > | "\\\\?\c:\temp" | DOS Device Path | :x: | + > | "\\\\127.0.0.1\c$\temp" | UNC | :x: | + > | "\\\\LOCALHOST\c$\temp" | UNC | :x: | + > | "\\\\.\UNC\LOCALHOST\c$\temp" | UNC | :x: | + > | "." | Relative | :x: | + > | ".\" | Relative | :x: | + > | "..\temp" | Relative | :x: | + - `filter` (optional): uses [MS-DOS wildcard match type](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/indexsrv/ms-dos-and-windows-wildcard-characters) i.e.. `*, ?`. Can be set to empty, which will be default to `"*"`. - `includeSubdirectories` (optional) : `"true|false"`, specify if sub-directories also need to be monitored. Defaults to `false`. - `includeFileNames` (optional): `"true|false"`, specifies whether to include file names in the logline, eg. `sample.log: xxxxx`. Defaults to `false`. @@ -231,10 +258,10 @@ This will monitor any changes in log files matching a specified filter, given th In this case, LogMonitor will wait forever for the folder to be created. - **NOTE:** - - This field is case insensitive - - When "INFINITY" is passed, it must be passed as a string. - - The infinity symbol, ∞, is also allowed as a string or the symbol itself. + > :grey_exclamation:**NOTE** + > - This field is case insensitive + > - When "INFINITY" is passed, it must be passed as a string. + > - The infinity symbol, ∞, is also allowed as a string or the symbol itself.
@@ -254,7 +281,10 @@ This will monitor any changes in log files matching a specified filter, given th WARNING: Failed to parse configuration file. Error retrieving source attributes. Invalid source ``` -### Examples +### Sample FileMonitor *LogMonitorConfig.json* +#### Example 1 + +LogMonitor will monitor log files in the directory "c:\inetpub\logs" along with its subfolders. If the directory does not exist, it will wait for up to 10 seconds for the directory to be created. ```json { @@ -273,7 +303,13 @@ This will monitor any changes in log files matching a specified filter, given th } ``` -**Note:** When the directory is the root directory (e.g. C:\\ ) we can only monitor a file that is in the root directory, not a subfolder. This is due to access issues (even when running LogMonitor as an Admin) for some of the folders in the root directory. Therefore, `includeSubdirectories` must be `false` for the root directory. See example below: +#### Example 2 + +LogMonitor will monitor log files in the root directory, "C:\". + + > When the directory is the root directory (e.g. "C:\\" ) we can only monitor a file that is in the root directory, not a subfolder. This is due to access issues (even when running LogMonitor as an Admin) for some of the folders in the root directory. Therefore, `includeSubdirectories` must be `false` for the root directory. + +See sample valid *LogMonitorConfig.json* below: ```json { @@ -289,7 +325,28 @@ This will monitor any changes in log files matching a specified filter, given th } } ``` + +#### Example 3 + +This example shows an invalid file monitor configuration: + +```json +{ + "LogConfig": { + "sources": [ + { + "type": "File", + "directory": "C:", + "filter": "*.log", + "includeSubdirectories": true + } + ] + } +} +``` + When the root directory is passed and `includeSubdirectories = true`, we get an error: + ``` ERROR: LoggerSettings: Invalid Source File atrribute 'directory' (C:) and 'includeSubdirectories' (true).'includeSubdirectories' attribute cannot be 'true' for the root directory WARNING: Failed to parse configuration file. Error retrieving source attributes. Invalid source diff --git a/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.cpp b/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.cpp index 8738bd1..57bb3f1 100644 --- a/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.cpp +++ b/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.cpp @@ -74,7 +74,8 @@ HANDLE FileMonitorUtilities::GetLogDirHandle( logWriter.TraceError( Utility::FormatString( L"Failed to create timer object. Log directory %ws will not be monitored for log entries. Error=%d", - logDirectory.c_str(), status).c_str()); + logDirectory.c_str(), status) + .c_str()); return INVALID_HANDLE_VALUE; } @@ -90,6 +91,10 @@ HANDLE FileMonitorUtilities::GetLogDirHandle( void FileMonitorUtilities::ParseDirectoryValue(_Inout_ std::wstring &directory) { + // Replace all occurrences of forward slashes (/) with backslashes (\). + std::replace(directory.begin(), directory.end(), L'/', L'\\'); + + // Remove trailing backslashes while (!directory.empty() && directory[directory.size() - 1] == L'\\') { directory.resize(directory.size() - 1); @@ -113,6 +118,33 @@ bool FileMonitorUtilities::CheckIsRootFolder(_In_ std::wstring dirPath) return std::regex_search(dirPath, matches, pattern); } +std::wstring FileMonitorUtilities::_GetParentDir(std::wstring dirPath) +{ + if (CheckIsRootFolder(dirPath)) + { + return dirPath; + } + + size_t pos = dirPath.find_last_of(L"/\\"); + std::wstring parentdir = L""; + if (pos != std::wstring::npos) + { + parentdir = dirPath.substr(0, pos); + } + + // Check if it is an empty string + if (parentdir.empty()) + { + std::wstring pathError = Utility::FormatString( + L"Directory cannot be a relative path or an empty string %s.", + dirPath.c_str()); + std::string strPathError(pathError.begin(), pathError.end()); + throw std::invalid_argument(strPathError); + } + + return parentdir; +} + HANDLE FileMonitorUtilities::_RetryOpenDirectoryWithInterval( std::wstring logDirectory, std::double_t waitInSeconds, @@ -123,11 +155,64 @@ HANDLE FileMonitorUtilities::_RetryOpenDirectoryWithInterval( DWORD status = ERROR_FILE_NOT_FOUND; int elapsedTime = 0; - const int eventsCount = 2; - HANDLE dirOpenEvents[eventsCount] = {stopEvent, timerEvent}; + const int eventsCount = 3; + + HANDLE dirOpenEvents[eventsCount]{}; + dirOpenEvents[0] = stopEvent; + dirOpenEvents[1] = timerEvent; + + // Start monitoring the parent directory for changes + // NOTE: For nested directories, the parent dir must exist. If it does not, this will raise an error + // ERROR_FILE_NOT_FOUND (STATUS: 2) - The system cannot find the file specified + std::wstring parentdir = _GetParentDir(logDirectory); + HANDLE dirChangesHandle = FindFirstChangeNotification( + parentdir.c_str(), // directory to watch + TRUE, // watch the subtree + FILE_NOTIFY_CHANGE_DIR_NAME); // watch dir name changes + dirOpenEvents[2] = dirChangesHandle; + + if (dirChangesHandle == INVALID_HANDLE_VALUE) + { + status = GetLastError(); + if (status == ERROR_FILE_NOT_FOUND) + { + logWriter.TraceError( + Utility::FormatString( + L"The parent directory '%s' does not exist for the specified path: '%s'. Error: %lu", + parentdir.c_str(), + logDirectory.c_str(), + status) + .c_str()); + return INVALID_HANDLE_VALUE; + } + else + { + logWriter.TraceError( + Utility::FormatString( + L"Failed to monitor changes in directory %s. Error: %lu", + logDirectory.c_str(), + status) + .c_str()); + return INVALID_HANDLE_VALUE; + } + } + + if (dirChangesHandle == NULL) + { + logWriter.TraceError( + Utility::FormatString( + L"Failed to monitor changes in directory %s.", + logDirectory.c_str()) + .c_str()); + return INVALID_HANDLE_VALUE; + } + + // Get the starting tick count + ULONGLONG startTick = GetTickCount64(); while (FileMonitorUtilities::_IsFileErrorStatus(status) && elapsedTime < waitInSeconds) { + // Calculate the wait interval int waitInterval = Utility::GetWaitInterval(waitInSeconds, elapsedTime); LARGE_INTEGER timeToWait = Utility::ConvertWaitIntervalToLargeInt(waitInterval); @@ -147,7 +232,7 @@ HANDLE FileMonitorUtilities::_RetryOpenDirectoryWithInterval( DWORD wait = WaitForMultipleObjects(eventsCount, dirOpenEvents, FALSE, INFINITE); switch (wait) { - case WAIT_OBJECT_0: + case WAIT_OBJECT_0: // stopEvent { // // The process is exiting. Stop the timer and return. @@ -157,11 +242,29 @@ HANDLE FileMonitorUtilities::_RetryOpenDirectoryWithInterval( return INVALID_HANDLE_VALUE; } - case WAIT_OBJECT_0 + 1: + case WAIT_OBJECT_0 + 1: // timerEvent { - // // Timer event. Retry opening directory handle. - // + break; + } + + case WAIT_OBJECT_0 + 2: // Directory change notification + { + if (FindNextChangeNotification(dirChangesHandle) == FALSE) + { + status = GetLastError(); + logWriter.TraceError( + Utility::FormatString( + L"Failed to request change notification in directory %s. Error: %lu", + parentdir.c_str(), + status) + .c_str()); + CancelWaitableTimer(timerEvent); + CloseHandle(timerEvent); + return INVALID_HANDLE_VALUE; + } + + elapsedTime = static_cast((GetTickCount64() - startTick) / 1000); break; } @@ -171,10 +274,14 @@ HANDLE FileMonitorUtilities::_RetryOpenDirectoryWithInterval( // Wait failed, return the failure. // status = GetLastError(); + logWriter.TraceError( + Utility::FormatString( + L"Unexpected error when waiting for directory: %lu. Error: %lu.", + wait, status) + .c_str()); CancelWaitableTimer(timerEvent); CloseHandle(timerEvent); - return INVALID_HANDLE_VALUE; } } @@ -204,6 +311,12 @@ HANDLE FileMonitorUtilities::_RetryOpenDirectoryWithInterval( } } + // Close the change notification handle if it was successfully created + if (dirChangesHandle != INVALID_HANDLE_VALUE) + { + FindCloseChangeNotification(dirChangesHandle); + } + return logDirHandle; } diff --git a/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.h b/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.h index bb4cc4f..cec7009 100644 --- a/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.h +++ b/LogMonitor/src/LogMonitor/FileMonitor/FileMonitorUtilities.h @@ -36,4 +36,7 @@ class FileMonitorUtilities final static std::wstring _GetWaitLogMessage( std::wstring logDirectory, std::double_t waitInSeconds); + + static std::wstring _GetParentDir( + std::wstring dirPath); }; diff --git a/LogMonitor/src/LogMonitor/LogFileMonitor.cpp b/LogMonitor/src/LogMonitor/LogFileMonitor.cpp index 5130173..f0658e2 100644 --- a/LogMonitor/src/LogMonitor/LogFileMonitor.cpp +++ b/LogMonitor/src/LogMonitor/LogFileMonitor.cpp @@ -62,7 +62,7 @@ LogFileMonitor::LogFileMonitor(_In_ const std::wstring& LogDirectory, // By default, the name is limited to MAX_PATH characters. To extend this limit to 32,767 wide characters, // we prepend "\?" to the path. Prepending the string "\?" does not allow access to the root directory // We, therefore, do not prepend for the root directory - bool isRootFolder = CheckIsRootFolder(m_logDirectory); + bool isRootFolder = FileMonitorUtilities::CheckIsRootFolder(m_logDirectory); m_logDirectory = isRootFolder ? m_logDirectory : PREFIX_EXTENDED_PATH + m_logDirectory; if (m_filter.empty()) @@ -215,7 +215,8 @@ LogFileMonitor::StartLogFileMonitorStatic( { logWriter.TraceError( Utility::FormatString( - L"Failed to start log file monitor. Log files in a directory %s will not be monitored. Error: %lu", + L"Failed to start log file monitor. Log files in a directory " + "'%s' will not be monitored. Error: %lu", pThis->m_logDirectory.c_str(), status ).c_str() @@ -227,7 +228,8 @@ LogFileMonitor::StartLogFileMonitorStatic( { logWriter.TraceError( Utility::FormatString( - L"Failed to start log file monitor. Log files in a directory %s will not be monitored. %S", + L"Failed to start log file monitor. Log files in a directory " + "'%s' will not be monitored. %S", pThis->m_logDirectory.c_str(), ex.what() ).c_str() @@ -238,7 +240,7 @@ LogFileMonitor::StartLogFileMonitorStatic( { logWriter.TraceError( Utility::FormatString( - L"Failed to start log file monitor. Log files in a directory %s will not be monitored.", + L"Failed to start log file monitor. Log files in a directory '%s' will not be monitored.", pThis->m_logDirectory.c_str() ).c_str() ); @@ -2043,12 +2045,3 @@ LogFileMonitor::GetFileId( return status; } - -bool -LogFileMonitor::CheckIsRootFolder(_In_ std::wstring dirPath) -{ - std::wregex pattern(L"^\\w:?$"); - - std::wsmatch matches; - return std::regex_search(dirPath, matches, pattern); -} diff --git a/LogMonitor/src/LogMonitor/LogFileMonitor.h b/LogMonitor/src/LogMonitor/LogFileMonitor.h index 0d5c6aa..228e651 100644 --- a/LogMonitor/src/LogMonitor/LogFileMonitor.h +++ b/LogMonitor/src/LogMonitor/LogFileMonitor.h @@ -5,18 +5,22 @@ #pragma once +#include +#include + #define REVERSE_BYTE_ORDER_MARK 0xFFFE -#define BYTE_ORDER_MARK 0xFEFF +#define BYTE_ORDER_MARK 0xFEFF -#define BOM_UTF8_HALF 0xBBEF -#define BOM_UTF8_2HALF 0xBF +#define BOM_UTF8_HALF 0xBBEF +#define BOM_UTF8_2HALF 0xBF #define PREFIX_EXTENDED_PATH L"\\\\?\\" /// /// LogMonitor filetype /// -enum LM_FILETYPE { +enum LM_FILETYPE +{ FileTypeUnknown, ANSI, UTF16LE, @@ -43,7 +47,6 @@ enum class EventAction Unknown = 5, }; - struct DirChangeNotificationEvent { std::wstring FileName; @@ -51,18 +54,16 @@ struct DirChangeNotificationEvent UINT64 Timestamp; }; - class LogFileMonitor final { public: LogFileMonitor() = delete; LogFileMonitor( - _In_ const std::wstring& LogDirectory, - _In_ const std::wstring& Filter, + _In_ const std::wstring &LogDirectory, + _In_ const std::wstring &Filter, _In_ bool IncludeSubfolders, - _In_ const std::double_t& WaitInSeconds - ); + _In_ const std::double_t &WaitInSeconds); ~LogFileMonitor(); @@ -112,7 +113,7 @@ class LogFileMonitor final // struct ci_less { - bool operator() (const std::wstring & s1, const std::wstring & s2) const + bool operator()(const std::wstring &s1, const std::wstring &s2) const { return _wcsicmp(s1.c_str(), s2.c_str()) < 0; } @@ -129,11 +130,11 @@ class LogFileMonitor final // struct file_id_less { - bool operator() (const FILE_ID_INFO& id1, const FILE_ID_INFO& id2) const + bool operator()(const FILE_ID_INFO &id1, const FILE_ID_INFO &id2) const { return id1.VolumeSerialNumber < id2.VolumeSerialNumber || - (id1.VolumeSerialNumber == id2.VolumeSerialNumber - && memcmp(id1.FileId.Identifier, id2.FileId.Identifier, sizeof(id1.FileId.Identifier)) < 0); + (id1.VolumeSerialNumber == id2.VolumeSerialNumber && + memcmp(id1.FileId.Identifier, id2.FileId.Identifier, sizeof(id1.FileId.Identifier)) < 0); } }; @@ -148,82 +149,69 @@ class LogFileMonitor final DWORD StartLogFileMonitor(); static DWORD StartLogFileMonitorStatic( - _In_ LPVOID Context - ); + _In_ LPVOID Context); static inline BOOL FileMatchesFilter( _In_ LPCWSTR FileName, - _In_ LPCWSTR SearchPattern - ); + _In_ LPCWSTR SearchPattern); DWORD InitializeMonitoredFilesInfo(); DWORD LogDirectoryChangeNotificationHandler(); static DWORD LogFilesChangeHandlerStatic( - _In_ LPVOID Context - ); + _In_ LPVOID Context); DWORD LogFilesChangeHandler(); DWORD InitializeDirectoryChangeEventsQueue(); static DWORD GetFilesInDirectory( - _In_ const std::wstring& FolderPath, - _In_ const std::wstring& SearchPattern, - _Out_ std::vector>& Files, - _In_ bool ShouldLookInSubfolders - ); + _In_ const std::wstring &FolderPath, + _In_ const std::wstring &SearchPattern, + _Out_ std::vector> &Files, + _In_ bool ShouldLookInSubfolders); - DWORD LogFileAddEventHandler(DirChangeNotificationEvent& Event); + DWORD LogFileAddEventHandler(DirChangeNotificationEvent &Event); - DWORD LogFileRemoveEventHandler(DirChangeNotificationEvent& Event); + DWORD LogFileRemoveEventHandler(DirChangeNotificationEvent &Event); - DWORD LogFileModifyEventHandler(DirChangeNotificationEvent& Event); + DWORD LogFileModifyEventHandler(DirChangeNotificationEvent &Event); - DWORD LogFileRenameNewEventHandler(DirChangeNotificationEvent& Event); + DWORD LogFileRenameNewEventHandler(DirChangeNotificationEvent &Event); void RenameFileInMaps( - _In_ const std::wstring& NewFullName, - _In_ const std::wstring& OldName, - _In_ const FILE_ID_INFO& FileId - ); + _In_ const std::wstring &NewFullName, + _In_ const std::wstring &OldName, + _In_ const FILE_ID_INFO &FileId); - DWORD LogFileReInitEventHandler(DirChangeNotificationEvent& Event); + DWORD LogFileReInitEventHandler(DirChangeNotificationEvent &Event); DWORD ReadLogFile( - _Inout_ std::shared_ptr LogFileInfo - ); + _Inout_ std::shared_ptr LogFileInfo); void WriteToConsole( _In_ std::wstring Message, - _In_ std::wstring FileName - ); + _In_ std::wstring FileName); LM_FILETYPE FileTypeFromBuffer( _In_reads_bytes_(ContentSize) LPBYTE FileContents, _In_ UINT ContentSize, _In_reads_bytes_(BomSize) LPBYTE Bom, _In_ UINT BomSize, - _Out_ UINT& FoundBomSize - ); + _Out_ UINT &FoundBomSize); std::wstring ConvertStringToUTF16( _In_reads_bytes_(StringSize) LPBYTE StringPtr, _In_ UINT StringSize, - _In_ LM_FILETYPE EncodingType - ); + _In_ LM_FILETYPE EncodingType); LogFileInfoMap::iterator GetLogFilesInformationIt( - _In_ const std::wstring& Key, - _Out_opt_ bool* IsShortPath = NULL - ); + _In_ const std::wstring &Key, + _Out_opt_ bool *IsShortPath = NULL); static DWORD GetFileId( - _In_ const std::wstring& FullLongPath, - _Out_ FILE_ID_INFO& FileId, - _In_opt_ HANDLE Handle = INVALID_HANDLE_VALUE - ); - - static bool CheckIsRootFolder(_In_ std::wstring dirPath); + _In_ const std::wstring &FullLongPath, + _Out_ FILE_ID_INFO &FileId, + _In_opt_ HANDLE Handle = INVALID_HANDLE_VALUE); };