/*++ Copyright (c) 1997 Microsoft Corporation Module Name : aspdirmon.cpp Abstract: This module includes derivation of class supporting change notification for ASP template cache, from abstract class DIR_MON_ENTRY Author: Charles Grant ( cgrant ) June-1997 Revision History: --*/ /************************************************************ * Include Headers ************************************************************/ #include "denpre.h" #pragma hdrstop #include "aspdmon.h" #include "ie449.h" #include "memchk.h" #ifndef UNICODE #error "ASPDMON.CPP must be compiled with UNICODE defined" #endif /************************************************************ * Inlined Documentation on change notification * * Change Notification: * This module is to used to monitor the file system for changes * to scripts. We need to know about changes to scripts for two * reasons: * 1) To keep the template cache current * 2) To manage applications lifetimes. If the GLOBAL.ASA * for an application, or a file included in the GLOBAL.ASA * changes, that application should be restarted. * * * Outline of Change Notification System * * To obtain change notification we use the ReadDirectoryChangesW * API as wrapped by the CDirMonitor and CDirMonitorEntry classes. * Three hash tables are used by the change notifcation system: * * CTemplateCacheManager g_TemplateCache * CDirMonitor g_DirMonitor * CFileApplicationMap g_FileAppMap * * When a template is compiled and inserted into the g_TemplateCache * the template is provided with a list of files included in that * template. For each file included in the template, we search the * g_DirMonitor table to see if see if we are already monitoring the * files parent directory for changes. If so we simply addref the * CDirMonitorEntry instance we obtain, and save a pointer to the * monitor entry in an array in the corresponding file map. If the * directory is not being monitored we create a new CDirMonitorEntry' * instance and add it to g_DirMonitor. When we add the monitor entry * to the g_DirMonitor we launch an asynchronous request to ReadDirectoryChangesW * for that directory. * * Managing the template cache and application life times are logically * independent activities. We must monitor GLOBAL.ASA for changes even if * the GLOBAL.ASA template is not currently in the template cache. * So, if the template is a GLOBAL.ASA for an application, additional work * must be done. For each file included in the GLOBAL.ASA we add an entry * to g_FileAppMap relating that file to the applications that depend on it. * We store a back pointer to the file/application mappping in the application * instance, so that the application can remove the mapping when it shuts down. * In the application we store a pointer to the GLOBAL.ASA template. For * each file in the GLOBAL.ASA, We check g_DirMonitor to find the monitor entry * for the parent directory for that file, AddRef the monitor entry we find, and * add it to a list of monitor entries in the application. * * When a change occurs to a directory we are monitoring, the callback function * DirMontiorCompletionFunction will be invoked, and in turn will invoke the * the ActOnNotification method of the monitor entry for that directory. If a file * has changed we use g_FileAppMap to shut down those applications that depend on * that file and flush the file from the template cache. * ************************************************************/ #define MAX_BUFFER_SIZE 8192 PTRACE_LOG CASPDirMonitorEntry::gm_pTraceLog = NULL; CDirMonitor *g_pDirMonitor = NULL; CASPDirMonitorEntry::CASPDirMonitorEntry() : m_cNotificationFailures(0) /*++ Routine Description: Constructor Arguments: None Return Value: None --*/ { } CASPDirMonitorEntry::~CASPDirMonitorEntry() /*++ Routine Description: Destructor Arguments: None Return Value: None --*/ { } /*++ increment refcount for an entry -- writes to reftrace log if it is defined --*/ VOID CASPDirMonitorEntry::AddRef(VOID) { CDirMonitorEntry::AddRef(); IF_DEBUG(FCN) WriteRefTraceLogEx(gm_pTraceLog, m_cDirRefCount, this, PVOID(UIntToPtr(m_cIORefCount)), m_pszPath, 0); } BOOL CASPDirMonitorEntry::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 --*/ { BOOL fAlive = CDirMonitorEntry::Release(); IF_DEBUG(FCN) WriteRefTraceLogEx(gm_pTraceLog, m_cDirRefCount, this, PVOID(UIntToPtr(m_cIORefCount)), m_pszPath, 0); return fAlive; } BOOL CASPDirMonitorEntry::ActOnNotification( DWORD dwStatus, DWORD dwBytesWritten) /*++ Routine Description: Do any work associated with a change notification, i.e. Arguments: None Return Value: TRUE if application should continue to be monitored, otherwise FALSE --*/ { FILE_NOTIFY_INFORMATION *pNotify = NULL; FILE_NOTIFY_INFORMATION *pNextNotify = NULL; WCHAR *pwstrFileName = NULL; // Wide file name pNextNotify = (FILE_NOTIFY_INFORMATION *) m_pbBuffer; // If the status word is not S_OK, 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. The changes to the // individual scripts will flush the template cache, but we may also be watching // the directory for the addition of a GLOBAL.ASA. By calling FileChanged on // global.asa we will force that handle on the directory to close. if (dwStatus == ERROR_ACCESS_DENIED) { FileChanged(SZ_GLOBAL_ASA, false); // No further notificaitons 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(FCN) DBGPRINTF((DBG_CONTEXT, "[CASPDirMonitorEntry] ReadDirectoryChange failed. Status = %d\n", dwStatus)); m_cNotificationFailures++; return TRUE; // Try to get change notification again } else { IF_DEBUG(FCN) DBGPRINTF((DBG_CONTEXT, "[CASPDirMonitorEntry] 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. Flush the whole cache 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(FCN) DBGPRINTF((DBG_CONTEXT, "[CASPDirMonitorEntry] ReadDirectoryChange failed, too many changes for buffer\n")); FlushAll: // Flush the 449 response file cache Do449ChangeNotification(); // Flush everything in the cache as a precaution g_TemplateCache.FlushAll(); // Check all applications to see if they need to be restarted g_ApplnMgr.RestartApplications(); // Flush the script engine cache as a precaution (should be flushed via TemplateCache, but just in case.) g_ScriptManager.FlushAll(); // Try to increase the buffer size so this doesn't happen again // Unfortunately the first call to ReadDirectoryChangesW on this // file handle establishes the buffer size. We must close and re-open // the file handle to change the buffer size if (ResetDirectoryHandle() && (GetBufferSize() < MAX_BUFFER_SIZE)) { SetBufferSize(2 * GetBufferSize()); } return TRUE; } STACK_BUFFER(filename, MAX_PATH * sizeof(WCHAR)); while ( pNextNotify != NULL ) { DWORD cch; pNotify = pNextNotify; pNextNotify = (FILE_NOTIFY_INFORMATION *) ((PCHAR) pNotify + pNotify->NextEntryOffset); // Resize the stack buffer to the size of the filename. I know it's // ugly, but if it fails, jump back up to the flush all logic. // NOTE that the FileNameLength in the NOTIFY structure is in Bytes, not chars if (!filename.Resize(pNotify->FileNameLength+2)) { goto FlushAll; } pwstrFileName = (WCHAR *)filename.QueryPtr(); memcpy(pwstrFileName, pNotify->FileName, pNotify->FileNameLength); cch = pNotify->FileNameLength/sizeof(WCHAR); pwstrFileName[cch] = L'\0'; // Take the appropriate action for the directory change switch (pNotify->Action) { case FILE_ACTION_ADDED: case FILE_ACTION_RENAMED_NEW_NAME: // 'File Added' only matters for GLOBAL.ASA IF_DEBUG(FCN) DBGPRINTF((DBG_CONTEXT, "Change Notification: New file added: %S\n", pwstrFileName)); if (cch != CCH_GLOBAL_ASA || wcsicmp(pwstrFileName, SZ_GLOBAL_ASA) != 0) { break; } case FILE_ACTION_REMOVED: case FILE_ACTION_MODIFIED: case FILE_ACTION_RENAMED_OLD_NAME: IF_DEBUG(FCN) DBGPRINTF((DBG_CONTEXT, "Change Notification: File %s: %S\n", pNotify->Action == FILE_ACTION_MODIFIED? "changed" : "removed", pwstrFileName)); FileChanged(pwstrFileName, pNotify->Action != FILE_ACTION_MODIFIED); break; default: break; } if(pNotify == pNextNotify) { break; } } // We should sign up for further change notification return TRUE; } void CASPDirMonitorEntry::FileChanged(const WCHAR *pszScriptName, bool fFileWasRemoved) /*++ 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; STACK_BUFFER( tempScriptName, MAX_PATH ); STACK_BUFFER( tempScriptPath, MAX_PATH ); bool fRemoveMultiple = false; WCHAR *pT = wcschr(pszScriptName, '~'); if (pT) { if (ConvertToLongFileName(m_pszPath, pszScriptName, &wfd)) { pszScriptName = (WCHAR *) &wfd.cFileName; } else { // It could be a long filename that was deleted, so remove everything in cache past the '~'. if (fFileWasRemoved) { fRemoveMultiple = true; DWORD cchToCopy = (DWORD)(pT - pszScriptName)/sizeof(WCHAR); if (tempScriptName.Resize((cchToCopy+1)*sizeof(WCHAR)) == FALSE) { return; } WCHAR *szScriptNameCopy = (WCHAR *)tempScriptName.QueryPtr(); // copy prefix to delete into local buffer. wcsncpy(szScriptNameCopy, pszScriptName, cchToCopy); szScriptNameCopy[cchToCopy] = '\0'; pszScriptName = szScriptNameCopy; } else return; } } // Allocate enough memory to concatentate the // application path and script name DWORD cch = m_cPathLength + wcslen(pszScriptName); if (tempScriptPath.Resize((cch + 1)*sizeof(WCHAR)) == FALSE) { return; } LPWSTR pszScriptPath = (LPWSTR) tempScriptPath.QueryPtr(); Assert(pszScriptPath != NULL); // Copy the application path into the script path // pT will point to the terminator of the application path pT = strcpyEx(pszScriptPath, m_pszPath); // Now append the script name. Note that the script name is // relative to the directory that we received the notification for wcscpy(pT, pszScriptName); Normalize(pszScriptPath); // It is important that we flush the cache and then shutdown applications // The call to shut down applications is asynch, and could result in the // template being delted while we are in the process of flushing it. // CONSIDER: Is this really indicative of a ref-counting problem? if (fRemoveMultiple) { IF_DEBUG(FCN) DBGPRINTF((DBG_CONTEXT, "ChangeNotification: Flushing \"%S*\" from cache.\n", pszScriptPath)); g_IncFileMap.FlushFiles(pszScriptPath); g_TemplateCache.FlushFiles(pszScriptPath); Do449ChangeNotification(NULL); // not used often, no selective delete } else { g_IncFileMap.Flush(pszScriptPath); g_TemplateCache.Flush(pszScriptPath, MATCH_ALL_INSTANCE_IDS); Do449ChangeNotification( pszScriptPath ); } // g_FileAppMap will shutdown any applications // that depend on this file. g_FileAppMap.ShutdownApplications( pszScriptPath ); } BOOL CASPDirMonitorEntry::FPathMonitored(LPCWSTR pszPath) { if (m_fWatchSubdirectories && (wcsncmp(m_pszPath,pszPath, m_cPathLength) == 0)) { return TRUE; } return FALSE; } BOOL RegisterASPDirMonitorEntry( LPCWSTR pcwszDirectoryName, CASPDirMonitorEntry **ppDME, BOOL fWatchSubDirs /* = FALSE */ ) /*++ Routine Description: Find entry and create a new one and start monitoring if not found. Arguments: pszDirectory - directory to monitor ppDNE - Found (or newly created) entry (optional) Return Value: TRUE if success, otherwise FALSE Remarks: Not compatible with WIN95 --*/ { DWORD cchDirectory; WCHAR *pwszDirectory = (WCHAR *)pcwszDirectoryName; STACK_BUFFER(tempDirectory, 256); cchDirectory = wcslen(pcwszDirectoryName); // The directory monitor code requires, or possibly ASP's use of the directory // monitor, that the directory contain a trailing backslash if( cchDirectory && (pcwszDirectoryName[cchDirectory - 1] != L'\\') ) { // we need to add the backslash. To do this, we'll need to allocate // memory from somewhere to make a copy of the converted string with // the trailing backslash. if (tempDirectory.Resize((cchDirectory + 2) * sizeof(WCHAR)) == FALSE) { return FALSE; } // copy the converted string to the just allocated buffer and add // the trailing backslash and NULL terminator wcscpy((WCHAR *)tempDirectory.QueryPtr(), pcwszDirectoryName); pwszDirectory = (WCHAR *)tempDirectory.QueryPtr(); pwszDirectory[cchDirectory] = L'\\'; cchDirectory++; pwszDirectory[cchDirectory] = '\0'; } // Don't loop forever BOOL fTriedTwice = FALSE; TryAgain: // Check Existing first CASPDirMonitorEntry *pDME = (CASPDirMonitorEntry *)g_pDirMonitor->FindEntry( pwszDirectory ); if ( pDME == NULL ) { // Not found - create new entry pDME = new CASPDirMonitorEntry; if ( pDME ) { pDME->AddRef(); pDME->Init(NULL); // Start monitoring if ( !g_pDirMonitor->Monitor(pDME, pwszDirectory, fWatchSubDirs, FILE_NOTIFY_FILTER) ) { // Cleanup if failed pDME->Release(); pDME = NULL; // // We might still be successful if the monitor failed because // someone slipped it into the hash table before we had // a chance // if ( GetLastError() == ERROR_ALREADY_EXISTS && !fTriedTwice ) { fTriedTwice = TRUE; goto TryAgain; } } } } // Return entry if found if ( pDME != NULL ) { *ppDME = static_cast(pDME); return TRUE; } else { *ppDME = NULL; return FALSE; } } BOOL ConvertToLongFileName( const WCHAR *pszPath, const WCHAR *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 --*/ { STACK_BUFFER( tempName, MAX_PATH*sizeof(WCHAR) ); // Allocate enough memory to concatentate the file path and name DWORD cb = (wcslen(pszPath) + wcslen(pszName)) * sizeof(WCHAR); if (tempName.Resize(cb + sizeof(WCHAR)) == FALSE) { return FALSE; } WCHAR *pszFullName = (WCHAR *) tempName.QueryPtr(); Assert(pszFullName != NULL); // Copy the path into the working string // pT will point to the terminator of the application path WCHAR* pT = strcpyEx(pszFullName, pszPath); // Now append the file name. Note that the script name is // relative to the directory that we received the notification for wcscpy(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; }