/*++ Copyright (c) 1998 Microsoft Corporation Module Name : dirchang.cxx Abstract: This module contains the directory change manager routines Author: MuraliK Revision History: MCourage 24-Mar-1998 Rewrote to use CDirMonitor --*/ #include "tsunamip.Hxx" #pragma hdrstop #include "dbgutil.h" #include extern "C" { #include } #if ENABLE_DIR_MONITOR #include #include "filecach.hxx" #include "filehash.hxx" //#define DIR_CHANGE_FILTER (FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES) #define DIR_CHANGE_FILTER (FILE_NOTIFY_VALID_MASK & ~FILE_NOTIFY_CHANGE_LAST_ACCESS) #define DIRMON_BUFFER_SIZE 4096 BOOL ConvertToLongFileName( const char *pszPath, const char *pszName, WIN32_FIND_DATA *pwfd); CDirMonitor * g_pVRootDirMonitor; #endif // ENABLE_DIR_MONITOR BOOL DcmInitialize( VOID ) { #if ENABLE_DIR_MONITOR g_pVRootDirMonitor = new CDirMonitor; return (g_pVRootDirMonitor != NULL); #else return TRUE; #endif // ENABLE_DIR_MONITOR } VOID DcmTerminate( VOID ) { #if ENABLE_DIR_MONITOR if (g_pVRootDirMonitor) { g_pVRootDirMonitor->Cleanup(); delete g_pVRootDirMonitor; g_pVRootDirMonitor = NULL; } #endif // ENABLE_DIR_MONITOR } BOOL DcmAddRoot( PVIRTUAL_ROOT_MAPPING pVrm ) { #if ENABLE_DIR_MONITOR IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: Adding root \"%s\" to \"%s\"\n", pVrm->pszRootA, pVrm->pszDirectoryA )); } ASSERT(NULL != g_pVRootDirMonitor); CVRootDirMonitorEntry * pDME; // // See if we're already watching this directory // pDME = (CVRootDirMonitorEntry *) g_pVRootDirMonitor->FindEntry(pVrm->pszDirectoryA); if ( pDME == NULL ) { // Not found - create new entry pDME = new CVRootDirMonitorEntry; if ( pDME ) { pDME->AddRef(); // Start monitoring if ( !g_pVRootDirMonitor->Monitor(pDME, pVrm->pszDirectoryA, TRUE, DIR_CHANGE_FILTER) ) { // Cleanup if failed pDME->Release(); pDME = NULL; } } } // Return entry if found if ( pDME != NULL ) { pVrm->pDME = static_cast(pDME); return TRUE; } else { pVrm->pDME = NULL; return FALSE; } #else // !ENABLE_DIR_MONITOR // // Doesn't do anything. Ha! // return TRUE; #endif // ENABLE_DIR_MONITOR } VOID DcmRemoveRoot( PVIRTUAL_ROOT_MAPPING pVrm ) { #if ENABLE_DIR_MONITOR IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: Removing root \"%s\" to \"%s\"\n", pVrm->pszRootA, pVrm->pszDirectoryA )); } CVRootDirMonitorEntry * pDME = pVrm->pDME; pVrm->pDME = NULL; if (pDME) { pDME->Release(); } #else // !ENABLE_DIR_MONITOR // // Doesn't do anything. Ha! // #endif // ENABLE_DIR_MONITOR } #if ENABLE_DIR_MONITOR typedef struct _FLUSH_PREFIX_PARAM { PCSTR pszPrefix; DWORD cbPrefix; } FLUSH_PREFIX_PARAM; BOOL FlushFilterPrefix( TS_OPEN_FILE_INFO * pOpenFile, PVOID pv ) { DBG_ASSERT( pOpenFile ); DBG_ASSERT( pOpenFile->GetKey() ); FLUSH_PREFIX_PARAM * fpp = (FLUSH_PREFIX_PARAM *)pv; const CFileKey * pfk = pOpenFile->GetKey(); // // If the prefix matches then we flush. // // // The key stored in TS_OPEN_FILE_INFO is uppercased, so we will do a // case insensitive memcmp here. // The alternative is to create a temporary and uppercase all instances // when the directory is dumped or to have CDirMonitorEntry store its // name uppercased. // return ((pfk->m_cbFileName >= fpp->cbPrefix) && (_memicmp(pfk->m_pszFileName, fpp->pszPrefix, fpp->cbPrefix) == 0)); } /*=================================================================== strcpyEx Copy one string to another, returning a pointer to the NUL character in the destination Parameters: szDest - pointer to the destination string szSrc - pointer to the source string Returns: A pointer to the NUL terminator is returned. ===================================================================*/ char *strcpyEx(char *szDest, const char *szSrc) { while (*szDest++ = *szSrc++) ; return szDest - 1; } CVRootDirMonitorEntry::CVRootDirMonitorEntry() : m_cNotificationFailures(0) /*++ Routine Description: Constructor Arguments: None Return Value: None --*/ { } CVRootDirMonitorEntry::~CVRootDirMonitorEntry() /*++ Routine Description: Destructor Arguments: None Return Value: None --*/ { } BOOL CVRootDirMonitorEntry::Init( VOID ) /*++ Routine Description: Initialize monitor entry Arguments: pvData - passed to base Init member Return Value: TRUE if success, otherwise FALSE --*/ { return CDirMonitorEntry::Init(DIRMON_BUFFER_SIZE); } #if 0 BOOL CVRootDirMonitorEntry::Release(VOID) /*++ Routine Description: Decrement refcount to an entry, we override the base class because otherwise Denali's memory manager can't track when we free the object and reports it as a memory leak Arguments: None Return Value: TRUE if object still alive, FALSE if was last release and object destroyed --*/ { IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF((DBG_CONTEXT, "[CVRootDirMonitorEntry] Before Release Ref count %d on directory %s\n", m_cDirRefCount, m_pszPath)); } if ( !InterlockedDecrement( &m_cDirRefCount ) ) { BOOL fDeleteNeeded = Cleanup(); if (fDeleteNeeded) { delete this; } return FALSE; } return TRUE; } #endif BOOL CVRootDirMonitorEntry::ActOnNotification( DWORD dwStatus, DWORD dwBytesWritten) /*++ Routine Description: Do any work associated with a change notification, i.e. Arguments: None Return Value: TRUE if directory should continue to be monitored, otherwise FALSE --*/ { FILE_NOTIFY_INFORMATION *pNotify = NULL; FILE_NOTIFY_INFORMATION *pNextNotify = NULL; LPSTR pszScriptName = NULL; // Name of script WCHAR *pwstrFileName = NULL; // Wide file name DWORD cch = 0; pNextNotify = (FILE_NOTIFY_INFORMATION *) m_pbBuffer; // If the status word is not NOERROR, then the ReadDirectoryChangesW failed if (dwStatus) { // If the status is ERROR_ACCESS_DENIED the directory may be deleted // or secured so we want to stop watching it for changes. // we should flush the dir and everything in it. if (dwStatus == ERROR_ACCESS_DENIED) { // // Flush the dir here // IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: Flushing directory \"%s\" because we got ACCESS_DENIED\n", m_pszPath )); } FLUSH_PREFIX_PARAM param; param.pszPrefix = m_pszPath; param.cbPrefix = strlen(m_pszPath); FilteredFlushFileCache(FlushFilterPrefix, ¶m); // // no point in having the handle open anymore // m_hDir = INVALID_HANDLE_VALUE; AtqCloseFileHandle( m_pAtqCtxt ); // No further notifications desired // so return false return FALSE; } // If we return TRUE, we'll try change notification again // If we return FALSE, we give up on any further change notifcation // We'll try a MAX_NOTIFICATION_FAILURES times and give up. if (m_cNotificationFailures < MAX_NOTIFICATION_FAILURES) { IF_DEBUG ( DIRECTORY_CHANGE ) { DBGPRINTF((DBG_CONTEXT, "[CVRootDirMonitorEntry] ReadDirectoryChange failed. Status = %d\n", dwStatus)); } m_cNotificationFailures++; return TRUE; // Try to get change notification again } else { // CONSIDER: Should we log this? DBGPRINTF((DBG_CONTEXT, "[CVRootDirMonitorEntry] ReadDirectoryChange failed too many times. Giving up.\n")); return FALSE; // Give up trying to get change notification } } else { // Reset the number of notification failure m_cNotificationFailures = 0; } // If dwBytesWritten is 0, then there were more changes then could be // recorded in the buffer we provided. Expire the application just in case // CONSIDER: is this the best course of action, or should iterate through the // cache and test which files are expired if (dwBytesWritten == 0) { IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF((DBG_CONTEXT, "[CVRootDirMonitorEntry] ReadDirectoryChange failed, too many changes for buffer\n")); } // Flush everything in the dir as a precaution FLUSH_PREFIX_PARAM param; param.pszPrefix = m_pszPath; param.cbPrefix = strlen(m_pszPath); FilteredFlushFileCache(FlushFilterPrefix, ¶m); return TRUE; } while ( pNextNotify != NULL ) { BOOL bDoFlush = TRUE; pNotify = pNextNotify; pNextNotify = (FILE_NOTIFY_INFORMATION *) ((PCHAR) pNotify + pNotify->NextEntryOffset); // Get the unicode file name from the notification struct // pNotify->FileNameLength returns the wstr's length in **bytes** not wchars cch = pNotify->FileNameLength / 2; // Convert to ANSI with uniform case and directory delimiters pszScriptName = (LPSTR) _alloca(pNotify->FileNameLength + 1); DBG_ASSERT(pszScriptName != NULL); pszScriptName[ 0 ] = '\0'; cch = WideCharToMultiByte(CP_ACP, 0, pNotify->FileName, cch, pszScriptName, pNotify->FileNameLength + 1, NULL, NULL); pszScriptName[cch] = '\0'; // Take the appropriate action for the directory change switch (pNotify->Action) { case FILE_ACTION_MODIFIED: // // Since this change won't change the pathname of // any files, we don't have to do a flush. // bDoFlush = FALSE; case FILE_ACTION_REMOVED: case FILE_ACTION_RENAMED_OLD_NAME: FileChanged(pszScriptName, bDoFlush); break; case FILE_ACTION_ADDED: case FILE_ACTION_RENAMED_NEW_NAME: default: break; } if(pNotify == pNextNotify) { break; } } // We should sign up for further change notification return TRUE; } void CVRootDirMonitorEntry::FileChanged(const char *pszScriptName, BOOL bDoFlush) /*++ Routine Description: An existing file has been modified or deleted Flush scripts from cache or mark application as expired Arguments: pszScriptName Name of file that changed Return Value: None Fail silently --*/ { // The file name is set by the application that // modified the file, so old applications like EDIT // may hand us a munged 8.3 file name which we should // convert to a long name. All munged 8.3 file names contain '~' // We assume the path does not contain any munged names. WIN32_FIND_DATA wfd; CHAR achFullScriptName[ MAX_PATH + 1 ]; IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: Notification on \"%s\\%s\"\n", m_pszPath, pszScriptName )); } if (strchr(pszScriptName, '~')) { if (ConvertToLongFileName(m_pszPath, pszScriptName, &wfd)) { CHAR * pszEnd; // // The filename in wfd.cFileName is the path-less. We need to // append it to another other sub dirs in pszScriptName (if any) // pszEnd = strrchr( pszScriptName, '\\' ); if ( pszEnd ) { memcpy( achFullScriptName, pszScriptName, DIFF( pszEnd - pszScriptName ) + 1 ); memcpy( achFullScriptName + ( pszEnd - pszScriptName ) + 1, wfd.cFileName, strlen( wfd.cFileName ) + 1 ); pszScriptName = achFullScriptName; } else { pszScriptName = wfd.cFileName; } IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: Converted name to \"%s\"\n", pszScriptName )); } } else { // Fail silently return; } } // Allocate enough memory to concatentate the // application path and script name DWORD cch = m_cPathLength + strlen(pszScriptName) + 1; LPSTR pszScriptPath = (LPSTR) _alloca(cch + 1); // CONSIDER using malloc DBG_ASSERT(pszScriptPath != NULL); // Copy the application path into the script path // pT will point to the terminator of the application path char* pT = strcpyEx(pszScriptPath, m_pszPath); // append a backslash *pT++ = '\\'; // Now append the script name. Note that the script name is // relative to the directory that we received the notification for lstrcpy(pT, pszScriptName); _mbsupr((PUCHAR)pszScriptPath); // Get rid of this file, or dir tree TS_OPEN_FILE_INFO * pOpenFile; if (bDoFlush) { // // This path is a directory that got removed or renamed // so we have to flush everything below it. // IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: Flushing directory \"%s\"\n", pszScriptPath )); } FLUSH_PREFIX_PARAM param; param.pszPrefix = pszScriptPath; param.cbPrefix = strlen(pszScriptPath); FilteredFlushFileCache(FlushFilterPrefix, ¶m); } else if (CheckoutFile(pszScriptPath, 0, &pOpenFile)) { // // this is just one file, or a directory whose // name didn't change. We only have to decache it. // IF_DEBUG( DIRECTORY_CHANGE ) { DBGPRINTF(( DBG_CONTEXT, "DCM: decaching file \"%s\"\n", pszScriptPath )); } DecacheFile(pOpenFile, 0); } } BOOL ConvertToLongFileName( const char *pszPath, const char *pszName, WIN32_FIND_DATA *pwfd) /*++ Routine Description: Finds the long filename corresponding to a munged 8.3 filename. Arguments: pszPath The path to the file pszName The 8.3 munged version of the file name pwfd Find data structure used to contain the long version of the file name. Return Value: TRUE if the file is found, FALSE otherwise --*/ { // Allocate enough memory to concatentate the file path and name DWORD cch = strlen(pszPath) + strlen(pszName) + 1; char *pszFullName = (char *) _alloca(cch + 1); DBG_ASSERT(pszFullName != NULL); // Copy the path into the working string // pT will point to the terminator of the application path char* pT = strcpyEx(pszFullName, pszPath); // append a backslash *pT++ = '\\'; // Now append the file name. Note that the script name is // relative to the directory that we received the notification for lstrcpy(pT, pszName); // FindFirstFile will find using the short name // We can then find the long name from the WIN32_FIND_DATA HANDLE hFindFile = FindFirstFile(pszFullName, pwfd); if (hFindFile == INVALID_HANDLE_VALUE) { return FALSE; } // Now that we have the find data we don't need the handle FindClose(hFindFile); return TRUE; } #endif // ENABLE_DIR_MONITOR /******************************************************************* NAME: IsCharTermA (DBCS enabled) SYNOPSIS: check the character in string is terminator or not. terminator is '/', '\0' or '\\' ENTRY: lpszString - string cch - offset for char to check RETURNS: BOOL - TRUE if it is a terminator HISTORY: v-ChiKos 15-May-1997 Created. ********************************************************************/ BOOL IsCharTermA( IN LPCSTR lpszString, IN INT cch ) { CHAR achLast; achLast = *(lpszString + cch); if ( achLast == '/' || achLast == '\0' ) { return TRUE; } achLast = *CharPrev(lpszString, lpszString + cch + 1); return (achLast == '\\'); }