Source Insight做个外挂系列之六--“TabSiPlus”的其它问题
关于如何做一个Source Insight外挂插件的全过程都已经写完了,这么一点东西拖了一年的时间才写完,足以说明我是一个很懒的人,如果不是很多朋友的关心和督促,恐怕是难以完成了。许多朋友希望顺着本文的思路也作一个类似于TabSiPlus功能的Source Insight外挂插件,抱歉让他们等了这么长时间,看了本文或许能让大家消消气(大头在后面)。其实即使不是为了给Source Insight做外挂插件,本文的很多方法都可以用于给其它软件做外挂。
尽管本文介绍了做TabSiPlus外挂插件的完整过程,但是要做一个有使用价值的外挂插件还有很多细节要注意,首先是稳定,插入到Source Insight进程中的代码一定要考虑周到,仔细地测试所有分支,确保不能频繁地挂死Source Insight;其次,附加的功能一定不能干扰Source Insight的正常工作,比如窗口消息的处理;最后就是在界面上要能和Source Insight融为一体,插件创建的窗口一定不能覆盖Source Insight的窗口。
以上都是空话,现在用具体的例子来说几个细节,比如TabsiPlus提供了直接根据标签关闭文档窗口的功能,由于文档窗口创建的时候已经获取到窗口的句柄,所以TabsiPlus的第一个版本就使用DestroyWindow() API直接关闭了文档窗口,从外表看确实大到了效果,但是却隐藏了一个BUG,那就是虽然窗口被关闭了,但是Source Insight并不知道文档窗口被关闭了,相应的文件依然处于打开状态,如果文档修改过,这样关闭窗口甚至不会提醒用户保存文档。在自己的程序中关闭窗口当然直接DestroyWindow()就行了,但是既然你的代码是“寄人篱下”,就要按照“别人”的规矩来。通过Spy++观察Source Insight窗口的消息,发现Source Insight窗口只对WM_CLOSE消息会有正常的反映,也就是说Source Insight可能在OnClose()中处理了关闭文件和提示保存修改的操作(很奇怪DestroyWindow()后为什么没有触发Source Insight的OnClose()处理被调用,看来远程注入的代码确实有很多需要注意的地方),后来的版本使用SendMessage将一个WM_CLOSE消息发给文档窗口,这样就很好地解决了这个问题。
上面的问题还没完,让窗口消失就算是关闭了吗,有没有考虑窗口的Focus? 如果关闭某个拥有“焦点(Focus)”的子窗口,Windows会激活此焦点窗口的一个兄弟窗口,通常是上一个拥有焦点的窗口,这个相信使用Windows的人都知道,我也是这么认为的,但是,这一点在外挂中失灵了,在TabSiPlus的线程中,关闭当前拥有焦点的文档窗口后,其它的文档窗口标题栏竟然都是灰的,也就是Source Insight的MDIClient窗口没有选择上一个焦点窗口激活,怎么办?看看TabSiPlus中关闭文档窗口的代码:
void CTabBarsWnd::CloseSIWindow(CSiWindow*& pWindow)
{
ASSERT(pWindow);
HWND hPrevActive = NULL;
if(pGlobalActiveSIWindow != NULL && pWindow != pGlobalActiveSIWindow)
{
DebugTracing(gnDbgLevelNormalDebug,_T("CTabBarsWnd::CloseSIWindow() pGlobalActiveSIWindow = %x"),pGlobalActiveSIWindow);
hPrevActive = pGlobalActiveSIWindow->GetSafeHwnd();
m_iLockUpdates++;
}
pWindow->SendMessage(WM_CLOSE, 0, 0);//now close it!
if(hPrevActive != NULL)
{
::PostMessage(::GetParent(hPrevActive), WM_MDIACTIVATE,(WPARAM)hPrevActive, 0);
}
}
核心只有一句:
pWindow->SendMessage(WM_CLOSE, 0, 0);//now close it!
却要围绕它做很多事情。
再来看一个问题,有没有考虑过Tab标签栏上的标签与实际打开的文档窗口个数不一致的情况?虽然我们Hook可MDI_CREATE消息,但是依然有一些窗口创建是TabsiPlus插件无法感知的,比如Source Insight支持内置宏语言,通过宏进行窗口操作TabsiPlus插件无法感知,还有一种情况是Source Insight对于一些不激活的文档通常不是立即创建窗口,而是在激活的时候才创建窗口显示文档,当用户通过Windows菜单看到的已经打开的文件与你的Tab标签栏不一致会怎么想?没有好的办法,TabSiPlus使用一个定时器处理这种不一致,具体代码在CTabBarsWnd::OnTimer()中。
还有一个问题,如何安全地关闭外挂插件?有一种方法先关闭Source Insight,然后关闭加载器TabSiHost.exe,然后再打开Source Insight。让自己接受这种方案都很难,更何况别人,如果能够在插件中提供一个界面,通过用户选择可以直接退出插件就好了,实现这一点关键是Source Insight内部关闭Tab标签窗口后如何中止加载器TabSiHost.exe,如果不中止TabSiHost.exe,TabSiHost.exe会再次加载TabsiPlus.dll插件。TabsiPlus通过内核对象完成与TabSiHost.exe的同步:
void CTabBarsWnd::ShutDownTabSiPlus()
{
HANDLE hAnotherTabSiHostEvent = NULL;
LPCTSTR szGlobalKernelName = _T("Local//TabSiHostIsAlreadyRunning");
hAnotherTabSiHostEvent = CreateEvent( NULL, TRUE, FALSE, szGlobalKernelName );
DWORD dwer = GetLastError();
if(dwer == ERROR_ALREADY_EXISTS)
{
ResetEvent( hAnotherTabSiHostEvent );
}
::CloseHandle(hAnotherTabSiHostEvent);
g_pSiMDIClientWnd->SetManaging(false);
DestroyWindow();
}
还有,TabSiPlus内部窗口之前传递数据都是通过自定义消息进行的,原因就是Tab标签窗口与Source Insight的窗口是工作在不同的线程中的,在线程之间只有句柄是安全的,向窗口句柄发送消息要比直接操纵数据要安全。还有,当使用了Source Insight的查找字符串功能时,Source Insight会打开一个窗口显示搜索的结果,这个窗口的窗口类名和代码窗口一样,都是si_Sw,但是其窗口标题却和代码窗口的标题不一样,这个要区分。其它的细节还有很多,就不一样列举了,具体看代码吧。
罗嗦了半天,代码在哪里?本来想随本文一起上传的,但是这个Blog上传附件太麻烦,只好放到我的CSDN资源里了,大家可以到我的空间下载源代码。本文附带的代码是一份精简的TabSiPlus插件代码,为了大家理解代码,我去掉了全部装饰性的代码和附加功能代码,包括很多预防性代码,这样做地目的就是为了大家在学习源代码时能够将注意力集中在框架上而不是枝节琐事。尽管如此,这是一个完整的可工作的Tab标签栏,演示了本文写的全部内容。除了我的代码之外,源代码中还使用了一些*代码,使用时请注意相关作者的权利要求。
代码的编译很简单,用VC 6.0打开直接Build就行了,为什么不升级到VC7 or VC8?其实我很懒。调试的时候注意相关的资源文件要在同一个目录中,TaiSiHost.exe的调试比较简单,直接加载就行了,调试TabSiPlus.dll比较麻烦,首先关闭已经打开的Source Insight程序,然后在Project Setting/Debug 窗口中设置“Executable for debug session:”为Source Insight的主程序,通常是Insight3.exe,再然后运行TabSiHost.exe,最后就可以按F5开始调试了。整个过程就是:按下F5后,VC根据调试设置启动insight3.exe,已经运行的TabSiHost.exe发现启动了Source Insight,就会远程代码注入到insight3.exe,于是insight3.exe就会加载TabSiPlus.dll,这样就可以调试了。
Source Insignt文件标签外挂:TabSiPlus的下载地址:
http://www.winmsg.com/download/tabsiplus.zip