Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Come eseguire un programma esterno con delphi

Come eseguire un programma esterno con delphi

E-mail Stampa PDF

Se all'interno di una nostra applicazione volessimo inserire una funzione che esegue un comando esterno in delphi potremo procedere in diversi modi:

  • usando la funzione shellexecute
  • creando un processo

Nel primo caso si tratta di una funzione appartenente alla libreria ShellApi, quindi come prima cosa nel nostro sorgente dovremo aggiungere alle librerie usate anche quest'ultima:

uses ShellApi;
 ...

a questo punto possiamo richiamarla all'interno del nostro programma:

ShellExecute(Handle, 'open', 'c:\Windows\notepad.exe', nil, nil,SW_SHOWNORMAL) ;

In questo esempio abbiamo lanciato il classico programmino notepad, ma al suo posto potevamo inserire qualsiasi altro applicativo.

un altro esempio potrebbe essere il passaggio di qualche parametro all'avvio del nostro programma:

ShellExecute(Handle,'open', 'c:\windows\notepad.exe','c:\EsempioText.txt', nil, SW_SHOWNORMAL) ;

in questo caso ho scelto di lanciare notepad facendogli leggere il file "c:\EsempioText.txt".

Per mostrare il contenuto di una directory posso lanciare il seguente comando:

ShellExecute(Handle,'open', 'c:\Download', nil, nil, SW_SHOWNORMAL) ;

Per aprire un file in base alla sua estensione:

ShellExecute(Handle, 'open', 'c:\Documenti\lettera.doc',nil,nil,SW_SHOWNORMAL) ;

In quest'ultimo caso verrà lanciata l'applicazione predisposta a leggere i file ".doc"

Per questo motivo se volessimo connetterci ad un sito web useremo questo comando:

ShellExecute(Handle, 'open', 'http://www.compago.it',nil,nil, SW_SHOWNORMAL) ;

per inviare una mail:

var em_subject, em_body, em_mail : string;
begin
  em_subject := 'This is the subject line';
  em_body := 'Message body text goes here';
 
  em_mail := 'mailto:pippo@compago.it?subject='+em_subject + '&body=' + em_body ;
 
  ShellExecute(Handle,'open',PChar(em_mail), nil, nil, SW_SHOWNORMAL) ;
end;

Per eseguire un programma aspettandone la fine:

// esegue la calcolatrice di windows e apre una finestra// che avvisa quando questo viene terminato
uses ShellApi;
...
 
var
   SEInfo: TShellExecuteInfo;
   ExitCode: DWORD;
   ExecuteFile, ParamString, StartInString: string;
 
begin
  ExecuteFile:='c:\Windows\Calc.exe';
  FillChar(SEInfo, SizeOf(SEInfo), 0) ;
  SEInfo.cbSize := SizeOf(TShellExecuteInfo) ;
  with SEInfo do begin
    fMask := SEE_MASK_NOCLOSEPROCESS;
    Wnd := Application.Handle;
    lpFile := PChar(ExecuteFile) ;
    // ParamString contiene i parametri da passare all'applicazione.
 
    // lpParameters := PChar(ParamString) ;
 
    // StartInString specifica il nome della directory dell'applicazione
    // se omesso verrà usata la directory corrente.
 
    // lpDirectory := PChar(StartInString) ;
    nShow := SW_SHOWNORMAL;
  end;
  if ShellExecuteEx(@SEInfo) then begin
    repeat
      Application.ProcessMessages;
      GetExitCodeProcess(SEInfo.hProcess, ExitCode) ;
    until (ExitCode  STILL_ACTIVE) or Application.Terminated;
    ShowMessage('Calculator terminated') ;
  end
  else ShowMessage('Error starting Calc!') ;
end;
 

Per intercettare l'output di una applicazione di tipo console dovremo creare il processo con la funzione CreateProcess, nella quale dovremo specificare come parametro di tipo  TStartupInfo una struttura dove imposteremo lo standard input e output del programma.
Lo StdInput rappresenta la porta di ingresso dove andremo a inserire i comandi dell'utente e che verranno acquisiti dal programma dos, mentre lo StdOutput riceverà tutti i dati provenienti dal programma e rivolti verso l'utente.
Per completezza ci sarebbe anche un terzo flusso in uscita, ovvero lo StdError che solitamente viene usato dal sistema per comunicare all'utente errori nel programma.
Tutti questi flussi in entrata o in uscita devono essere intercettati o meglio collegati ad un file, il quale potrà essere un comune file su disco oppure in memoria, come ad esempio un pipe.

