Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Chris Miller on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Use Pipes (or receive input and output from a console)

How To

Use Pipes (or receive input and output from a console)

by  Glenn9999  Posted    (Edited  )
The question that is usually asked is: How do I run a command-line process, capture the output to a form, and provide input from a form?

Pipes are an OS facility provided to share memory between applications. They are ideal in most cases for directly receiving the output and providing input to a command-line program. But they can be applied to any case where an inter-process communication can be desired.

[link http://msdn.microsoft.com/en-us/library/aa365780%28v=VS.85%29.aspx]Microsoft reference on Pipes[/link]

Complete code will not be posted here, but a downloadable sample will be provided.

Pipes are handled much like files are. You open them, write/read from them and close them. However, they are different in that each pipe has both an input and an output. Both must be assigned and addressed. The analogy of a real pipe can be useful to understand this.

Create a Pipe
To do that you use the [link http://msdn.microsoft.com/en-us/library/aa365152%28VS.85%29.aspx]CreatePipe[/link]. The call looks like this:

Code:
var
Security : TSecurityAttributes;

With Security do
   begin
     nlength := SizeOf(TSecurityAttributes) ;
     binherithandle := true;
     lpsecuritydescriptor := nil;
   end;

CreatePipe(InputPipeRead, InputPipeWrite, @Security, 0);

The security attributes block usually must be present for this to work, and seems to generally work well as listed.
InputPipeRead and InputPipeWrite are the two ends of the pipe and these handles are used to address the pipe.

Read and Write from a Pipe
The standard processes used to write or read a file are also used to read from and write to a pipe. These are [link http://msdn.microsoft.com/en-us/library/aa365467%28VS.85%29.aspx]ReadFile[/link] and [link http://msdn.microsoft.com/en-us/library/aa365747%28VS.85%29.aspx]WriteFile[/link]. These work much like blockread or blockwrite.

Code:
WriteFile(OutputPipe, Instring[1], Length(Instring), byteswritten, nil);

Read example later. Full examples are in the file below, along with other places.

Do I have something in the pipe line?
The question of reading a pipe has this question first and foremost, since there is always the possibility of having nothing to read. This is solved by the [link http://msdn.microsoft.com/en-us/library/aa365779%28VS.85%29.aspx]PeekNamedPipe[/link] function. It checks the pipe and returns information related to its state. Attempting to read an empty pipe will cause a program crash, so using this function is a necessity.

Code:
PeekNamedPipe(InputPipe, nil, PipeSize, @BytesRead, @PipeSize, @BytesRem);
if BytesRead > 0 then
  ReadFile(InputPipe, TextBuffer, pipesize, bytesread, nil);

While you can control how fast you can send input to a program, you can't control how fast a program sends its output to you (for example, issuing dir /s sends a lot of text very fast). So define a big enough buffer to receive any output you get as quickly as possible (so you don't lose any), since this is a realtime process and pipes are limited in their buffer size. I used 32K and it seemed to work well in all my testing.

Closing a Pipe
As with any resource, you have to close it when you are done with it. With pipes you have to close both ends.

Code:
CloseHandle(InputPipeRead);
CloseHandle(InputPipeWrite);

Hooking up the pipeline
Now that we have the basics of pipes down, the question now comes of how one is hooked up to a command-line process in order to route the input and output through my form. In a CreateProcess call, you do this by specifying one side of the pipe for each of the standard console handles.

Code:
start.hStdInput := InputPipeRead;
start.hStdOutput := OutputPipeWrite;
start.hStdError :=  ErrorPipeWrite;

Now this part gets conceptually confusing. From your program's perspective you WRITE input and READ output - I used these handle names in this order as a mnemonic to aid the concept.

From the perspective of the command-line app, StdInput is what is read from, StdOutput is what is written to. StdError is written when any error messages occur (for example, "'xxxx' is not recognized as an internal or external command,
operable program or batch file."
) - you might not have these in directly calling a command-line program, but it is always safe to define it.

Real-Time Process
Since piping from a command-line program is a real-time process, you will need to handle any significant inputs or outputs within a thread process. An illustration is in the sample using TThread, but any method will work.

Full Sample Source Is Below
Code:
unit mcunit;

{ written by Glenn9999 @ tek-tips.com.  Posted here 6/21/2011 }
interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  monitor = class(TThread)  // pipe monitoring thread for console output
  private
    TextString: String;
    procedure UpdateCaption;
  protected
    procedure Execute; override;
  end;
  TForm1 = class(TForm)
    CommandText: TMemo;
    CommandRun: TComboBox;
    Button2: TButton;
    SaveDialog1: TSaveDialog;
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    cmdcount: integer;
  end;

var
  Form1: TForm1;
  InputPipeRead, InputPipeWrite: THandle;
  OutputPipeRead, OutputPipeWrite: THandle;
  ErrorPipeRead, ErrorPipeWrite: THandle;
  ProcessInfo : TProcessInformation;
  myThread: monitor;

implementation

{$R *.DFM}

procedure WritePipeOut(OutputPipe: THandle; InString: string);
// writes Instring to the pipe handle described by OutputPipe
  var
    byteswritten: DWord;
  begin
// most console programs require CR/LF after their input.
    InString := InString + #13#10;
    WriteFile(OutputPipe, Instring[1], Length(Instring), byteswritten, nil);
  end;

function ReadPipeInput(InputPipe: THandle; var BytesRem: Integer): String;
  {
    reads console output from InputPipe.  Returns the input in function
    result.  Returns bytes of remaining information to BytesRem
  }
  var
    TextBuffer: array[1..32767] of char;
    TextString: String;
    BytesRead: Integer;
    PipeSize: Integer;
  begin
    Result := '';
    PipeSize := Sizeof(TextBuffer);
    // check if there is something to read in pipe
    PeekNamedPipe(InputPipe, nil, PipeSize, @BytesRead, @PipeSize, @BytesRem);
    if bytesread > 0 then
      begin
        ReadFile(InputPipe, TextBuffer, pipesize, bytesread, nil);
        // a requirement for Windows OS system components
        OemToChar(@TextBuffer, @TextBuffer);
        TextString := String(TextBuffer);
        SetLength(TextString, BytesRead);
        Result := TextString;
      end;
  end;

procedure monitor.Execute;
{ monitor thread execution for console output.  This must be threaded.
   checks the error and output pipes for information every 40 ms, pulls the
   data in and updates the memo on the form with the output }
var
  BytesRem: DWord;
begin
  while not Terminated do
    begin
      // read regular output stream and put on screen.
      TextString := ReadPipeInput(OutputPipeRead, BytesRem);
      if TextString <> '' then
         Synchronize(UpdateCaption);
      // now read error stream and put that on screen.
      TextString := ReadPipeInput(ErrorPipeRead, BytesRem);
      if TextString <> '' then
         Synchronize(UpdateCaption);
      sleep(40);
    end;
end;

procedure monitor.UpdateCaption;
// synchronize procedure for monitor thread - updates memo on form.
begin
  With Form1.CommandText.Lines do
    Add(TextString);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  WritePipeOut(InputPipeWrite, 'EXIT'); // quit the CMD we started
  MyThread.Terminate;
  // close process handles
  CloseHandle(ProcessInfo.hProcess);
  CloseHandle(ProcessInfo.hThread);
  // close pipe handles
  CloseHandle(InputPipeRead);
  CloseHandle(InputPipeWrite);
  CloseHandle(OutputPipeRead);
  CloseHandle(OutputPipeWrite);
  CloseHandle(ErrorPipeRead);
  CloseHandle(ErrorPipeWrite);
end;

procedure TForm1.Button2Click(Sender: TObject);
 { takes the input from the command edit box and processes it }
  var
    UpText: String;
  begin
    UpText := UpperCase(CommandRun.Text);  // done to eliminate case-sensitivity
    if UpText = 'CLR' then        // clear the memo
      begin
        CommandText.Clear;
        WritePipeOut(InputPipeWrite, #13);
      end
    else
    if UpText = 'SAVELOG' then    // save the memo box to a file.
      begin
        if SaveDialog1.Execute then
          begin
            CommandText.Lines.SaveToFile(SaveDialog1.FileName);
            CommandText.Lines.Add('Log file saved.');
          end
        else
          CommandText.Lines.Add('Log file not saved.');
      end
  // expand this, it needs to catch any variation where the command-interpreter
  // is called.  Any different ideas?
    else
    if UpText = 'CMD' then
       inc(cmdcount)
    else
    if UpText = 'COMMAND' then
       inc(cmdcount)
  // terminate app if user types exit, else let alone
    else
    if UpText = 'EXIT' then
      begin
        if cmdcount = 1 then
           Application.Terminate
        else
          dec(cmdcount);
      end
    else
      WritePipeOut(InputPipeWrite, CommandRun.Text);
    CommandRun.Items.Add(CommandRun.Text);
    CommandRun.Text := '';
    CommandRun.SetFocus;
  end;

procedure TForm1.FormCreate(Sender: TObject);
 { upon form creation, this calls the command-interpreter, sets up the three
   pipes to catch input and output, and starts a thread to monitor and show
   the output of the command-interpreter }
  var
    DosApp: String;
    DosSize: Integer;
    Security : TSecurityAttributes;
    start : TStartUpInfo;
  begin
    CommandText.Clear;
    // get COMSPEC variable, this is the path of the command-interpreter
    SetLength(Dosapp, 255);
    DosSize := GetEnvironmentVariable('COMSPEC', @DosApp[1], 255);
    SetLength(Dosapp, DosSize);

  // create pipes
    With Security do
      begin
        nlength := SizeOf(TSecurityAttributes) ;
        binherithandle := true;
        lpsecuritydescriptor := nil;
      end;
    CreatePipe(InputPipeRead, InputPipeWrite, @Security, 0);
    CreatePipe(OutputPipeRead, OutputPipeWrite, @Security, 0);
    CreatePipe(ErrorPipeRead, ErrorPipeWrite, @Security, 0);

  // start command-interpreter
    FillChar(Start,Sizeof(Start),#0) ;
    start.cb := SizeOf(start) ;
    start.hStdInput := InputPipeRead;
    start.hStdOutput := OutputPipeWrite;
    start.hStdError :=  ErrorPipeWrite;
    start.dwFlags := STARTF_USESTDHANDLES + STARTF_USESHOWWINDOW;
    start.wShowWindow := SW_HIDE;
    if CreateProcess(nil, PChar(DosApp), @Security, @Security, true,
               CREATE_NEW_CONSOLE or SYNCHRONIZE,
               nil, nil, start, ProcessInfo) then
      begin
        MyThread := monitor.Create(false);  // start monitor thread
        MyThread.Priority := tpHigher;
      end;
    Button2.Enabled := true;
    cmdcount := 1;
 end;

 end.
Register to rate this FAQ  : BAD 1 2 3 4 5 6 7 8 9 10 GOOD
Please Note: 1 is Bad, 10 is Good :-)

Part and Inventory Search

Back
Top