#include "FSMon.h" NAMESPACE_UPP #ifdef PLATFORM_WIN32 // Enable/Disable process privileges. BOOL FSMon::EnablePrivilege(LPCTSTR pszPrivName, BOOL fEnable) { // Assume function fails BOOL fOk = FALSE; // Try to open this process's access token HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { // privilege TOKEN_PRIVILEGES tp = { 1 }; if (LookupPrivilegeValue(NULL, pszPrivName, &tp.Privileges[0].Luid)) { tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); fOk = (GetLastError() == ERROR_SUCCESS); } CloseHandle(hToken); } return(fOk); } // check if path is a folder or a file bool FSMon::isFolder(String const &path) { DWORD dwAttrib = GetFileAttributes(path); return static_cast((dwAttrib != 0xffffffff && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY))); } // constructor FSMon::FSMon(bool nOnClose) { // must create completion port on first use completionPort = NULL; // reset completion keys generator // skip 0 descriptor, we never know if 0 is returned... lastDescriptor = 1; // those are needed for ReadDirectoryChangesW EnablePrivilege(SE_BACKUP_NAME, true); EnablePrivilege(SE_RESTORE_NAME, true); EnablePrivilege(SE_CHANGE_NOTIFY_NAME, true); shutDown = false; threadRunning = false; } // destructor FSMon::~FSMon() { // if thread not started, no need to shutdown if(threadRunning) { // cancels all pending IO operations // can't use the CancelIoEx (available from Vista+) // so we first cancel IO for current thread // then we send a packet that causes worker thread to // cancel its part for(int i = 0; i < monitoredInfo.GetCount(); i++) { CancelIo(monitoredInfo[i].hDir); // no way to pass valid parameters with PostQueuedCompletionStatus(), // so we use a 'cancelling' flag inside Info recors INTERLOCKED_(fsmMutex2){ monitoredInfo[i].cancelling = true; } PostQueuedCompletionStatus(completionPort, 0, monitoredDescriptors[i], NULL); while(true) { bool cancelling; INTERLOCKED_(fsmMutex2){ cancelling = monitoredInfo[i].cancelling; } if(!cancelling) break; else Sleep(10); } } // wait some time just to allow thread to cancel all pending I/O Sleep(10); // signal thread shutdown shutDown = true; // send a completion packet to completion port // in order make it return PostQueuedCompletionStatus(completionPort, 0, 0, NULL); while(shutDown) ; threadRunning = false; } // close handles and completion port for(int i = 0; i < monitoredInfo.GetCount(); i++) CloseHandle(monitoredInfo[i].hDir); CloseHandle(completionPort); } // get error string from code String FSMon::GetErrorStr(HRESULT err) { LPTSTR errorText; String res; FormatMessage( // use system message tables to retrieve error text FORMAT_MESSAGE_FROM_SYSTEM | // allocate buffer on local heap for error text FORMAT_MESSAGE_ALLOCATE_BUFFER | // Important! will fail otherwise, since we're not // (and CANNOT) pass insertion parameters FORMAT_MESSAGE_IGNORE_INSERTS, NULL, // unused with FORMAT_MESSAGE_FROM_SYSTEM err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorText, // output 0, // minimum size for output buffer NULL); // arguments - see note if(errorText) { res = errorText; LocalFree(errorText); } return res; } // add a monitored path bool FSMon::Add(String const &path) { bool res = true; for(int iType = 0; iType < 2; iType++) { //open the directory to watch HANDLE hDir = CreateFile( path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE ,//| FILE_SHARE_DELETE, <-- removing FILE_SHARE_DELETE prevents the user or someone else from renaming or deleting the watched directory. This is a good thing to prevent. NULL, //security attributes OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | //<- the required priviliges for this flag are: SE_BACKUP_NAME and SE_RESTORE_NAME. CPrivilegeEnabler takes care of that. FILE_FLAG_OVERLAPPED, // for async mode NULL ); if (hDir == INVALID_HANDLE_VALUE) return false; //create a IO completion port/or associate this key with //the existing IO completion port completionPort = CreateIoCompletionPort( hDir, // if completionPort is NULL, hDir is associated with a NEW completion port, // if completionPort is NON-NULL, hDir is associated with the existing completion // port that the handle completionPort references completionPort, // the completion 'key'... this ptr is returned from GetQueuedCompletionStatus() when one of the events in the dwChangesToWatchFor filter takes place lastDescriptor, 0 ); if(!completionPort) { SetError(GetLastError()); CloseHandle(hDir); return false; } // completion port created, we can now store monitored path and its key // and update key generator // must lock here to avoid working threat to access // removed stuffs INTERLOCKED_(fsmMutex2) { monitoredPaths.Add(path); monitoredDescriptors.Add(lastDescriptor++); monitoredInfo.Add(); } CHANGESINFO &info = monitoredInfo.Top(); info.hDir = hDir; info.cancelling = false; // overlapped MUST be zero filled, otherwise ReadDirectoryChangesW fails memset(&info.overlapped, 0, sizeof(OVERLAPPED)); // an now, if not already done, we must start working thread if(!threadRunning) { shutDown = false; fsmThread.Start(THISBACK(monitorCb)); threadRunning = true; } // thread is started, we shall add the monitored folder with ReadDirectoryChangesW DWORD bufLen; if (!ReadDirectoryChangesW( info.hDir, info.buffer, READ_DIR_CHANGE_BUFFER_SIZE, true, // we want subdirs events iType ? FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY | FILE_NOTIFY_CHANGE_DIR_NAME : FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE, &bufLen, //this var not set when using asynchronous mechanisms... &info.overlapped, NULL)) //no completion routine! { SetError(GetLastError()); return false; } } // just in case of 1 of 2 calls went bad... we must have an EVEN // numbero of elements in arrays if(monitoredPaths.GetCount() & 1) { INTERLOCKED_(fsmMutex2) { monitoredPaths.Drop(); monitoredDescriptors.Drop(); monitoredInfo.Drop(); } } return true; } bool FSMon::Remove(String const &path) { int idx = monitoredPaths.Find(path); if(idx < 0) return false; idx &= 0xFFFFFFFFFFFFFFFE; for(int iType = 0; iType < 2; iType++) { CHANGESINFO &info = monitoredInfo[idx + iType]; // camcels io for corresponding path // as usual, we can't use CancelIoEx because we want it working on XP // so we cancel io in current thread and fake the behaviour // sending a cancel packet to worker thread // no way to pass valid parameters with PostQueuedCompletionStatus(), // so we use a 'cancelling' flag inside Info recors INTERLOCKED_(fsmMutex2) { info.cancelling = true; } PostQueuedCompletionStatus(completionPort, 0, monitoredDescriptors[idx + iType], NULL); CancelIo(info.hDir); // wait for tread get the cancelling packet and reset the flag... while(true) { bool cancelling; INTERLOCKED_(fsmMutex2){ cancelling = info.cancelling; } if(!cancelling) break; else Sleep(10); } // free dir handle CloseHandle(info.hDir); } // must lock here to avoid working thread to access // removed stuffs INTERLOCKED_(fsmMutex2) { monitoredPaths.Remove(idx, 2); monitoredInfo.Remove(idx, 2); monitoredDescriptors.Remove(idx, 2); } return true; } // scans result buffer for FILE_NOTIFY_INFORMATION records // and process them /* typedef struct _FILE_NOTIFY_INFORMATION { DWORD NextEntryOffset; DWORD Action; DWORD FileNameLength; WCHAR FileName[1]; } FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION; To separate the events from originating flags, we need a couple of listeners Flags and corresponding catched events : LISTENER 1 INPUT FLAG CATCHED EVENTS MAPPED TO FILE_NOTIFY_CHANGE_FILE_NAME FILE_ACTION_ADDED FSM_Created FILE_ACTION_REMOVED FSM_Deleted FILE_ACTION_RENAMED_OLD_NAME FSM_Moved FILE_ACTION_RENAMED_NEW_NAME FILE_NOTIFY_CHANGE_DIR_NAME FILE_ACTION_ADDED FSM_FolderCreated FILE_ACTION_REMOVED FSM_FolderDeleted FILE_ACTION_RENAMED_OLD_NAME FSM_Moved FILE_ACTION_RENAMED_NEW_NAME FILE_NOTIFY_CHANGE_SIZE FILE_ACTION_MODIFIED FSM_Modified FILE_NOTIFY_CHANGE_LAST_WRITE FILE_ACTION_MODIFIED FSM_Modified LISTENER 2 FILE_NOTIFY_CHANGE_ATTRIBUTES FILE_ACTION_MODIFIED FSM_AttribChange FILE_NOTIFY_CHANGE_SECURITY FILE_ACTION_MODIFIED FSM_AttribChange FILE_NOTIFY_CHANGE_LAST_ACCESS FILE_ACTION_MODIFIED NOT CATCHED - USELESS FILE_NOTIFY_CHANGE_CREATION FILE_ACTION_MODIFIED NOT CATCHED - USELESS */ void FSMon::ProcessNotify(FILE_NOTIFY_INFORMATION *buf, String const &path, bool second) { do { switch(buf->Action) { case FILE_ACTION_ADDED : INTERLOCKED_(fsmMutex) { Info &info = changed.Add(); info.flags = second ? FSM_FolderCreated : FSM_Created; WString ws = WString(buf->FileName, buf->FileNameLength / sizeof(WCHAR)); info.path = AppendFileName(path, ws.ToString()); } callHandlerCb(); break; case FILE_ACTION_REMOVED : INTERLOCKED_(fsmMutex) { Info &info = changed.Add(); info.flags = second ? FSM_FolderDeleted : FSM_Deleted; WString ws = WString(buf->FileName, buf->FileNameLength / sizeof(WCHAR)); info.path = AppendFileName(path, ws.ToString()); } callHandlerCb(); break; case FILE_ACTION_MODIFIED : { // skip changes on folders... wathever they can mean WString ws = WString(buf->FileName, buf->FileNameLength / sizeof(WCHAR)); String p = AppendFileName(path, ws.ToString()); if(isFolder(p) && !second) break; INTERLOCKED_(fsmMutex) { Info &info = changed.Add(); info.flags = second ? FSM_AttribChange : FSM_Modified; info.path = p; } callHandlerCb(); } break; case FILE_ACTION_RENAMED_OLD_NAME : { WString ws = WString(buf->FileName, buf->FileNameLength / sizeof(WCHAR)); String oldPath = path + ws.ToString(); if(!buf->NextEntryOffset) break; buf = (FILE_NOTIFY_INFORMATION *)((byte *)buf + buf->NextEntryOffset); if(buf->Action != FILE_ACTION_RENAMED_NEW_NAME) { break; } INTERLOCKED_(fsmMutex) { Info &info = changed.Add(); info.flags = second ? FSM_FolderMoved : FSM_Moved; info.path = oldPath; ws = WString(buf->FileName, buf->FileNameLength / sizeof(WCHAR)); info.newPath = AppendFileName(path, ws.ToString()); } callHandlerCb(); break; } case FILE_ACTION_RENAMED_NEW_NAME : break; default : break; } // go to next record buf = (FILE_NOTIFY_INFORMATION *)((byte *)buf + buf->NextEntryOffset); } while(buf->NextEntryOffset); } // monitoring callback (runs in a separate thread) void FSMon::monitorCb(void) { DWORD numBytes; LONG descriptor; LPOVERLAPPED overlapped; // completion port is ok, start monitoring it while(!shutDown) { // Retrieve the directory info for this directory // through the io port's completion key if(!GetQueuedCompletionStatus( completionPort, &numBytes, (ULONG *)&descriptor, &overlapped, INFINITE // remember to send a completion packet to shut down thread !! )) { // error state, if 'overlapped' is NULL, the call timed out // with no event, otherwise we got some error if(!overlapped) { // this would mean timeout, but shouldn't happen // even if called with a null overlapped, it sets up one // so this can't be used for cancel packets // we use numBytes parameter, using value of 1 which should // be never valid } else { // here we land on errors or on CancelIo operations int err = GetLastError(); if(err == ERROR_OPERATION_ABORTED) { // we land here if a CancelIo was called on this handle // just do nothing... we don't have to respawn the I/O INTERLOCKED_(fsmMutex2) { int idx = monitoredDescriptors.Find(descriptor); if(idx >= 0) CancelIo(monitoredInfo[idx].hDir); } } else SetError(err); } } else { // just quick exit if shutting down if(shutDown) break; // get path data from descriptor // we use a second mutex here, it just lock adding/removing paths INTERLOCKED_(fsmMutex2) { int idx = monitoredDescriptors.Find(descriptor); if(idx >= 0) { // check flag if we wanna cancel... CHANGESINFO &info = monitoredInfo[idx]; if(info.cancelling) { CancelIo(info.hDir); info.cancelling = false; } else { String path = monitoredPaths[idx]; // we got an event, just parse it and fill received event structures ProcessNotify((FILE_NOTIFY_INFORMATION *)info.buffer, path, (idx & 1)); // differentiate between the 2 handlers depending on index // EVEN indexes : CREATE/DELETE/RENAME FOR FILES, MODIFY FOR ALL // ODD indexes : CREATE/DELETE/RENAME FOR FOLDERS, ATTRIBUTES FOR ALL // re-post the request, we don't want to loose events here.... DWORD bufLen; if (!ReadDirectoryChangesW( info.hDir, info.buffer, READ_DIR_CHANGE_BUFFER_SIZE, true, // we want subdirs events (idx & 1) ? FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY | FILE_NOTIFY_CHANGE_DIR_NAME : FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE, &bufLen, //this var not set when using asynchronous mechanisms... &info.overlapped, NULL)) //no completion routine! SetError(GetLastError()); } } } } } // reset shutdown flag to signal tread exiting shutDown = false; } // callback to call event handler in maint thread // (via PostCallback) when using GUI void FSMon::callHandlerCb(void) { #ifdef flagGUI if(EventHandler) PostCallback(EventHandler); #else EventHandler(); #endif } // query for changed files/folders Vector FSMon::GetChanged(void) { Vector info; INTERLOCKED_(fsmMutex) { info = changed; changed.Clear(); } return info; } // gets actually opened files Index FSMon::GetOpenedFiles(void) { // no way on windows to catch open/close on files // without a dedicated filter return Index(); } // check error contidion and get error message VectorMap FSMon::GetErrorMap(void) { // just return last error -- on windows we can't have // sequences of errors VectorMap res; res.Add("", (int)errCode); return res; } // query IF some changes happened bool FSMon::HasChanges(void) { bool res; INTERLOCKED_(fsmMutex){ res = changed.GetCount(); } return res; } #endif END_UPP_NAMESPACE