编写简易目录监控的心得

目录监控主要用WinAPI函数ReadDirectoryChangeW实现对目录中的文件进行删除,修改,重命名功能监控,并将监控信息显示在界面上.用户可以选择多个目录进行监控.

主界面如图1所示:

编写简易目录监控的心得_第1张图片

                               图1

功能实现:

  1.   目录选择(如图2所示)

编写简易目录监控的心得_第2张图片

实现函数:

PIDLIST_ABSOLUTE SHBrowseForFolder( LPBROWSEINFO lpbi)

参数:结构体指针.

返回值:PIDL..PIDL表示所选目录相对于跟的位置.(需要用SHGetPathFromIDList函数将PIDL转换为具体路径.如果选择取消,返回NULL

      char szPath[MAX_PATH];
	CString str;
	ZeroMemory(szPath,sizeof(szPath));
	BROWSEINFO bi;
	bi.hwndOwner=m_hWnd;
	bi.pidlRoot=NULL;
	bi.pszDisplayName=szPath;
	bi.lpszTitle="请选择监控的目录!";
	bi.ulFlags=0;
	bi.lpfn=NULL;
	bi.lParam=0;
	bi.iImage=0;
	LPITEMIDLIST lp=SHBrowseForFolder(&bi);
	if(lp)
	{
		if(SHGetPathFromIDList(lp,szPath))
		{
			str.Format("你选择的目录是:\n%s\n是否开始监听?",szPath);
			if(IDOK==MessageBox(str,"提醒",MB_OKCANCEL | MB_ICONQUESTION))
			{
				bool isSuc=fileWatcher->runNewWatch(szPath);
				if(isSuc)
				{
					HANDLE hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
					handMap[szPath]=hEvent;
					CString sDir;
					m_watchDir.GetWindowText(sDir);
					sDir+=szPath;
					sDir+=";";
					m_watchDir.SetWindowText(sDir);
					MessageBox("监控成功!");
				}
			
		}else
		{
			MessageBox("目录选择有误!");
		}
		
	}

2. 因为需要监控多个目录,所以采用多线程.

函数:CreateThread

HANDLE WINAPICreateThread(

LPSECURITY_ATTRIBUTESlpThreadAttributes,//安全性.一般设置为NULL

SIZE_TdwStackSize,//初试栈大小,一般设置为0.,使用与调用该函数的线程相同大小的栈空间

LPTHREAD_START_ROUTINElpStartAddress,//线程函数入口地址.函数因为全局函数或类静态函数.

LPVOIDlpParameter,//线程函数参数

DWORDdwCreationFlags,//控制线程创建的附加标志.如果是CREATE_SUSPENDED表示线程创建后为暂停状态.如果是0,创建后立即运行

LPDWORDlpThreadId//接收线程ID

);

3. 目录监控:

     这部分主要实现在线程函数里面.用户选择一个监控目录,系统都会创建一个线程去监控变化.在使用ReadDirectoryChangesW函数前,需要使用CreateFile函数.在msdn里面提到.目录需要用FILE_LIST_DIRECTORY打开.

 HANDLE WINAPI CreateFile(
  __in      LPCTSTR lpFileName,//创建或打开的文件名
  __in      DWORD dwDesiredAccess,//对象访问方式
  __in      DWORD dwShareMode,//共享方式
  __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes,//安全性
  __in      DWORD dwCreationDisposition,//如何创建文件
  __in      DWORD dwFlagsAndAttributes,//文件属性和标志
  __in_opt  HANDLE hTemplateFile//模版文件句柄
);
HANDLE hDir;
hDir=CreateFile(watchDir,GENERIC_READ | GENERIC_WRITE
		,FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
		,NULL,OPEN_EXISTING,FILE_FLAG_BACKUP_SEMANTICS,NULL);
if(INVALID_HANDLE_VALUE==hDir)
    return false;

4. ReadDirectoryChangesW函数实现监控.具体见代码.

char *buf=new char[DeFileWaterBuffer];
	FILE_NOTIFY_INFORMATION *pNotify=(FILE_NOTIFY_INFORMATION *)buf;
	monitorParam *mp=new monitorParam;
	DWORD BytesReturned;
	while(dirThreadMap[watchDir])
	{
		memset(buf,0,DeFileWaterBuffer);
		BytesReturned=0;
		if(ReadDirectoryChangesW(hDir,pNotify,DeFileWaterBuffer,true,
			FILE_NOTIFY_CHANGE_FILE_NAME |
			FILE_NOTIFY_CHANGE_DIR_NAME |
			FILE_NOTIFY_CHANGE_ATTRIBUTES |
			FILE_NOTIFY_CHANGE_SIZE|
			FILE_NOTIFY_CHANGE_LAST_WRITE|
			FILE_NOTIFY_CHANGE_LAST_ACCESS|
			FILE_NOTIFY_CHANGE_CREATION|
			FILE_NOTIFY_CHANGE_SECURITY,
			&BytesReturned,
			NULL,
			NULL))
		{
			if(BytesReturned>0)
			{
				while(true)
				{
					char szPath[MAX_PATH];
					memset(szPath,0,MAX_PATH);
					WideCharToMultiByte(CP_ACP,0,pNotify->FileName,pNotify->FileNameLength/2,szPath,MAX_PATH,NULL,NULL);
					CString sInfo;
					CString lastInfo;
					CString path=watchDir+"\\"+szPath;
					switch(pNotify->Action)
					{
					case FILE_ACTION_ADDED:
						sInfo.Format("添加 %s",szPath);
						if(sInfo!=lastInfo)
						{	
							mp->sPath=path;
							mp->sReason="添加";
							mp->sTime=CMeans::getNowTime();
							::SendMessage(parentHwnd,UM_THREADMONITOR,0,(LPARAM)mp);
							lastInfo=sInfo;
						}
						break;
					case FILE_ACTION_REMOVED:
						sInfo.Format("删除 %s",szPath);
						if(sInfo!=lastInfo)
						{
							mp->sPath=path;
							mp->sReason="删除";
							mp->sTime=CMeans::getNowTime();
							::SendMessage(parentHwnd,UM_THREADMONITOR,0,(LPARAM)mp);
							lastInfo=sInfo;
						}
						break;
					case FILE_ACTION_MODIFIED:
						sInfo.Format("修改 %s",szPath);
						if(sInfo!=lastInfo)
						{
							mp->sPath=path;
							mp->sReason="修改";
							mp->sTime=CMeans::getNowTime();
							::SendMessage(parentHwnd,UM_THREADMONITOR,0,(LPARAM)mp);
							lastInfo=sInfo;
						}
						break;
					case FILE_ACTION_RENAMED_OLD_NAME:
						sInfo.Format("更名前 %s",szPath);
						if(sInfo!=lastInfo)
						{
							mp->sPath=path;
							mp->sReason="更名前";
							mp->sTime=CMeans::getNowTime();
							::SendMessage(parentHwnd,UM_THREADMONITOR,0,(LPARAM)mp);
							lastInfo=sInfo;
						}
						break;
					case FILE_ACTION_RENAMED_NEW_NAME:
						sInfo.Format("更名后 %s",szPath);
						if(sInfo!=lastInfo)
						{
							mp->sPath=path;
							mp->sReason="更名后";
							mp->sTime=CMeans::getNowTime();
							::SendMessage(parentHwnd,UM_THREADMONITOR,0,(LPARAM)mp);
							lastInfo=sInfo;
						}
						break;

					}
					if(pNotify->NextEntryOffset)
					{
						pNotify=(PFILE_NOTIFY_INFORMATION)((char*)pNotify+pNotify->NextEntryOffset);

					}else
						break;
				}
				
			}else
			{
				AfxMessageBox("存储区溢出!");
			}
		}else
		{
			DWORD dw=GetLastError();
			CString str;
			str.Format("错误码:%d",dw);
			AfxMessageBox(str);
			break;
		}

	}
	AfxMessageBox("退出监控!");
	delete[] buf;
	delete mp;
	SetEvent(handMap[watchDir]);
	CloseHandle(handMap[watchDir]);
	handMap[watchDir]=NULL;
	CloseHandle(hDir);

 

遇到的问题:

目录监控功能实现并不难.就那几个API.在这次做目录监控时,遇到的问题才让我学到了不少东西.

1、 线程函数使用了CString,线程非正常退出.CString将会导致内存泄露!

 

DWORD WINAPI watchThreadProc(LPVOID lpParam)//线程函数
{
     CString str;
     While(1)
     {
}
}

首先定义了一个线程函数watchThreadProc.里面很简单.就定义了CString对象,然后一个死循环.主窗体启动时,创建一个线程.当主窗体退出时,子线程强制退出.此时必然内存泄露.

按理说局部变量内存分配是在栈上,生命期结束后会自动释放内存,不会造成内存泄露.而CString比较特殊,它是从进程堆(在ATL上)或CRT堆(在MFC中)分配内存.既然是在堆上面分配的内存,则必然需要释放.正常情况下,该变量生命期结束后,有管理器回收.当线程非正常退出后,无法释放CString所分配的内存,必然造成内存泄露.自然而然,当主窗体退出时,就得想办法让运行中的线程正常退出.

1、 主线程与子线程同步问题

       子线程监控目录变化,之前是用的while(true),改为了while(bool变量).但是bool变量又不能事先定义,我不知道用户会创建几个线程.这时想到用map<CString,BOOL>(这里补充一点,因为将map定义为全局变量.而且多个函数中会调用,多个函数有可能在多个cpp文件中.所以用extern map<CString,BOOL>声明在需要用的.h文件中,然后定义map在一个cpp文件中.如果你直接定义map在.h文件中,会导致链接错误.)map是个好东西,用起来挺顺手.我喜欢.每次创建一个线程成功后,用路径szPath做键,TRUE做路径对应的值.如果结束某个目录的监听,就可以将对应的BOOL设置为FALSE.当关闭主窗体时,将所有监听目录的BOOL值设置为FALSE,然后退出.

       但是这样有时候依然会导致内存泄露.因为主线程退出速度比较快,主线程完全退出后,子线程还没有运行完毕,这样也会导致子线程非正常退出.所以想到了线程之间的同步.主线程等待子线程完全退出后才退出.

        线程同步有三种方法.互斥对象、事件对象、关键代码段.

        我选择事件对象.用CreateEvent创建.

HANDLE WINAPI CreateEvent(
  __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,//安全性.
  __in      BOOL bManualReset,//创建人工重置对象,还是自动重置对象
  __in      BOOL bInitialState,//初试状态是否为有信号状态
  __in_opt  LPCTSTR lpName//事件对象名称.
);

       每当线程创建成功,创建一个事件对象,初始为无信号状态.当线程结束时,将该事件对象设置为有信号状态.主线程等待所有的事件对象为有限状态后退出.等待单个内核对象是否为有效状态,用WatiForSingleObject函数.等到多个用WaitForMultipleObjects函数.按照这种思路实现出来,一运行当点击关闭按钮后,主程序死了.刚开始以为写的代码有误,仔细检查后发现没错.后来百度谷歌才明白.windows是基于消息系统的,很多操作都是靠消息完成的,但主线程挂起以后,自然而然无法处理消息.而子线程中需要主线程处理消息后才能返回.这样就导致了死锁.

       为了避免死锁,放弃使用WaitForMultipleObjects函数,该用MsgWaitForMultipleObjects函数.这个函数的特点是不仅可以等待内核对象,也可以等待消息.当有消息到达也可以返回,这样就可以处理消息,避免死锁了.

DWORD WINAPI MsgWaitForMultipleObjects(
  __in  DWORD nCount,//要等待的内核对象个数
  __in  const HANDLE *pHandles,//要等待的内核对象句柄数组指针
  __in  BOOL bWaitAll,//true表示当所等待的内核对象全部有效是才返回,false表示只要有一个对象有效就返回.或者有消息到达时也返回.
  __in  DWORD dwMilliseconds,//等到时间.毫秒级.INFINITE表示无限等待
  __in  DWORD dwWakeMask//表示等待的对象类型.一般设置QS_ALLEVENTS表示等待任何类型
);

 

这里需要提到的是函数的返回值.

返回值 意义
WAIT_OBJECT_0+nCount 有消息到达

WAIT_OBJECT_0~WAIT_OBJECT_0+

nCount-1

如果bWaitAll为true表示所有等待的对象为有效状态.

如果bWaitAll为false,表示某一个对象为有效状态.

可以用函数返回值减去WAIT_OBJECT_0得到该对象在数组中的位置

WAIT_TIMEOUT 等待超时
WAIT_FAILED

函数出错.可用GetLastError得到错误码

while(1)
{
	dRet=MsgWaitForMultipleObjects(nWaitCount,hEvent,FALSE,10000,QS_ALLINPUT);
	if(dRet==WAIT_OBJECT_0+nWaitCount)
	{
		TRACE("收到消息,函数返回值%d\n",dRet);
		while(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}else
		if(dRet>=WAIT_OBJECT_0&&dRet<WAIT_OBJECT_0+nWaitCount)
		{
			nExitThreadCount++;
			if(nExitThreadCount<avaSize)
			{
				TRACE("一个线程退出了\n");
				int nIndex=dRet-WAIT_OBJECT_0;
				hEvent[nIndex]=hEvent[nWaitCount-1];
				hEvent[nWaitCount-1]=NULL;
				--nWaitCount;
			}else
			{
				TRACE("所有的线程都退出!\n");
				break;
			}
		}
}


3.关于子线程与主线程通信的问题.       

    就像这次做的目录监控一样,子线程监控目录变化,并将变化信息显示在主窗体的CListCtrl上.之前用的是主窗体this指针传入子线程函数,然后回调主窗体方法.不过这样做有时候是有问题的.比如说我子线程监控到目录变化,我想创建一个窗体.这时候必然出错.解决方法就是采用消息机制.你自定义消息.采用SendMessage或者PostMessage将消息发送给主线程.让主线程创建窗体.

 



 

你可能感兴趣的:(编写简易目录监控的心得)