윈도우즈에서는 ReadDirectoryChangesW 함수를 이용해 간단히 처리할 수 있다.
ReadDirectoryChangesW 함수는 동기(synchronous) 방식으로도 동작하고, 비동기 방식(asynchronous)으로도 동작하는데, “파일 시스템 모니터링”이라는 특성상 비동기 방식으로 사용하는 게 좋다. 동기 방식으로 처리하면, 파일 시스템에 변화가 생길 때까지 함수가 반환이 되지 않기 때문에, 따로 스레드를 만들지 않으면, UI가 먹통이 되버린다. 아래의 샘플은 AlertableIo 방식을 이용한 비동기 방식 샘플이다. 컴파일은 알아서, 잘~
//////////////////////////////////////////////////////////////////////////////// /// \file FileSystemWatcher.h /// \author excel96 /// \date 2006.10.26 //////////////////////////////////////////////////////////////////////////////// #pragma once //////////////////////////////////////////////////////////////////////////////// /// \class cFileSystemEvent /// \brief 변경된 파일 시스템에 대한 정보. /// /// 변경된 파일은 일반 파일일 수도 있고, 디렉토리일 수도 있다. 이름만으로는 /// 변경된 것이 파일인지 디렉토리인지 구분할 수 없으므로, GetFileAttributes /// 함수를 이용해서 구분하도록 한다. //////////////////////////////////////////////////////////////////////////////// class cFileSystemEvent { public: enum ChangeType { CREATED, ///< 파일 생성 CHANGED, ///< 파일 내용 변경 DELETED, ///< 파일 삭제 RENAMED, ///< 파일 이름 변경 MAX, ///< 배열을 위한 맥스값 }; private: ChangeType m_ChangeType; ///< 변경의 종류 std::string m_Directory; ///< 디렉토리 std::string m_FileName; ///< 원래 파일 이름 std::string m_NewFileName; ///< 변경된 파일 이름 public: /// \brief 생성자 cFileSystemEvent(ChangeType type, const std::string& directory, const std::string& fileName); /// \brief 생성자 cFileSystemEvent(const std::string& directory, const std::string& oldFileName, const std::string& newFileName); /// \brief 소멸자 ~cFileSystemEvent(); public: /// \brief 디렉토리까지 포함한 파일 경로를 반환한다. std::string GetFullPath() const; /// \brief 디렉토리까지 포함한 이전 파일 경로를 반환한다. std::string GetOldFullPath() const; /// \brief 디렉토리까지 포함한 새로운 파일 경로를 반환한다. std::string GetNewFullPath() const; public: /// \name Simple getter/setter /// \{ ChangeType GetChangeType() const { return m_ChangeType; } const std::string& GetDirectory() const { return m_Directory; } const std::string& GetFileName() const { return m_FileName; } const std::string& GetOldFileName() const { return m_FileName; } const std::string& GetNewFileName() const { return m_NewFileName; } /// \} }; //////////////////////////////////////////////////////////////////////////////// /// \class cFileSystemWatcher /// \brief 파일 시스템 변경 모니터링을 위한 클래스. /// /// 클래스 인스턴스를 생성하고, Open 함수를 통해 모니터링할 디렉토리를 지정한 /// 다음, 메인 루프 어딘가에서 Heartbeat() 함수를 지속적으로 호출해줘야 한다. /// /// <pre> /// /// cFileSystemWatcher watcher("C:\\Program Files"); /// watcher.SetEventHandler(cFileSystemEvent::CREATED, MyCreateHandler); /// watcher.SetEventHandler(cFileSystemEvent::CHANGED, MyChangeHandler); /// watcher.SetEventHandler(cFileSystemEvent::DELETED, MyDeleteHandler); /// watcher.SetEventHandler(cFileSystemEvent::RENAMED, MyRenameHandler); /// ... /// while (true) /// { /// ... /// watcher.Heartbeat(); /// } /// </pre> //////////////////////////////////////////////////////////////////////////////// class cFileSystemWatcher { public: typedef void (*PFN_EVENT_HANDLER)(const cFileSystemEvent& args); private: struct MONITOR; std::string m_Path; ///< 디렉토리 경로 문자열 HANDLE m_hDirectory; ///< 디렉토리 핸들 MONITOR* m_Monitor; ///< OVERLAPPED 구조체 PFN_EVENT_HANDLER m_EventHandler[cFileSystemEvent::MAX]; ///< 이벤트 핸들러 public: /// \brief 생성자 cFileSystemWatcher(); /// \brief 생성자 + Open cFileSystemWatcher(const std::string& directory); /// \brief 소멸자 virtual ~cFileSystemWatcher(); public: /// \brief 모니터링을 시작한다. bool Open(const std::string& directory); /// \brief 모니터링을 중단한다. void Close(); /// \brief 객체의 상태가 정상적인지의 여부를 반환한다. bool IsGood() const; /// \brief 폴링 함수 void Heartbeat(DWORD period = 1); /// \brief 이벤트 핸들러를 설정한다. void SetEventHandler( cFileSystemEvent::ChangeType type, PFN_EVENT_HANDLER handler); /// \brief 모니터링 중인 디렉토리를 반환한다. const std::string& GetPath() const { return m_Path; } private: /// \brief 변경 사항 요청을 포스트한다. bool PostChangeNotificationRequest(int operation); /// \brief 파일 생성시 외부에서 호출하기 위한 함수 void OnCreate(const std::string& fileName); /// \brief 파일 변경시 외부에서 호출하기 위한 함수 void OnChange(const std::string& fileName); /// \brief 파일 삭제시 외부에서 호출하기 위한 함수 void OnDelete(const std::string& fileName); /// \brief 파일 이름 변경시 외부에서 호출하기 위한 함수 void OnRename(const std::string& oldFileName, const std::string& newFileName); /// \brief 완료 통지 함수 static void CALLBACK FileSystemWatcherCallback(DWORD, DWORD, LPOVERLAPPED); };
//////////////////////////////////////////////////////////////////////////////// /// \file FileSystemWatcher.cpp /// \author excel96 /// \date 2006.10.26 //////////////////////////////////////////////////////////////////////////////// #include "PCH.h" #include "FileSystemWatcher.h" namespace { /* const DWORD FILTER = FILE_NOTIFY_CHANGE_SECURITY | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_ACCESS | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME; */ /// 작업의 종류 enum Operation { CONTINUE_MONITORING, ///< 모니터링을 계속 한다. STOP_MONITORING, ///< 모니터링을 중단한다. }; /// 모니터링할 이벤트의 종류 const DWORD FILTER = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME; /// MONITOR 버퍼의 크기 const size_t BUFFER_SIZE = sizeof(FILE_NOTIFY_INFORMATION) * 1024; /// FILE_NOTIFY_INFORMATION 구조체 안에 와이드 문자열을 std::string 형태로 /// 변환해서 반환한다. std::string ExtractFileName(const FILE_NOTIFY_INFORMATION* info) { char fileName[1024] = {0, }; int count = WideCharToMultiByte( CP_ACP, 0, info->FileName, info->FileNameLength / sizeof(WCHAR), fileName, _ARRAYSIZE(fileName) - 1, NULL, NULL); fileName[count] = '\0'; return std::string(fileName); } /// 경로 끝에다 백슬래쉬를 추가한 경로를 반환한다. std::string AddBackslash(const std::string& path) { if (path.empty()) return std::string(""); return path[path.size()-1] != '\\' ? path + std::string("\\") : path; } /// 디렉토리 + 파일 이름을 합쳐서 전체 경로를 생성한다. std::string Combine(const std::string& dir, const std::string& file) { return AddBackslash(dir) + file; } /// 디렉토리가 존재하는지의 여부를 반환한다. bool IsDirectoryExist(const std::string& path) { DWORD attr = GetFileAttributes(path.c_str()); return (attr != INVALID_FILE_ATTRIBUTES) && (attr & FILE_ATTRIBUTE_DIRECTORY); } } //////////////////////////////////////////////////////////////////////////////// /// \brief 생성자 /// \param type 변경의 종류 /// \param directory 디렉토리 /// \param fileName 파일 이름 //////////////////////////////////////////////////////////////////////////////// cFileSystemEvent::cFileSystemEvent(ChangeType type, const std::string& directory, const std::string& fileName) : m_ChangeType(type), m_Directory(directory), m_FileName(fileName) { } //////////////////////////////////////////////////////////////////////////////// /// \brief 생성자 /// \param directory 디렉토리 /// \param oldFileName 원래 이름 /// \param newFileName 변경된 이름 //////////////////////////////////////////////////////////////////////////////// cFileSystemEvent::cFileSystemEvent(const std::string& directory, const std::string& oldFileName, const std::string& newFileName) : m_ChangeType(RENAMED), m_Directory(directory), m_FileName(oldFileName), m_NewFileName(newFileName) { } //////////////////////////////////////////////////////////////////////////////// /// \brief 소멸자 //////////////////////////////////////////////////////////////////////////////// cFileSystemEvent::~cFileSystemEvent() { } //////////////////////////////////////////////////////////////////////////////// /// \brief 디렉토리까지 포함한 파일 경로를 반환한다. /// \return std::string 경로 문자열 //////////////////////////////////////////////////////////////////////////////// std::string cFileSystemEvent::GetFullPath() const { return Combine(m_Directory, m_FileName); } //////////////////////////////////////////////////////////////////////////////// /// \brief 디렉토리까지 포함한 이전 파일 경로를 반환한다. /// \return std::string 경로 문자열 //////////////////////////////////////////////////////////////////////////////// std::string cFileSystemEvent::GetOldFullPath() const { return Combine(m_Directory, m_FileName); } //////////////////////////////////////////////////////////////////////////////// /// \brief 디렉토리까지 포함한 새로운 파일 경로를 반환한다. /// \return std::string 경로 문자열 //////////////////////////////////////////////////////////////////////////////// std::string cFileSystemEvent::GetNewFullPath() const { return Combine(m_Directory, m_NewFileName); } //////////////////////////////////////////////////////////////////////////////// /// \struct cFileSystemWatcher::MONITOR /// \brief 비동기 함수 호출을 위한 OVERLAPPED 구조체 //////////////////////////////////////////////////////////////////////////////// struct cFileSystemWatcher::MONITOR : public OVERLAPPED { BYTE Buffer[BUFFER_SIZE]; ///< 버퍼 int Operation; ///< 작업의 종류 cFileSystemWatcher* Watcher; ///< 왓처 객체 }; //////////////////////////////////////////////////////////////////////////////// /// \brief 생성자 //////////////////////////////////////////////////////////////////////////////// cFileSystemWatcher::cFileSystemWatcher() : m_hDirectory(INVALID_HANDLE_VALUE), m_Monitor(NULL) { ZeroMemory(m_EventHandler, sizeof(PFN_EVENT_HANDLER) * cFileSystemEvent::MAX); } //////////////////////////////////////////////////////////////////////////////// /// \brief 생성자 + Open /// \param directory 모니터링할 디렉토리 //////////////////////////////////////////////////////////////////////////////// cFileSystemWatcher::cFileSystemWatcher(const std::string& directory) : m_hDirectory(INVALID_HANDLE_VALUE), m_Monitor(NULL) { ZeroMemory(m_EventHandler, sizeof(PFN_EVENT_HANDLER) * cFileSystemEvent::MAX); if (!Open(directory)) Close(); } //////////////////////////////////////////////////////////////////////////////// /// \brief 소멸자 //////////////////////////////////////////////////////////////////////////////// cFileSystemWatcher::~cFileSystemWatcher() { Close(); } //////////////////////////////////////////////////////////////////////////////// /// \brief 모니터링을 시작한다. /// \param directory 모니터링할 디렉토리 /// \return bool 무사히 모니터링을 시작했다면 true, 뭔가 에러가 생겼다면 false //////////////////////////////////////////////////////////////////////////////// bool cFileSystemWatcher::Open(const std::string& directory) { Close(); bool success = false; do { m_Path = AddBackslash(directory); if (!IsDirectoryExist(m_Path)) break; m_hDirectory = CreateFile(m_Path.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL ); if (m_hDirectory == INVALID_HANDLE_VALUE) break; m_Monitor = reinterpret_cast<MONITOR*>(HeapAlloc(GetProcessHeap(), 0, sizeof(MONITOR))); if (!m_Monitor) break; ZeroMemory(m_Monitor, sizeof(MONITOR)); m_Monitor->Watcher = this; if (!PostChangeNotificationRequest(CONTINUE_MONITORING)) break; success = true; } while(0); if (!success) Close(); return success; } //////////////////////////////////////////////////////////////////////////////// /// \brief 모니터링을 중지한다. //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::Close() { m_Path = ""; if (m_hDirectory != INVALID_HANDLE_VALUE) { CloseHandle(m_hDirectory); m_hDirectory = INVALID_HANDLE_VALUE; } if (m_Monitor) { HeapFree(GetProcessHeap(), 0, m_Monitor); m_Monitor = NULL; } } //////////////////////////////////////////////////////////////////////////////// /// \brief 객체의 상태가 정상적인지의 여부를 반환한다. /// \return bool 정상적이라면 true //////////////////////////////////////////////////////////////////////////////// bool cFileSystemWatcher::IsGood() const { return m_hDirectory != INVALID_HANDLE_VALUE && m_Monitor != NULL; } //////////////////////////////////////////////////////////////////////////////// /// \brief 폴링 함수 /// \param period 비동기 함수 호출 결과를 처리할 시간 //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::Heartbeat(DWORD period) { if (IsGood()) { SleepEx(period, TRUE); // make thread alertable } } //////////////////////////////////////////////////////////////////////////////// /// \brief 이벤트 핸들러를 설정한다. /// \param type 이벤트의 종류 /// \param handler 핸들러 //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::SetEventHandler( cFileSystemEvent::ChangeType type, cFileSystemWatcher::PFN_EVENT_HANDLER handler) { if (0 <= type && type < cFileSystemEvent::MAX) m_EventHandler[type] = handler; } //////////////////////////////////////////////////////////////////////////////// /// \brief 변경 사항 요청을 포스트한다. /// \param operation 작업의 종류 /// \return bool 무사히 실행했다면 true 뭔가 에러가 생겼다면 false //////////////////////////////////////////////////////////////////////////////// bool cFileSystemWatcher::PostChangeNotificationRequest(int operation) { if (!IsGood()) return false; m_Monitor->Operation = operation; DWORD bytesReturned = 0; DWORD bufferSize = static_cast<DWORD>(BUFFER_SIZE); if (ReadDirectoryChangesW(m_hDirectory, m_Monitor->Buffer, bufferSize, TRUE, FILTER, &bytesReturned, m_Monitor, FileSystemWatcherCallback)) { return true; } else { return false; } } //////////////////////////////////////////////////////////////////////////////// /// \brief 파일 생성시 외부에서 호출하기 위한 함수 /// \param fileName 생성된 파일 이름 //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::OnCreate(const std::string& fileName) { if (m_EventHandler[cFileSystemEvent::CREATED]) { cFileSystemEvent evt(cFileSystemEvent::CREATED, m_Path, fileName); m_EventHandler[cFileSystemEvent::CREATED](evt); } } //////////////////////////////////////////////////////////////////////////////// /// \brief 파일 변경시 외부에서 호출하기 위한 함수 /// \param fileName 변경된 파일 이름 //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::OnChange(const std::string& fileName) { if (m_EventHandler[cFileSystemEvent::CHANGED]) { cFileSystemEvent evt(cFileSystemEvent::CHANGED, m_Path, fileName); m_EventHandler[cFileSystemEvent::CHANGED](evt); } } //////////////////////////////////////////////////////////////////////////////// /// \brief 파일 삭제시 외부에서 호출하기 위한 함수 /// \param fileName 삭제된 파일 이름 //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::OnDelete(const std::string& fileName) { if (m_EventHandler[cFileSystemEvent::DELETED]) { cFileSystemEvent evt(cFileSystemEvent::DELETED, m_Path, fileName); m_EventHandler[cFileSystemEvent::DELETED](evt); } } //////////////////////////////////////////////////////////////////////////////// /// \brief 파일 이름 변경시 외부에서 호출하기 위한 함수 /// \param oldFileName 원래 파일 이름 /// \param newFileName 변경된 파일 이름 //////////////////////////////////////////////////////////////////////////////// void cFileSystemWatcher::OnRename(const std::string& oldFileName, const std::string& newFileName) { if (m_EventHandler[cFileSystemEvent::RENAMED]) { cFileSystemEvent evt(m_Path, oldFileName, newFileName); m_EventHandler[cFileSystemEvent::RENAMED](evt); } } //////////////////////////////////////////////////////////////////////////////// /// \brief 완료 통지 함수 /// \param errorCode 에러 코드 /// \param bytesTransferred 실제로 처리한 양 /// \param overlapped ReadDirectoryChangesW 함수에다 넘긴 오버랩드 구조체 //////////////////////////////////////////////////////////////////////////////// void CALLBACK cFileSystemWatcher::FileSystemWatcherCallback( DWORD errorCode, DWORD bytesTransferred, LPOVERLAPPED overlapped) { cFileSystemWatcher::MONITOR* monitor = reinterpret_cast<cFileSystemWatcher::MONITOR*>(overlapped); cFileSystemWatcher* watcher = monitor->Watcher; if (errorCode == ERROR_SUCCESS) { PFILE_NOTIFY_INFORMATION info = NULL; size_t offset = 0; std::string fileName, oldFileName, newFileName; do { info = reinterpret_cast<PFILE_NOTIFY_INFORMATION>(&monitor->Buffer[offset]); offset += info->NextEntryOffset; if (info->Action == FILE_ACTION_ADDED) { fileName = ExtractFileName(info); watcher->OnCreate(fileName); } else if (info->Action == FILE_ACTION_REMOVED) { fileName = ExtractFileName(info); watcher->OnDelete(fileName); } else if (info->Action == FILE_ACTION_MODIFIED) { fileName = ExtractFileName(info); watcher->OnChange(fileName); } else if (info->Action == FILE_ACTION_RENAMED_OLD_NAME) { oldFileName = ExtractFileName(info); } else if (info->Action == FILE_ACTION_RENAMED_NEW_NAME) { newFileName = ExtractFileName(info); watcher->OnRename(oldFileName, newFileName); } } while (info->NextEntryOffset != 0); } if (monitor->Operation == CONTINUE_MONITORING) watcher->PostChangeNotificationRequest(CONTINUE_MONITORING); }
void OnFileCreate(const cFileSystemEvent& evt) { std::cout << evt.GetFullPath() << " created" << std::endl; } void OnFileChange(const cFileSystemEvent& evt) { std::cout << evt.GetFullPath() << " changed" << std::endl; } void OnFileDelete(const cFileSystemEvent& evt) { std::cout << evt.GetFullPath() << " deleted" << std::endl; } void OnFileRename(const cFileSystemEvent& evt) { std::cout << evt.GetOldFullPath() << " rename to " << evt.GetNewFullPath() << std::endl; } void main() { cFileSystemWatcher watcher("C:\\Program Files"); watch.SetEventHandler(cFileSystemEvent::CREATED, OnFileCreate); watch.SetEventHandler(cFileSystemEvent::CHANGED, OnFileChange); watch.SetEventHandler(cFileSystemEvent::DELETED, OnFileDelete); watch.SetEventHandler(cFileSystemEvent::RENAMED, OnFileRename); while (true) { watcher.Heartbeat(); } }