主要是用pipe,<br>演示程序已上传到:http://www.playicq.com/dispdoc.asp?id=1066<br><br>unit ConsoleUnit;<br>{30/4/00<br>This unit demonstrates a GUI application spawning a<br>console application, and capturing the output of the<br>console application to display in the GUI application.<br><br>Any matters arising, questions, comments etc... contact<br><br>Martin Lafferty<br>martinl@prel.co.uk<br><br>Production Robots Engineering Ltd<br>Box 2290, Wimborne, Dorset, BH21 2YY, England.<br><br>Background<br>----------<br>This example is based on a similar thing I wrote some years<br>ago which worked not very well under Win95 and not at all under<br>Windows NT. If you are one of the many people who wrote to me<br>asking me about this, I am sorry it has taken me so long to sort<br>it out. I didn't have a need for it until now, and I have been<br>busy - you know how it is.<br><br>The Win32 SDK has a topic called<br><br>"Creating a Child process with redirected input and output". I tried to<br>use that as a basis for this work but found it very confusing and could<br>not really get it to do what I wanted. The code presented here is really<br>based on information from Richter ("Advanced Windows" ISBN 1-57231-548-2)<br>notably chapters 2 (Kernel Objects) and chapter 3 (Processes)<br><br>Here is an interesting thing that might be bug (but I don't think so)<br><br>Try this on NT:<br>Open TestApp.dpr (simple console app, supplied) and compile<br>Open ConsoleTest.dpr in the Delphi IDE<br>Enter TestApp as command line.<br>You should get an output - testapp should return 0.<br><br>Now without closing down Delphi close ConsoleTest.dpr and reopen TestApp.dpr.<br>Try to compile and you will get a 'Cannot create output file' error - which<br>normally indicates that the EXE image is still loaded, but if you check the<br>process list using the NT Task manager there is no sign of Testapp.exe.<br><br>If you close Delphi, and restart it, you can compile OK.<br><br><br>It would be reasonable to assume that a bug in ConsoleTest.dpr was failing to<br>allow TestApp to terminate properly. I have looked for such a bug, and cannot<br>find anything. If you run ConsoleTest direct from NT (not in the IDE) then the<br>problem is not present. You can compile TestApp.dpr quite happily in the IDE<br>after running the EXE via ConsoleTest running outside the IDE. I am not too<br>sure what is going on here but it seems to be only a problem when TestApp is<br>running as a grandchild of Delphi. If you find out more, let me know.<br>}<br><br><br>interface<br><br>uses<br> Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,<br> StdCtrls, ExtCtrls;<br><br>type<br> TConsoleForm = class(TForm)<br> Output: TListBox;<br> Panel1: TPanel;<br> CmdLineLabel: TLabel;<br> CmdLineEdit: TEdit;<br> BrowseButton: TButton;<br> RunButton: TButton;<br> BrowseDlg: TOpenDialog;<br> procedure RunButtonClick(Sender: TObject);<br> procedure BrowseButtonClick(Sender: TObject);<br> private<br> procedure RunningUpdate(Sender: TObject);<br> public<br> { Public declarations }<br> end;<br><br>var<br> ConsoleForm: TConsoleForm;<br><br>implementation<br><br>{$IFDEF DEBUG}<br>var<br> ReadCount: Integer;<br>{$ENDIF}<br><br>function ExecConsoleApp(CommandLine: String;<br> AppOutput: TStrings; {will receive output of child process}<br> OnNewLine: TNotifyEvent {if assigned called on each new line}<br> ): Cardinal;<br><br>{child process has no input. I have not thought about this.<br>Function returns exit code of child process (normally 0 for no error)<br><br>If the function returns STILL_ACTIVE ($00000103) then the ReadLoop<br>has terminated before the app has finished executing. See comments in body<br>of function<br>}<br><br>const<br> CR = #$0D;<br> LF = #$0A;<br> TerminationWaitTime = 5000;<br><br>var<br> StartupInfo:TStartupInfo;<br> ProcessInfo:TProcessInformation;<br> SecurityAttributes: TSecurityAttributes;<br><br> TempHandle,<br> WriteHandle,<br> ReadHandle: THandle;<br> ReadBuf: array[0..$100] of Char;<br> BytesRead: Cardinal;<br> LineBuf: array[0..$100] of Char;<br> LineBufPtr: Integer;<br> Newline: Boolean;<br> i: Integer;<br><br>procedure OutputLine;<br>begin<br> LineBuf[LineBufPtr]:= #0;<br> with AppOutput do<br> if Newline then<br> Add(LineBuf)<br> else<br> Strings[Count-1]:= LineBuf; {should never happen with count = 0}<br> Newline:= false;<br> LineBufPtr:= 0;<br> if Assigned(OnNewLine) then<br> OnNewLine(AppOutput) {there is no reasonable justification for passing<br> AppOutput as self, but I don't have anything else}<br>end;<br><br>begin<br> FillChar(StartupInfo,SizeOf(StartupInfo), 0);<br> FillChar(ReadBuf, SizeOf(ReadBuf), 0);<br> FillChar(SecurityAttributes, SizeOf(SecurityAttributes), 0);<br>{$IFDEF DEBUG}<br> ReadCount:= 0;<br>{$ENDIF}<br> LineBufPtr:= 0;<br> Newline:= true;<br> with SecurityAttributes do<br> begin<br> nLength:= Sizeof(SecurityAttributes);<br> bInheritHandle:= true<br> end;<br> if not CreatePipe(ReadHandle, WriteHandle, @SecurityAttributes, 0) then<br> RaiseLastWin32Error;<br> {create a pipe to act as StdOut for the child. The write end will need<br> to be inherited by the child process}<br><br> try<br> {Read end should not be inherited by child process}<br> if Win32Platform = VER_PLATFORM_WIN32_NT then<br> begin<br> if not SetHandleInformation(ReadHandle, HANDLE_FLAG_INHERIT, 0) then<br> RaiseLastWin32Error<br> end else<br> begin<br> {SetHandleInformation does not work under Window95, so we<br> have to make a copy then close the original}<br> if not DuplicateHandle(GetCurrentProcess, ReadHandle,<br> GetCurrentProcess, @TempHandle, 0, True, DUPLICATE_SAME_ACCESS) then<br> RaiseLastWin32Error;<br> CloseHandle(ReadHandle);<br> ReadHandle:= TempHandle<br> end;<br><br> with StartupInfo do<br> begin<br> cb:= SizeOf(StartupInfo);<br> dwFlags:= STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;<br> wShowWindow:= SW_HIDE;<br> hStdOutput:= WriteHandle<br> end;<br> {Initialise the startup info. I suspect that it is only safe to pass<br> WriteHandle as hStdOutput because we are going to make sure that the<br> child inherits it. This is not documented anywhere, but I am reasonably<br> sure it is correct. We should not have to use STARTF_USESHOWWINDOW and<br> wShowWindow:= SW_HIDE as we are going to tell CreateProcess not to<br> bother with an output window, but it would appear that Windows 95<br> ignores the CREATE_NO_WINDOW flag. Fair enough - it is not in the SDK<br> documentation (I got it out of Richter). CREATE_NO_WINDOW definately works<br> under NT 4.0, so it is worth doing}<br><br> if not CreateProcess(nil, PChar(CommandLine), nil, nil,<br> true, {inherit kernel object handles from parent}<br> NORMAL_PRIORITY_CLASS or CREATE_NO_WINDOW,<br> {DETACHED_PROCESS relevant for Console parent only<br> No need to create an output window - it would be<br> blank anyway}<br> nil,<br> nil,<br> StartupInfo,<br> ProcessInfo) then<br> RaiseLastWin32Error;<br><br> CloseHandle(ProcessInfo.hThread);<br> {not interested in threadhandle - close it}<br><br> CloseHandle(WriteHandle);<br> {close our copy of Write handle - Child has its own copy now. It is important<br> to close ours, otherwise ReadFile may not return when child closes its<br> StdOutput - this is the mechanism by which the following loop detects the<br> termination of the child process: it does not poll GetExitCodeProcess.<br><br> The clue to this behaviour is in the 'Anonymous Pipes' topic of Win32.hlp - quote<br><br> "To read from the pipe, a process uses the read handle in a call to the<br> ReadFile function. When a write operation of any number of bytes completes,<br> the ReadFile call returns. The ReadFile call also returns when all handles<br> to the write end of the pipe have been closed or if any errors occur before<br> the read operation completes normally."<br><br> On this basis (and going somewhat beyond that stated above) I have assumed that<br> ReadFile will return TRUE when a write is completed at the other end of the pipe<br> and will return FALSE when the write handle is closed at the other end.<br><br> I have also assumed that ReadFile will return when its output buffer is full<br> regardless of the size of the write at the other end.<br><br> I have tested all these assumptions as best I can (under NT 4)}<br><br> try<br> while ReadFile(ReadHandle, ReadBuf, SizeOf(ReadBuf), BytesRead, nil) do<br> begin<br> {There are much more efficient ways of doing this: we don't really<br> need two buffers, but we do need to scan for CR & LF &&&}<br>{$IFDEF Debug}<br> Inc(ReadCount);<br>{$ENDIF}<br> for i:= 0 to BytesRead - 1 do<br> begin<br> if (ReadBuf = LF) then<br> begin<br> Newline:= true<br> end else<br> if (ReadBuf = CR) then<br> begin<br> OutputLine<br> end else<br> begin<br> LineBuf[LineBufPtr]:= ReadBuf;<br> Inc(LineBufPtr);<br> if LineBufPtr >= (SizeOf(LineBuf) - 1) then {line too long - force a break}<br> begin<br> Newline:= true;<br> OutputLine<br> end<br> end<br> end<br> end;<br> WaitForSingleObject(ProcessInfo.hProcess, TerminationWaitTime);<br> {The child process may have closed its stdoutput handle but not yet<br> terminated, so will wait for up to five seconds to it a chance to<br> terminate. If it has not done so after this time, then we will end<br> up returning STILL_ACTIVE ($103)<br><br> If you don't care about the exit code of the process, then you don't<br> need this wait: having said that, unless the child process has a<br> particularly longwinded cleanup routine, the wait will be very short<br> in any event.<br> I recommend you leave this wait in unless you have an intimate<br> understanding of the child process you are spawining and are sure you<br> don't want to wait for it}<br><br> GetExitCodeProcess(ProcessInfo.hProcess, Result);<br> OutputLine {flush the line buffer}<br> finally<br> CloseHandle(ProcessInfo.hProcess)<br> end<br> finally<br> CloseHandle(ReadHandle);<br> end<br>end;<br><br>{$R *.DFM}<br><br>procedure TConsoleForm.RunButtonClick(Sender: TObject);<br>var<br> s: String;<br> CAExitCode: Integer;<br>begin<br> s:= CmdLineEdit.Text;<br> Output.Items.Clear;<br> Output.Items.Add('Executing ' + s);<br> CAExitCode:= ExecConsoleApp(s, Output.Items, RunningUpdate);<br>{$IFDEF DEBUG}<br> Output.Items.Add(Format('%s returned %d (rc = %d)', [s, CAExitCode, ReadCount]))<br>{$ELSE}<br> Output.Items.Add(Format('%s returned %d', [s, CAExitCode]))<br>{$ENDIF}<br>end;<br><br>procedure TConsoleForm.RunningUpdate(Sender: TObject);<br>begin<br> Output.Update {flush paint messages to show progress}<br>end;<br><br>procedure TConsoleForm.BrowseButtonClick(Sender: TObject);<br>begin<br> if BrowseDlg.Execute then<br> CmdLineEdit.Text:= BrowseDlg.Filename<br>end;<br><br>end.<br><br><br>