65.9K
CodeProject 正在变化。 阅读更多。
Home

MFC 进程类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (13投票s)

2001 年 10 月 16 日

9分钟阅读

viewsIcon

241228

downloadIcon

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;

hreadFromChildProcess 类的成员变量,由工作线程用于从子进程读取。其他两个句柄(如下)无需在此函数之外存在。

     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(常量字符串)。因此,我们不能使用 commandCString 值作为参数。特别是,缓冲区必须是可修改的。我们使用 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);

如果子进程终止,句柄将被关闭,但实际上关闭的是子进程中的句柄。hwriteToParenthwriteToParent2 句柄保持有效。因此,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

许可证

本文没有明确的许可证,但可能包含文章文本或下载文件本身的使用条款。如有疑问,请通过下方的讨论区与作者联系。作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.