Nell'esempio successivo viene catturato il flusso in uscita di un programma e conservato in un file temporaneo, il quale verrà letto e il suo contenuto verrà visualizzato in un TMemo.

function TForm1.RunCaptured(const dirName, exeName, cmdLine: string): Boolean;
var
  start: TStartupInfo;
  procInfo: TProcessInformation;
  tmpName: string;
  tmp: Windows.THandle;
  tmpSec: TSecurityAttributes;
  res: TStringList;
  return: Cardinal;
begin
  Result := False;
  try
  { Predispone un file temporaneo }
    tmpName := 'temp.tmp';
    FillChar(tmpSec, SizeOf(tmpSec), #0);
    tmpSec.nLength := SizeOf(tmpSec);
    tmpSec.bInheritHandle := True;
    tmp := Windows.CreateFile(PChar(tmpName),Generic_Write, File_Share_Write,@tmpSec, Create_Always, File_Attribute_Normal, 0);
    try
      { Imposta la struttura SturtupInfo del processo }
      FillChar(start, SizeOf(start), #0);
      start.cb          := SizeOf(start);
      start.hStdOutput  := tmp;  // connette l'output del programma al file temporaneo
      start.dwFlags     := StartF_UseStdHandles or StartF_UseShowWindow;
      start.wShowWindow := SW_Hide;
 
      { Crea il processo }
      if CreateProcess(nil, PChar(exeName + ' ' + cmdLine), nil, nil, True,0, nil, PChar(dirName), start, procInfo) then begin
        { Se la creazione è andata a buon fine:
          imposta la sua classe di priorità }
        SetPriorityClass(procInfo.hProcess, Idle_Priority_Class);
        { Aspetta la conclusione del programma }
        WaitForSingleObject(procInfo.hProcess, Infinite);
        GetExitCodeProcess(procInfo.hProcess, return);
        Result := (return = 0);
        {ripulisce tutto}
        CloseHandle(procInfo.hThread);
        CloseHandle(procInfo.hProcess);
        Windows.CloseHandle(tmp);
 
        { Legge il file prodotto e lo visualizza in un memo }
        res := TStringList.Create;
        try
          res.LoadFromFile(tmpName);
          Memo1.Lines.AddStrings(res);
        finally
          res.Free;
        end;
        Windows.DeleteFile(PChar(tmpName));
      end
      else begin
        Application.MessageBox(PChar(SysErrorMessage(GetLastError())),'RunCaptured Error', MB_OK);
      end;
    except
      Windows.CloseHandle(tmp);
      Windows.DeleteFile(PChar(tmpName));
      raise;
    end;
  finally
  end;
end;
 

Esempio di utilizzo:

procedure TForm1.Button1Click(Sender: TObject);
begin
  RunCaptured('C:\', 'cmd.exe', '/c dir');
end;

Questo ci permetterà di catturare l'output di un programma una volta che questo abbia terminato la sua funzione. In particolare è stata lanciata una console di comando (cmd.exe) con l'opzione "/c" la quale esegue il successivo comando e poi termina l'applicazione.
Se avessimo voluto leggere l'output del programma durante la sua esecuzione non avremo potuto usare un file normale, dato che questo sarebbe stato impegnato in scrittura dal programma e quindi ci sarebbe stato proibito l'accesso in lettura.
Per questo motivo avremo potuto usare un pipe anonimo, che ha due punti di accesso, uno in scrittura ed uno in lettura, e quindi permetterà di leggere in tempo reale quello che nello stesso pipe verrà scritto.
Se poi occorre interagire con l'applicazione, allora occorre connettere un secondo pipe al suo StdInput, dove potremo scrivere i dati da inviare all'applicazione.
Naturalmente interagire crea altri problemi, come ad esempio non è possibile aspettare che il programma sia terminato, ma dovremo aspettare un determinato periodo di tempo abbastanza lungo da permettere all'applicazione di scrivere l'output e sufficientemente breve da non compromettere l'interattività. Questo dipenderà fondamentalmente dal tipo di applicazione con cui avremo a che fare.

Nell'esempio successivo verrà utilizzato l'applicazione dos "plink.exe" che permette di connettersi ad un server ssh tramite riga di comando.
I parametri da fornire sono lo username da usare e l'indirizzo del server, eventualmente anche la password e la porta su quale il server deve ricevere la connessione. Esempio:

plink.exe nomeutente@miodominio.it -P 4455 -pw miapassword

Una volta connesso ed effettuato il login correttamente sarà possibile lanciare un comando e poi eseguire il logout.

function TForm1.SendSshCmd: Boolean;
var
  start: TStartupInfo;
  procInfo: TProcessInformation;
  return: Cardinal;
  AppRunning : DWord ;
  Security : TSecurityAttributes;
  StdInReadPipe,StdInWritePipe,StdOutReadPipe,StdOutWritePipe : THandle;
  buffer:pchar;
  BytesRead,BytesWrite:cardinal;
  cmdApp:string;
begin
  cmdapp:='plink.exe nomeutente@miodominio.it -P 4455 -pw miapassword';
  Result := False;
  try
    Security.nlength := SizeOf(TSecurityAttributes) ;
    Security.binherithandle := true;
    Security.lpsecuritydescriptor := nil;
    //crea i due pipe uno per l'output e uno per l'input
    Createpipe(StdInReadPipe, StdInWritePipe, @Security, 0);
    Createpipe(StdOutReadPipe, StdOutWritePipe, @Security, 0);
    try
      FillChar(start, SizeOf(start), #0);
      start.cb          := SizeOf(start);
      //connette i pipe all'output e all'input
      start.hStdOutput  := StdOutWritePipe;
      start.hStdError   := StdOutWritePipe;
      start.hStdInput   := StdInReadPipe;
      start.dwFlags     := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
      start.wShowWindow := SW_HIDE;
      { Crea il processo dell'applicazione dos }
      if CreateProcess(nil, PChar(cmdApp), nil, nil, True,0, nil, nil, start, procInfo) then begin
        SetPriorityClass(procInfo.hProcess, Idle_Priority_Class);
        Buffer := AllocMem(200 + 1) ;
        repeat
          //Aspetta un secondo in modo che il login sia completato
          AppRunning:=WaitForSingleObject(procInfo.hProcess, 1000);
 
          //Legge l'output
          repeat
            BytesRead := 0;
            ReadFile(StdOutReadPipe,Buffer[0], 200,BytesRead,nil) ;
            Buffer[BytesRead]:= #0;
            OemToAnsi(Buffer,Buffer) ;
            Memo.Text := Memo.text + String(Buffer) ;
          until (BytesRead < 200) ;
 
          //Se tutto è andato bene impartisce il comando
          if pos('nomeutente@',Memo.Lines[Memo.Lines.Count-1])>=0 then begin
            WriteFile(StdInWritePipe,'sudo ./prova.sh'#$0D#$0A,18,BytesWrite,nil);
          end;
          //Aspetta ancora un secondo e invia la password
          AppRunning:=WaitForSingleObject(procInfo.hProcess, 1000);
          WriteFile(StdInWritePipe,'miapassword'#$0D#$0A,12,BytesWrite,nil);
          //Aspetta ancora un secondo e poi si disconnette
          AppRunning:=WaitForSingleObject(procInfo.hProcess, 1000);
          WriteFile(StdInWritePipe,'exit'#$0D#$0A,6,BytesWrite,nil);
        until (AppRunning <> WAIT_TIMEOUT) or(Apprunning <> WAIT_FAILED)or(Apprunning <> WAIT_ABANDONED);
 
        GetExitCodeProcess(procInfo.hProcess, return);
        Result := (return = 0);
        CloseHandle(procInfo.hThread);
        CloseHandle(procInfo.hProcess);
        FreeMem(Buffer) ;
        CloseHandle(StdInReadPipe);
        CloseHandle(StdInWritePipe);
        CloseHandle(StdOutReadPipe);
        CloseHandle(StdOutWritePipe);
      end
      else begin
        Application.MessageBox(PChar(SysErrorMessage(GetLastError())),'SendSshCmd Error', MB_OK);
      end;
    except
      CloseHandle(StdInReadPipe);
      CloseHandle(StdInWritePipe);
      CloseHandle(StdOutReadPipe);
      CloseHandle(StdOutWritePipe);
      raise;
    end;
  finally
  end;
end;

Una cosa da sottolineare è la modalità con la quale sono stati connessi i due pipe, infatti quello relativo allo StdOutput deve essere utilizzato in scrittura dall'applicazione dos e letto da noi, mentre per quello connesso allo SdtInput avviene l'opposto, quindi attenzione anche a questi dettagli.
Aggiungo una ulteriore versione di una procedura per catturare l'output di una console e come inserirla in un memo:

procedure TFMainForm.RunDosInMemo(const DosApp: String; AMemo: TRichEdit);
const
  ReadBuffer = 2400;
var
  Security : TSecurityAttributes;
  StdInPipeR, StdInPipeW : THandle;
  StdOutPipeR, StdOutPipeW : THandle;
  StartInfo : TStartUpInfo;
  ProcessInfo : TProcessInformation;
  Buffer : PByte;
  BytesAvailable, BytesRead : DWord;
  sDosApp: String;
  sData: RawByteString;
begin
  sDosApp := DosApp;
  UniqueString(sDosApp);
 
  with Security do begin
    nLength := SizeOf(TSecurityAttributes);
    bInheritHandle := True;
    lpSecurityDescriptor := nil;
  end;
 
   if CreatePipe(StdInPipeR, StdInPipeW, @Security, 0) then
     try
       SetHandleInformation(StdInPipeW, HANDLE_FLAG_INHERIT, 0);
       if CreatePipe(StdOutPipeR, StdOutPipeW, @Security, 0) then
         try
           SetHandleInformation(StdOutPipeR, HANDLE_FLAG_INHERIT, 0);
           GetMem(Buffer, ReadBuffer);
           try
             ZeroMemory(@StartInfo, SizeOf(StartInfo));
             StartInfo.cb := SizeOf(StartInfo);
             StartInfo.hStdOutput := StdOutPipeW;
             StartInfo.hStdInput := StdInPipeR;
             StartInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
             StartInfo.wShowWindow := SW_HIDE;
 
             if CreateProcess(nil,PChar(sDosApp),nil,nil,True,NORMAL_PRIORITY_CLASS,nil,nil,StartInfo,ProcessInfo) then
               try
                 while WaitForSingleObject(ProcessInfo.hProcess, 500) <> WAIT_TIMEOUT do
                   Application.ProcessMessages;
                 //controlla se nel pipe vi sono dati da leggere
                 while PeekNamedPipe(StdOutPipeR, nil, 0, nil, BytesAvailable, nil) do begin
                   //se non c'è nulla ferma il ciclo
                   if BytesAvailable < 1 then 
                     Break;
                   //imposta il limite dei byte da leggere
                   if BytesAvailable > ReadBuffer then 
                     BytesAvailable := ReadBuffer;
                   //legge i dati
                   if not ReadFile(StdOutPipeR,Buffer[0],BytesAvailable,BytesRead,nil) then 
                     Break;
                   //inseriscei dati in una raw string
                   SetString(sData, PAnsiChar(Buffer), BytesRead);
                   // assigna la codifica (codepage) per i dati letti:
                   // 0 per default Ansi, 1252 o 20157 per ASCII, 1200 per Unicode, etc...
                   SetCodePage(sData, 0);
                   // Inserisce in maniera un po' particolare ma veloce il testo nel memo
                   AMemo.SelStart := AMemo.GetTextLen;
                   AMemo.SelLength := 0;
                   AMemo.SelText := sData;
                 end;
               finally
                 CloseHandle(ProcessInfo.hThread);
                 CloseHandle(ProcessInfo.hProcess);
               end;
           finally
             FreeMem(Buffer);
           end;
         finally
           CloseHandle(StdOutPipeR);
           CloseHandle(StdOutPipeW);
         end;
     finally
       CloseHandle(StdInPipeR);
       CloseHandle(StdInPipeW);
     end;
 end;
 
 

La parte che differisce dalle altre è il fatto di usare i pipe controllando se vi sono dei dati da leggere prima di eseguire la lettura, tramite la funzione PeekNamedPipe.

Un'altra particolarità è la gestione della codifica dell'output, per cui una string normale (AnsiString) viene interpretata a seconda della codifica con la funzione:

procedure SetCodePage(var S: AnsiString; CodePage: Word; Convert: Boolean);

che è stata introdotta dalla versione 2009 per aggiungere il supporto ai caratteri Unicode.

Ultimo aggiornamento ( Domenica 12 Dicembre 2010 20:25 )  
Loading

Login




Chiudi