关于MFC中使用ShellExecute出现的进程冲突问题

2023-11-17

目录

问题背景

问题分析


问题背景

       现在有一个MFC写的界面程序,以及一个外部exe文件。用户通过界面选择文件a,MFC将文件a的路径作为参数,调用exe文件生成一个解析文件b,然后MFC再读取这个文件b。

       为了完成这一目的,就需要在MFC中调用外部exe文件,我这里选用的是ShellExecute函数。

//function
...
HINSTANCE proc = ShellExecute(this->m_hWnd, L"open", L"pdf2txt.exe", FilePath, L".\\", SW_HIDE);  //异步调用
//伪代码
  if(!open(b))
  {
     Warn("Open failed !") and return;
  }
  read(b);
...

       看似逻辑正确,但是实际运行时会出现这样的情况:在第一次执行function时会出现文件b打开失败的情况,但是接着执行第二次function又变成正常的了,这是什么原因呢?

问题分析

       按道理来说,第一次执行时先调用外部exe文件就会生成文件b,那么接着再打开文件b应该是可以打开的,而这里打开文件b失败,由于文件b是由无到有,因此最有可能的就是在打开b文件的时候b文件不存在导致的。

       为什么b文件会不存在呢?一种可能的原因是ShellExecute函数调用失败,但是通过测试发现,第一次function执行结束后,b文件是存在的,也就是说,ShellExecute是的执行是成功的;那么,为什么b文件明明能够生成,但是open时不存在呢?最大的可能就是b文件是在open之后生成的,也就是说,函数实际运行的顺序并没有按照逻辑来

       换句话说,ShellExecute开辟一个进程去执行exe文件,当前进程继续按照当前程序执行,两个进程的执行顺序是不定的,这就有可能出现当前进程比运行exe的进程更快地就执行到open函数了,此时open函数时发现b文件不存在,然后较慢的进程才执行exe文件生成了b文件。

       引起这个问题的原因,当然就要考虑ShellExecute函数本身了。查阅相关资料发现,ShellExecute实际上是一个异步函数,它并不会等到exe执行结束后才返回,而是立即返回,很显然,程序的逻辑是希望等待exe执行结束后ShellExecute再返回的,也就是同步执行,为了解决这个问题,就可以考虑使用WaitForSingleObject来进行阻塞,如下所示。

//function
...

HINSTANCE proc = ShellExecute(this->m_hWnd, L"open", L"pdf2txt.exe", FilePath, L".\\", SW_HIDE);  //异步调用
WaitForSingleObject(proc, INFINITE);  //等待结束

//伪代码
  if(!open(b))
  {
     Warn("Open failed !") and return;
  }
  read(b);
...

        这样程序是不是就正常了呢?再来测试一下:

        问题依旧!这又是什么原因呢?

       实际上,这是因为 ShellExecute返回的是一个HINSTANCE句柄,而WaitForSingleObject所需要的是一个HANDLE句柄,二者是虽然类似,但是也是有差别的,HINSTANCE句柄指向一个模块的地址,而HANDLE句柄指向一个具体的进程、线程等资源,因此,这里不应该直接通过HINSTANCE来调用WaitForSingleObject。

       可是ShellExecute只返回一个HINSTANCE,这该怎么办呢?可能也有一些其他的办法通过HINSTANCE来获得HANDLE,但是应该会很麻烦,这里一个比较好的办法就是调用更强大的ShellExecuteEx函数。其定义如下:

BOOL ShellExecuteEx(
  _Inout_ SHELLEXECUTEINFO *pExecInfo
);

       ShellExecuteEx函数只有一个SHELLEXECUTEINFO类型的结构体参数,而SHELLEXECUTEINFO结构体内的成员则非常丰富了:

typedef struct _SHELLEXECUTEINFO {
  DWORD     cbSize;//结构大小,sizeof(SHELLEXECUTEINFO)
  ULONG     fMask;//掩码,指定结构成员的有效性
  HWND      hwnd;//父窗口句柄或出错时显示错误父窗口的句柄,可以为 NULL
  LPCTSTR   lpVerb;//指定该函数的执行动作
  LPCTSTR   lpFile;//操作对象路径
  LPCTSTR   lpParameters;//执行参数,可以为 ULL
  LPCTSTR   lpDirectory;//工作目录,可以为 NULL
  int       nShow;//显示方式
  HINSTANCE hInstApp;//如果设置了 SEE_MASK_NOCLOSEPROCESS ,并且调用成功则该值大于32,调用失败者被设置错误值
  LPVOID    lpIDList;//ITEMIDLIST结构的地址,存储成员的特别标识符,当fMask不包括SEE_MASK_IDLIST或SEE_MASK_INVOKEIDLIST时该项被忽略
  LPCTSTR   lpClass;//指明文件类别的名字或GUID,当fMask不包括SEE_MASK_CLASSNAME时该项被忽略
  HKEY      hkeyClass;//获得已在系统注册的文件类型的Handle,当fMask不包括SEE_MASK_HOTKEY时该项被忽略
  DWORD     dwHotKey;//程序的热键关联,低位存储虚拟关键码(Key Code),高位存储修改标志位(HOTKEYF_),当fmask不包括SEE_MASK_HOTKEY时该项被忽略
  union {
    HANDLE hIcon;//取得对应文件类型的图标的Handle,当fMask不包括SEE_MASK_ICON时该项被忽略
    HANDLE hMonitor;//将文档显示在显示器上的Handle,当fMask不包括SEE_MASK_HMONITOR时该项被忽略
  } DUMMYUNIONNAME;
  HANDLE    hProcess;//指向新启动的程序的句柄。若fMask不设为SEE_MASK_NOCLOSEPROCESS则该项值为NULL。
                     //但若程序没有启动,即使fMask设为SEE_MASK_NOCLOSEPROCESS,该值也仍为NULL。
                     //如果没有新创建进程,也会为空
} SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;

fMask定义

fMask 用于指定结构成员的内容和有效性,可为下列值的组合:

SEE_MASK_DEFAULT (0)默认
SEE_MASK_CLASSNAME 使用 lpClass 参数,如果 SEE_MASK_CLASSKEY 也有效,则用后者
SEE_MASK_CLASSKEY 使用 hkeyClass 参数
SEE_MASK_IDLIST 使用 lpIDList 参数
SEE_MASK_INVOKEIDLIST 使用选定项目的快捷菜单 IContextMenu 接口处理程序
SEE_MASK_ICON 使用 hIcon 给出的菜单,不能与 SEE_MASK_HMONITOR 共用,Vista之后
SEE_MASK_HOTKEY 使用 dwHotKey 参数
SEE_MASK_NOCLOSEPROCESS 如果执行之后需要返回进程句柄,或者等待执行完毕的话,则需要指定该参数,从结构参数意义可以看到 hProcess 和 hInstApp 都依赖该选项
SEE_MASK_CONNECTNETDRV 验证共享并连接到驱动器号
SEE_MASK_NOASYNC 不等待操作完成,直接返回,会创建一个后台线程运行。
SEE_MASK_FLAG_DDEWAIT 弃用,使用 SEE_MASK_NOASYNC
SEE_MASK_DOENVSUBST 环境变量会被展开
SEE_MASK_FLAG_NO_UI 出现错误,不显示错误消息框,比如不会弹出找不到文件之类的窗口,直接返回失败
SEE_MASK_UNICODE UNICODE 程序
SEE_MASK_NO_CONSOLE 继承父进程的控制台,而不是创建新的控制台,与 CREATE_NEW_CONSOLE 相反
SEE_MASK_ASYNCOK 执行在后台线程,调用立即返回
SEE_MASK_NOQUERYCLASSSTORE 弃用
SEE_MASK_HMONITOR 使用 hmonitor,不能与 SEE_MASK_ICON 共存
SEE_MASK_NOZONECHECKS 不执行区域检查
SEE_MASK_WAITFORINPUTIDLE 创建新进程后,等待进程变为空闲状态再返回,超时时间为1分钟
SEE_MASK_FLAG_LOG_USAGE 跟踪应用程序启动次数
SEE_MASK_FLAG_HINST_IS_SITE

