MFC 进程类






4.87/5 (13投票s)
2001 年 10 月 16 日
9分钟阅读

241228

2647
此类允许您创建一个子进程并接收其输出的通知。
带输出的子进程
出现的一个常见问题是如何启动一个子进程并收集其输出。此类允许您创建一个子进程并接收其输出的通知。
该技术包括让工作线程将消息发布到主 GUI 线程的方法,正如我在关于工作线程的文章中所述。
使用该类
此类调用非常简单。此类设计成您可以真正拥有几个类并发运行,并在需要时对来自各个子类的输出进行排序。
简单用法
void CMyView::OnRun()
{
CString cmd; // the command to execute
cmd = ...;
Process * p = new Process(cmd, this);
if(!p->run())
... failed
}
这会创建一个 Process
对象来执行传入的命令 string
。事件的通知将发布到作为第二个参数传入的窗口。请注意,这不能是 NULL
指针。如果发生任何错误,或者进程完成时,Process
对象将被自动删除。run
方法实际上调用 CreateProcess
API。创建进程后,控制立即返回;它不会等待进程完成。
您需要在接收通知的 CWnd
类中处理两个事件
UPM_LINE
会为接收到的每一行输入发送,并将包含行内容的 CString *
传递给目标窗口。
UPM_FINISHED
通知窗口线程已完成,这允许窗口重新启用控件、菜单等。
这些是已注册的窗口消息。IMPLEMENT_MSG
宏用于在使用的模块中声明它们
IMPLEMENT_MSG(UPM_LINE)
IMPLEMENT_MSG(UPM_FINISHED)
您必须在头文件中为这些声明处理程序
afx_msg LRESULT OnLine(WPARAM, LPARAM)
afx_msg LRESULT OnFinished(WPARAM, LPARAM)
并在 MESSAGE_TABLE
中安装条目
ON_REGISTERED_MESSAGE(UPM_LINE, OnLine)
ON_REGISTERED_MESSAGE(UPM_FINISHED, OnFinished)
典型的处理程序是使用 CListBox
作为日志控件。示例使用简单的 CListBox
,禁用 Sorted
选项(如果输出只是按字母顺序排序,则大部分时间都毫无用处),或者您可以使用更复杂的控件,例如我的日志列表框控件。
LRESULT CMyClass::OnLine(WPARAM wParam, LPARAM)
{
CString * s = (CString *)wParam;
c_Output.AddString(*s);
delete s;
return 0;
}
多进程用法
如果您想并发使用多个进程,则需要区分事件。处理此问题的方法是创建一个唯一的 UINT
来表示一个进程。此 ID 值将随每条消息发送,您必须使用它来确定是哪个子进程生成了该消息。请注意,这是您分配的 ID;它不是进程 ID 或进程句柄。如何整理结果取决于您。只要您有一个子进程的唯一 ID,它就可以是动态生成的或简单的常量。
参考
方法
Process(const CString & command, CWnd * target, UINT id = 0)
const CString & command要执行的命令字符串
CWnd * target通知消息的目标窗口
UINT id进程标识符(应用程序生成),默认为零。
创建
Process
对象并将其初始化为指定的参数。这不会创建系统进程,仅创建进程对象。Process
对象必须始终从堆中分配,因为它将在进程终止时自动销毁。
BOOL run()
创建进程和用于接收其数据的线程。控制立即返回。如果进程和线程创建成功,则返回
TRUE
,否则返回FALSE
。如果返回FALSE
,则Process
对象立即被销毁。如果返回TRUE
,则Process
对象存在,并将一直存在直到进程终止。请注意,进程创建后没有任何方法有意义,因此在调用run
方法后没有理由保留Process
对象指针。
消息
UPM_PROCESS_HANDLE
WPARAM (WPARAM)(HANDLE)与子进程关联的进程句柄
LPARAM (LPARAM)(UINT)
Process
构造函数建立的 id 值LRESULT逻辑上为空,0,始终
UPM_LINE
WPARAM (WPARAM)(CString *)表示从子进程捕获的一行输出的
string
对象。CR 和 LF 已从string
中剥离。消息的接收者负责在不再需要此CString
对象时删除它。LPARAM (LPARAM)(UINT)
Process
构造函数建立的 id 值。LRESULT逻辑上为空,0,始终。
UPM_FINISHED
WPARAM (WPARAM)(DWORD)来自
::GetLastError
的错误代码,表示失败原因,或 0 表示无错误。LPARAM (LPARAM)(UINT)
Process
构造函数建立的 id 值。LRESULT逻辑上为空,0,始终。
在两种情况下发送此消息:工作线程创建失败,因此将不再传递输出;或者从子进程进行的
ReadFile
返回EOF
条件或以ERROR_BROKEN_PIPE
错误终止。
工作原理
操作中最复杂的部分是进程的创建和管道的建立。
Process::run
BOOL Process::run()
{
hreadFromChild = NULL;
hreadFromChild
是 Process
类的成员变量,由工作线程用于从子进程读取。其他两个句柄(如下)无需在此函数之外存在。
HANDLE hwriteToParent = NULL;
HANDLE hwriteToParent2 = NULL;
SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
// inheritable handle
SECURITY_ATTRIBUTES
用于创建可继承的句柄;结构体的最后一个成员设置为 TRUE
,以便句柄可继承。
if(!::CreatePipe(&hreadFromChild, &hwriteToParent, &sa, 0))
{ /* pipe failed */
// ::GetLastError() will reveal the cause
delete this;
return FALSE;
} /* pipe failed */
CreatePipe
操作创建单个单向匿名管道,并返回其读写端的句柄。通过 SECURITY_ATTRIBUTES
,它们是可继承的句柄。
if(!::DuplicateHandle(GetCurrentProcess(), // duplicate from this process
hwriteToParent, // this handle
GetCurrentProcess(), // into this process
&hwriteToParent2, // as this handle
0, // no access flags
// (subsumed by DUPLICATE_SAME_ACCESS)
TRUE, // create inheritable
DUPLICATE_SAME_ACCESS)) // create duplicate access
{ /* duplicate failed */
DWORD err = ::GetLastError();
::CloseHandle(hreadFromChild);
::CloseHandle(hwriteToParent);
::SetLastError(err);
delete this;
return FALSE;
} /* duplicate failed */
将传递给标准输出的标准句柄也将传递给标准错误句柄。许多子进程,包括命令解释器,倾向于在不使用标准错误句柄时关闭它。如果我们为输出和错误传递相同的句柄,当子进程关闭错误句柄时,它必然会关闭输出句柄。这意味着您永远看不到此类进程的输出。通过使用 DuplicateHandle
,我们获得了代表同一流的重复句柄。如果子进程关闭此重复句柄(我们将其作为错误句柄传入),则输出句柄仍然有效。
STARTUPINFO startup;
PROCESS_INFORMATION procinfo;
::ZeroMemory(&startup, sizeof(startup));
startup.cb = sizeof(startup);
startup.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
startup.wShowWindow = SW_HIDE; // hidden console window
startup.hStdInput = NULL; // not used
startup.hStdOutput = hwriteToParent;
startup.hStdError = hwriteToParent2;
在此,我们初始化结构体。请注意,为了遵循良好的编程实践,应首先将结构体清零。标准输入句柄未设置。如果您有一个需要输入的子进程,您应在此处设置它,并使用此代码的变体。您还必须创建一个线程来为输入流提供数据。
// We want a non-inherited read handle. DuplicateHandle with a
// NULL target fixes the read side to be non-inheritable
::DuplicateHandle(::GetCurrentProcess(), // in this process
hreadFromChild, // child read handle
::GetCurrentProcess(), // to this process
NULL, // modify existing handle
0, // flags
FALSE, // not inheritable
DUPLICATE_SAME_ACCESS); // same handle access
这似乎有点奇怪;我们正在创建一个“副本”而不指定目标(第四个参数是 NULL
)。这是一个奇怪的惯用法。我们希望继承输出句柄以便子进程可以发送输出,但我们不想继承该句柄的输入端。DuplicateHandle
的一个效果是,如果目标句柄地址被指定为 NULL
,它会修改输入句柄。通过将可继承特性设置为 FALSE
,该句柄将变为不可继承。
// We need a writeable buffer for the command (silly Windows restriction)
LPTSTR cmd = command.GetBuffer(command.GetLength() + 1);
CreateProcess
调用需要 LPTSTR
,而不是 LPCTSTR
(常量字符串)。因此,我们不能使用 command
的 CString
值作为参数。特别是,缓冲区必须是可修改的。我们使用 GetBuffer
获取一个可修改的缓冲区,并为可能添加到命令行的一个字符留出空间(请阅读 CreateProcess
文档)。
BOOL started = ::CreateProcess(NULL, // command is part of input string
cmd, // (writeable) command string
NULL, // process security
NULL, // thread security
TRUE, // inherit handles flag
0, // flags
NULL, // inherit environment
NULL, // inherit directory
&startup, // STARTUPINFO
&procinfo); // PROCESS_INFORMATION
CreateProcess
调用非常直接。在此我没有进行任何设置来处理任何线程或进程安全选项、环境或目录的修改等。如果您需要这些功能,可以增强 Process
构造函数以提供这些值,并创建成员变量来保存它们。此 CreateProcess
调用与其他实例之间的唯一区别是第五个参数为 TRUE
,表示所有可继承的句柄都将由子进程继承。这就是我们将标准句柄传递给子进程的方式。
command.ReleaseBuffer();
if(!started)
{ /* failed to start */
DWORD err = ::GetLastError(); // preserve across CloseHandle calls
::CloseHandle(hreadFromChild);
::CloseHandle(hwriteToParent);
::CloseHandle(hwriteToParent2);
::SetLastError(err);
target->PostMessage(UPM_FINISHED, (WPARAM)err, (LPARAM)pid);
delete this;
return FALSE;
} /* failed to start */
请注意,如果进程启动失败,目标将收到 UPM_FINISHED
消息,但不会收到 UPM_PROCESS_HANDLE
消息。请注意,WPARAM
是进程创建失败的原因代码。
target->PostMessage(UPM_PROCESS_HANDLE, (WPARAM)procinfo.hProcess, (LPARAM)pid);
PostMessage
调用通知目标窗口进程已启动,并将进程句柄传递进去。我不知道进程句柄有什么用,但似乎是合理的事情。请注意,只有当进程成功启动时才传递此信息。
// Now close the output pipes so we get true EOF/broken pipe
::CloseHandle(hwriteToParent);
::CloseHandle(hwriteToParent2);
如果子进程终止,句柄将被关闭,但实际上关闭的是子进程中的句柄。hwriteToParent
和 hwriteToParent2
句柄保持有效。因此,ReadFile
操作不会收到管道破裂的错误,并且会永远挂起,等待可能拥有活动句柄的其他进程发送数据。通过关闭我们自己的句柄副本,这意味着唯一可用的句柄是子进程的句柄,当它终止时,它们将被隐式关闭。由于这将意味着句柄的最后一个实例已被关闭,管道将被破坏,ReadFile
将收到正确的通知。
// We have to create a listener thread. We create a worker
// thread that handles this
CWinThread * thread = AfxBeginThread(listener, (LPVOID)this);
if(thread == NULL)
{ /* failed */
DWORD err = ::GetLastError();
target->PostMessage(UPM_FINISHED, (WPARAM)err, (LPARAM)pid);
delete this;
return FALSE;
} /* failed */
这会创建一个工作线程来接收子进程的数据。请注意,如果无法创建此线程,则不会有输出到达控制线程窗口。因此,我发送一条 UPM_FINISHED
消息来通知窗口进程已有效终止。请注意,描述故障模式的错误代码会通过 UPM_FINISHED
消息传回。
return TRUE;
} // Process::run
Process::listener
这是第二层方法(请参阅我的线程创建技术);这是在线程和 Process
对象上下文中执行的非静态方法。它读取输入流,该流可能包含多行文本,将行拆分,并将每一行发送给父进程。
#define MAX_LINE_LENGTH 1024
void Process::listener()
{
TCHAR buffer[MAX_LINE_LENGTH + 1];
CString * line;
line = new CString;
DWORD bytesRead;
while(::ReadFile(hreadFromChild, buffer, dim(buffer) - 1, &bytesRead, NULL))
{ /* got data */
if(bytesRead == 0)
break; // EOF condition
buffer[bytesRead] = _T('\0');
// Convert to lines
LPTSTR b = buffer;
while(TRUE)
{ /* convert and send */
LPTSTR p = _tcschr(b, _T('\n'));
if(p == NULL)
{ /* incomplete line */
*line += b;
break; // leave assembly loop
} /* incomplete line */
else
{ /* complete line */
int offset = 0;
if(p - b > 0)
{ /* get rid of \r */
if(p[-1] == _T('\r'))
offset = 1;
} /* get rid of \r */
*line += CString(b, (p - b) - offset);
target->PostMessage(UPM_LINE, (WPARAM)line, (LPARAM)pid);
b = p + 1;
line = new CString;
} /* complete line */
} /* convert and send */
} /* got data */
DWORD err = ::GetLastError();
::CloseHandle(hreadFromChild);
我们现在已完成读取,因此关闭句柄进行清理。否则,程序结束时将留下许多句柄。
if(line->GetLength() > 0)
target->PostMessage(UPM_LINE, (WPARAM)line, (LPARAM)pid);
else
delete line;
上述行会将任何部分构建的行发送到目标窗口。但是,如果字符串中没有内容,CString
对象仍然需要被删除。
DWORD status = 0;
if(err != ERROR_BROKEN_PIPE)
status = err;
正常的 EOF 或 ERROR_BROKEN_PIPE
是有效的终止条件。如果出现任何其他错误,则说明有问题,因此状态将通过 UPM_FINISHED
消息传回。
target->PostMessage(UPM_FINISHED, status, (LPARAM)pid);
delete this;
此最终操作删除 Process
对象。因为否则没有好办法跟踪这一点,所以我选择在这里这样做。这意味着只能使用 Process *
变量来保存 Process
对象;不能声明 Process
变量。
} // Process::listener
这些文章中表达的观点是作者的观点,不代表,也不被微软认可。
历史
- 2001 年 10 月 25 日 - 下载文件已更新
如有关于本文的疑问或评论,请发送邮件至 newcomer@flounder.com。
版权所有 © 1999 保留所有权利
http://www.flounder.com/mvp_tips.htm
许可证
本文没有明确的许可证,但可能包含文章文本或下载文件本身的使用条款。如有疑问,请通过下方的讨论区与作者联系。作者可能使用的许可证列表可以在此处找到。