目录监控功能主要用在系统间数据迁移时观察缓存目录文件变化的场景,结合文件类型检测、病毒检测、安全文件传输等功能可以构建一个完整的系统文件安全入出的解决方案。该功能有多种实现方式,下面我们介绍两种不同方式来实现该功能。
通过ReadDirectoryChangesW实现目录监控
ReadDirectoryChangesW是Windows专门用于监控文件系统变化的一个系统API,我们通过调用该系统API可以实现目录变化的监控。
参考如下:
ReadDirectoryChangesW 函数 (winbase.h) - Win32 apps | Microsoft Learn
下面具体说明一下如何实现:
目录监控功能控制
在SystemPanel的菜单资源编辑器中添加新的菜单项
修改菜单变量名,添加菜单事件处理函数
在目录监控控制菜单处理函数中添加如下代码:
//目录监控控制
#include <thread>
#include <future>
#include <functional>
#include <queue>
HANDLE m_hDirectory;//需要监控目录的句柄
queue<tuple<string, string, int>> dirChangeQueue;//目录变化数据队列
#define SYS_ALLOC_BUF 1024*128
future<void> t_rdm;//检测目录文件改变的后台过程futrue
future<void> t_redm;//处理目录文件改变的后台过程futrue
void SystemPanel::Onm_DirMonAPISelected(wxCommandEvent& event)
{
if(m_hDirectory!=NULL)
{
int ret=wxMessageBox(_("系统有正在监控的目录,是否停止?"),_("目录监控提示"),wxYES_NO,this);
if(ret==wxYES)
{
cout<<"正在停止目录监控"<<endl;
if(m_hDirectory!=NULL)
{
CloseHandle(m_hDirectory);
m_hDirectory=NULL;
}
//监控前清空监控列表
if(m_FileList->GetItemCount()!=0)
m_FileList->DeleteAllItems();
}
else if(ret==wxNO)
{
m_DirMonAPI->Check(true);
return;
}
else
{
m_DirMonAPI->Check(true);
return;
}
}
else
{
wxString pathName=wxDirSelector(_("请选择要监控的目录!"), "",
wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
if ( pathName.empty() )
{
m_DirMonAPI->Check(false);
return;
}
//监控前清空监控列表
if(m_FileList->GetItemCount()!=0)
m_FileList->DeleteAllItems();
//监控前获取所有监控目标
AdjustPrivileges();//提升权限,解决特殊文件访问导致的软件崩溃
for(auto &p:std::filesystem::recursive_directory_iterator(pathName.ToStdString()))
{
std::cout << p.path() << endl;
if(m_FileList->FindItem(-1,p.path().wstring(),false)!=wxNOT_FOUND)
continue;
auto indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
m_FileList->SetItem(indexItem, 0, (p.path().wstring()));
if(!std::filesystem::is_directory(p))
m_FileList->SetItem(indexItem, 1,to_string(std::filesystem::file_size(p)));
else
m_FileList->SetItem(indexItem, 1,_("Dir"));
m_FileList->SetItem(indexItem, 2,p.path().extension().wstring() );
}
//异步执行目录监控
t_rdm=async(std::launch::async,bind(&DoDirMonitor,this,pathName));
cout<<"当前正在监控的目录是: "+pathName<<endl;
}
}
监控目录消息
由于目录变化消息可能很多,为了防止在处理很多目录变化时可能会丢失目录变化消息的问题发生,我们采用异步的方式来运行如下目录消息监控代码:
void SystemPanel::DoDirMonitor(wxString szWatchDirectory)
{
if(szWatchDirectory.IsEmpty())
return;
FILE_NOTIFY_INFORMATION *pInfo;
TCHAR szBuffer[SYS_ALLOC_BUF/2]= {0};
DWORD dwOffset=0;
DWORD BytesReturned;
// Get Directory Handle by CreateFile
m_hDirectory=CreateFile( szWatchDirectory.t_str(),
GENERIC_READ | FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL );
if ( m_hDirectory==INVALID_HANDLE_VALUE)
{
cout<< "CreateFile Fail!"<<endl;
return ;
}
//异步执行文件变化队列处理,只有在目录被监控时才可以开始处理
t_redm=async(std::launch::async,bind(&DoDirChange,this));
string oldName="";
string newName="";
AdjustPrivileges();
while( ReadDirectoryChangesW(
m_hDirectory,
szBuffer,
SYS_ALLOC_BUF,
TRUE,
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME,
&BytesReturned,
NULL,
NULL))
{
cout<<endl;
dwOffset=0;
do
{
// Get a pointer to the first change record...
pInfo=(FILE_NOTIFY_INFORMATION *)&szBuffer[dwOffset/2];
dwOffset+=pInfo->NextEntryOffset;
// i++;
// Event Process
TCHAR pathName[100]= {0};
memcpy((void *)pathName, (void *)(pInfo->FileName), pInfo->FileNameLength);
wxString fpn=szWatchDirectory+_("\\")+_(pathName);
if (pInfo->Action==FILE_ACTION_ADDED)
{
dirChangeQueue.push(make_tuple(string(fpn), "", FILE_ACTION_ADDED));
cout<<_("新增文件: ") << fpn<<endl;
}
else if (pInfo->Action==FILE_ACTION_REMOVED)
{
dirChangeQueue.push(make_tuple(string(fpn), "", FILE_ACTION_REMOVED));
cout<<_("删除文件: ")+ fpn<<endl;
}
else if (pInfo->Action==FILE_ACTION_MODIFIED)
{
dirChangeQueue.push(make_tuple(string(fpn), "", FILE_ACTION_MODIFIED));
cout<<_("修改内容文件: ") + fpn<<endl;
}
else if (pInfo->Action==FILE_ACTION_RENAMED_OLD_NAME)//无法确认新文件名
{
oldName=string(fpn);
// dirChangeQueue.push(make_tuple(oldName,newName , FILE_ACTION_RENAMED_OLD_NAME));
// cout<<_("重命名文件2: 文件 ") +oldName+_("被重命名为 ") +newName <<endl;
}
else if (pInfo->Action==FILE_ACTION_RENAMED_NEW_NAME)
{
newName=string(fpn);
dirChangeQueue.push(make_tuple(oldName, newName, FILE_ACTION_RENAMED_NEW_NAME));
cout<<_("重命名文件: 文件 ")+oldName +_("被重命名为 ") +newName <<endl;
}
}
while(pInfo->NextEntryOffset!=0);
// sys-alloc buffer overflowed
if(BytesReturned==0)
printf("*********************sys-alloc buffer OVERFLOW!************************\n");
// printf("Byte Returned : %d\n",BytesReturned);
memset(szBuffer,0,sizeof(szBuffer));
}
CloseHandle( m_hDirectory);
cout<<GetLastError()<<endl;
}
处理目录消息
同样如果目录变化消息很多,处理目录变化的过程也可能出现太耗时导致目录监控消息丢失的情况,所以我们同样采取异步的方式采用如下代码处理目录消息:
void SystemPanel::DoDirChange()
{
while (m_hDirectory!=NULL)
{
while(!dirChangeQueue.empty())
{
std::tuple<string, string, int> dc=dirChangeQueue.front();
dirChangeQueue.pop();
string on=get<0>(dc);
string nn=get<1>(dc);
long indexItem;
long indexOldItem=m_FileList->FindItem(-1,on);
// long indexNewItem=m_FileList->FindItem(-1,nn);
UINT sizefile;
wxString wxon(on),wxnn(nn);
path old_path(wxon.wc_str()),new_path(wxnn.wc_str());
// cout<<"旧文件名: "<<old_path.wstring()<<" 新文件名: "<<new_path.wstring()<<endl;
switch(get<2>(dc))
{
case FILE_ACTION_ADDED:
{
indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
sizefile = file_size(old_path); //文件大小(字节)
m_FileList->SetItem(indexItem, 0, on);
m_FileList->SetItem(indexItem, 1,to_string(sizefile));
break;
}
case FILE_ACTION_MODIFIED:
{
// cout<<"文件监控: 发生文件修改, "<<on<<" 内容被修改!"<<endl;
sizefile = file_size(old_path); //文件大小(字节)
if(indexOldItem!=wxNOT_FOUND)
{
m_FileList->SetItem(indexOldItem,1,to_string(sizefile));
m_FileList->SetItem(indexOldItem,2,_(""));
}
else
{
indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
m_FileList->SetItem(indexItem, 0, on);
m_FileList->SetItem(indexItem,1,to_string(sizefile));
m_FileList->SetItem(indexItem,2,_(""));
}
break;
}
case FILE_ACTION_REMOVED:
{
// cout<<"文件监控: 发生文件删除, "<<on<<" 被删除!"<<endl;
m_FileList->DeleteItem(indexOldItem);
break;
}
case FILE_ACTION_RENAMED_NEW_NAME:
{
// cout<<"文件监控: 发生文件重命名, "<<on<<" 被重命名为: "<<nn<<endl;
if(indexOldItem!=wxNOT_FOUND)
{
m_FileList->DeleteItem(indexOldItem);
}
// if(indexNewItem!=wxNOT_FOUND)
// break;
indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
sizefile = file_size(new_path); //文件大小(字节)
m_FileList->SetItem(indexItem, 0, nn);
m_FileList->SetItem(indexItem, 1,to_string(sizefile));
break;
}
// case FILE_RENAMED_OLD_NAME:
// {
cout<<"文件监控: 发生文件重命名, "<<on<<" 被重命名为: "<<nn<<endl;
// if(indexOldItem!=wxNOT_FOUND)
// {
// m_FileList->DeleteItem(indexOldItem);
// //continue;
// }
// if(indexNewItem!=wxNOT_FOUND)
// break;
// indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),"");
// sizefile = fs::file_size(nn); //文件大小(字节)
// stringstream ss;
// ss<<sizefile;
// m_FileList->SetItem(indexItem, 0, nn);
// m_FileList->SetItem(indexItem, 1,ss.str());
// break;
// }
default:
break;
}
}
}
编译并运行程序,效果如下:
至此,我们完成了通过ReadDirectoryChangesW系统API实现目录监控的工作。
通过wxFileSystemWatcher实现目录监控
由于wxWidgets提供了专门用于目录监控的工具类wxFileSystemWatcher,我们可以直接用该类来实现目录监控功能。下面我们来具体介绍如何实现:
目录监控功能控制
在SystemPanel的菜单资源编辑器中添加新的菜单项
修改菜单变量名,添加菜单事件处理函数
在目录监控控制菜单处理函数中添加如下代码:
#include <wx/fswatcher.h>
wxFileSystemWatcher *m_watcher=NULL;
//wxFileSystemWatcher目录监控控制
void SystemPanel::Onm_DirMonWxSelected(wxCommandEvent& event)
{
if(m_watcher!=NULL)
{
int ret=wxMessageBox(_("系统有正在监控的目录,是否停止?"),_("目录监控提示"),wxYES_NO,this);
if(ret==wxYES)
{
wxDELETE(m_watcher);
m_watcher=NULL;
m_DirMonWx->Check(false);
cout<<"当前没有正在监控的目录"<<endl;
//监控前清空监控列表
if(m_FileList->GetItemCount()!=0)
m_FileList->DeleteAllItems();
}
else if(ret==wxNO)
{
m_DirMonWx->Check(true);
return;
}
else
{
m_DirMonWx->Check(true);
return;
}
}
else
{
wxString filename=wxDirSelector(_("请选择要监控的目录!"), "",
wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
if ( filename.empty() )
{
m_DirMonWx->Check(false);
return;
}
//监控前清空监控列表
if(m_FileList->GetItemCount()!=0)
m_FileList->DeleteAllItems();
//监控前获取所有监控目标
AdjustPrivileges();//提升权限,解决特殊文件访问导致的软件崩溃
for(auto &p:std::filesystem::recursive_directory_iterator(filename.ToStdString()))
{
std::cout << p.path() << endl;
if(m_FileList->FindItem(-1,p.path().wstring(),false)!=wxNOT_FOUND)
continue;
auto indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
m_FileList->SetItem(indexItem, 0, (p.path().wstring()));
if(!std::filesystem::is_directory(p))
m_FileList->SetItem(indexItem, 1,to_string(std::filesystem::file_size(p)));
else
m_FileList->SetItem(indexItem, 1,_("Dir"));
m_FileList->SetItem(indexItem, 2,p.path().extension().wstring() );
}
wxCHECK_RET(!m_watcher, "Watcher already initialized");
m_watcher = new wxFileSystemWatcher();
m_watcher->SetOwner(this);
wxFileName fn = wxFileName::DirName(filename);
m_watcher->AddTree(fn,wxFSW_EVENT_CREATE |wxFSW_EVENT_DELETE|wxFSW_EVENT_RENAME|wxFSW_EVENT_MODIFY);
Bind(wxEVT_FSWATCHER, OnFileSystemEvent, this);
cout<<"当前正在监控的目录是: "+filename<<endl;
}
}
监控目录消息
wxWidgets内部定义了wxEVT_FSWATCHER事件来传递目录变化信息,我们通过为wxFileSystemWatcher对象m_watcher指定目录监控获取的目录变化的类型和处理目标变化事件的函数,具体代码如下:
m_watcher = new wxFileSystemWatcher();
m_watcher->SetOwner(this);
wxFileName fn = wxFileName::DirName(pathName);
m_watcher->AddTree(fn,wxFSW_EVENT_CREATE |wxFSW_EVENT_DELETE|wxFSW_EVENT_RENAME|wxFSW_EVENT_MODIFY);
Bind(wxEVT_FSWATCHER, OnFileSystemEvent, this);
处理目录消息
处理目录变化的代码如下:
//把目录变化类型转换为字符串
static wxString GetFSWEventChangeTypeName(int changeType)
{
switch (changeType)
{
case wxFSW_EVENT_CREATE:
return "CREATE";
case wxFSW_EVENT_DELETE:
return "DELETE";
case wxFSW_EVENT_RENAME:
return "RENAME";
case wxFSW_EVENT_MODIFY:
return "MODIFY";
case wxFSW_EVENT_ACCESS:
return "ACCESS";
case wxFSW_EVENT_ATTRIB: // Currently this is wxGTK-only
return "ATTRIBUTE";
#ifdef wxHAS_INOTIFY
case wxFSW_EVENT_UNMOUNT: // Currently this is wxGTK-only
return "UNMOUNT";
#endif
case wxFSW_EVENT_WARNING:
return "WARNING";
case wxFSW_EVENT_ERROR:
return "ERROR";
}
return "INVALID_TYPE";
}
//格式化输出目录变化信息
const wxString LOG_FORMAT = " %-12s %-36s %-36s";
void LogEvent(const wxFileSystemWatcherEvent& event)
{
wxString entry = wxString::Format(LOG_FORMAT + "\n",
GetFSWEventChangeTypeName(event.GetChangeType()),
event.GetPath().GetFullPath(),
event.GetNewPath().GetFullPath());
wxTrace(entry);
}
//处理目录变化事件
void SystemPanel::OnFileSystemEvent(wxFileSystemWatcherEvent& event)
{
LogEvent(event);
wxString on=event.GetPath().GetFullPath();
wxString nn=event.GetNewPath().GetFullPath();
long indexItem;
long indexOldItem=m_FileList->FindItem(-1,on);
switch(event.GetChangeType())
{
case wxFSW_EVENT_CREATE:
{
indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
m_FileList->SetItem(indexItem, 0, on);
m_FileList->SetItem(indexItem, 1,event.GetPath().GetSize().ToString());
m_FileList->SetItem(indexItem, 2,GetFSWEventChangeTypeName(event.GetChangeType()));
break;
}
case wxFSW_EVENT_MODIFY:
{
if(indexOldItem!=wxNOT_FOUND)
{
m_FileList->SetItem(indexOldItem,1,event.GetPath().GetSize().ToString());
m_FileList->SetItem(indexOldItem,2,_(""));
m_FileList->SetItem(indexOldItem, 2,GetFSWEventChangeTypeName(event.GetChangeType()));
}
else
{
indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
m_FileList->SetItem(indexItem, 0, on);
m_FileList->SetItem(indexItem,1,event.GetPath().GetSize().ToString());
m_FileList->SetItem(indexItem,2,_(""));
m_FileList->SetItem(indexItem, 2,GetFSWEventChangeTypeName(event.GetChangeType()));
}
break;
}
case wxFSW_EVENT_DELETE:
{
m_FileList->DeleteItem(indexOldItem);
break;
}
case wxFSW_EVENT_RENAME:
{
if(indexOldItem!=wxNOT_FOUND)
{
m_FileList->DeleteItem(indexOldItem);
}
indexItem = m_FileList->InsertItem(m_FileList->GetItemCount(),_(""));
m_FileList->SetItem(indexItem, 0, nn);
m_FileList->SetItem(indexItem, 1,event.GetNewPath().GetSize().ToString());
m_FileList->SetItem(indexItem, 2,GetFSWEventChangeTypeName(event.GetChangeType()));
break;
}
default:
break;
}
}
编译并运行程序,其效果与通过ReadDirectoryChangesW实现目录监控完全一致。
至此通过 wxFileSystemWatcher实现目录监控的功能已经完成。