lpVerb参数定义如下: 

lpVerb 参数与 ShellExecute 的 lpOperation 参数一致:

edit 用编辑器打开 lpFile 指定的文档,如果 lpFile 不是文档,则会失败
explore 浏览 lpFile 指定的文件夹
find 搜索 lpDirectory 指定的目录
open 打开 lpFile 文件,lpFile 可以是文件或文件夹
print 打印 lpFile,如果 lpFile 不是文档,则函数失败
properties 显示属性
runas 请求以管理员权限运行,比如以管理员权限运行某个exe
NULL 执行默认”open”动作

如果设置了 SEE_MASK_NOCLOSEPROCESS ,调用成功则 hInstApp 返回大于32的值,调用失败会返回: 

SE_ERR_FNF (2) 文件未找到
SE_ERR_PNF (3) 路径未找到
SE_ERR_ACCESSDENIED (5) 拒绝访问
SE_ERR_OOM (8) 内存不足
SE_ERR_DLLNOTFOUND (32) 动态库未找到
SE_ERR_SHARE (26) 无法共享打开的文件
SE_ERR_ASSOCINCOMPLETE (27) 文件关联信息不完整
SE_ERR_DDETIMEOUT (28) 操作超时
SE_ERR_DDEFAIL (29) 操作失败
SE_ERR_DDEBUSY (30) DDE 操作忙
SE_ERR_NOASSOC (31) 文件关联不可用

返回值: 

函数执行成功,返回 TRUE ,否则返回 FALSE ,可使用 GetLastError 获取错误码。

ERROR_FILE_NOT_FOUND 文件不存在
ERROR_PATH_NOT_FOUND 路径不存在
ERROR_DDE_FAIL DDE(动态数据交换)失败
ERROR_NO_ASSOCIATION 未找到与指定文件拓展名关联的应用
ERROR_ACCESS_DENIED 拒绝访问
ERROR_DLL_NOT_FOUND 未找到dll
ERROR_CANCELLED 功能提示用户提供额外信息,但是用户取消请求。
ERROR_NOT_ENOUGH_MEMORY 内存不足
ERROR_SHARING_VIOLATION 发生共享冲突

        可以看到,SHELLEXECUTEINFO结构体里面是包含有HANDLE成员的,也就是说ShellExecuteEx函数可以获得执行的exe进程的HANDLE,那么就可以根据这个来等待并关闭进程,如下所示:

SHELLEXECUTEINFO exeInfo;
	memset(&exeInfo, 0, sizeof(SHELLEXECUTEINFO));

	exeInfo.hwnd = this->m_hWnd;     //设置当前窗口为调用窗口
	exeInfo.cbSize = sizeof(SHELLEXECUTEINFO);    //设置大小
	exeInfo.fMask = SEE_MASK_NOCLOSEPROCESS;    //掩码设置为可以返回HANDLE
	exeInfo.lpVerb = L"open";     //打开操作
	exeInfo.lpFile = L"pdf2txt.exe";    //执行的程序名
	exeInfo.nShow = SW_HIDE;    //后台执行
	exeInfo.lpParameters = FilePath;    //执行的路径
	ShellExecuteEx(&exeInfo);   //调用ShellExecuteEx

	WaitForSingleObject(exeInfo.hProcess, INFINITE);    //阻塞等待进程执行结束
	CloseHandle(exeInfo.hProcess);    //释放HANDLE

         此时运行程序后可以感觉到阻塞,结果也显示正确:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

关于MFC中使用ShellExecute出现的进程冲突问题 的相关文章

  • 代码随想录算法训练营19期第43天

    1049 最后一块石头的重量 II 视频讲解 动态规划之背包问题 这个背包最多能装多少 LeetCode 1049 最后一块石头的重量II 哔哩哔哩 bilibili 代码随想录 初步思路 动态规划 总结 套用01背包 dp j max d

随机推荐