søndag den 12. februar 2012

Et par gemte eller glemte funktioner

Denne gang vil jeg blogge om et par glemte eller gemte funktioner. Den ene hedder AttatchToConsole og den anden er IsDebuggerPresent.

Lad mig starte med den sidste først. Når jeg udvikler programmer har jeg tit brug for at teste noget mange gange under udviklingen. Typisk er der tale om en eller flere vinduer, hvori der skal udfyldes en 3-4 felter eller mere, klikkes på nogle knapper, aktiveres et par actions - eller på anden måde gøres forskelligt, inden jeg kommer til det, jeg udvikler på.

I den forbindelse er det meget almindeligt, at man bygger genveje ind i programmet. Jeg ved fra min tid på arbejdsmarkedet som Delphi programmør, at mange benytter sig af compiler direktiver fx {$IFDEF DEBUG}, men det giver det lille problem at programmer typisk opfører sig forskelligt, alt efter om det bliver afviklet med en compiler tilknyttet eller ej. Min erfaring siger mig at det er godt at kunne teste programmet uden for debuggeren. Så derfor bruger jeg altid funktionen IsDebuggerPresent.

Fra Delphi XE og frem kan man finde funktionen IsDebuggerPresent i Windows.pas, men ellers er den meget nemt at implementere selv, idet den ligger i kernel32.dll.

function IsDebuggerPresent: Bool stdcall; external 'kernel32.dll';

Som det fremgår af dokumentationen skal man som minimum bruge Wnindows 2000 før funktionen er tilgængelig. Skal du bruge den på en Windows 9x eller ME, skal du kode den selv:

//REF : http://en.wikipedia.org/wiki/Win32_Thread_Information_Block
function IsDebuggerAttached: Bool;
asm
  mov eax, fs:[$18]
  mov eax, dword ptr [eax + $30]  // Get the TIB's linear address
  mov eax, dword ptr [eax]        // Get the whole DWORD
  and eax, $00010000              
  // The 3rd byte is the byte we really 
  //need to check for the presence of a debugger. (bit 16)
end;

Delphi (fx via dos-prompt), og derefter vælge "attach to process" i Delphi (menuen Run --> Attach To Process) for at få programmet til at standse på et breakpoint.

Det løser jeg ved at bruge et uendeligt loop, som bliver ved med at gå i ring indtil debuggeren er attached:

   while not IsDebuggerPresent do
Sleep(50);

En anden lidt gemt eller glemt funktion hedder AttatchToConsole.

function AttachConsole(dwProcessId: DWORD): Bool; stdcall; external KERNEL32 name 'AttachConsole';

Hvis et program kan startes med en masse forskellige opstartsparametre, kan det være nemmere at starte programmet fra en dos-prompt. Til gengæld kan det være rart, at få vist en hjælpetekst, der beskriver de mulige parametre. AttatchToConsole kan bruges til at skrive en sådan hjælpetekst i dos-vinduet - uden at resten af programmet behøver at være et desideret console-applikationer.

En dos-prompt bruger CodePage 850, så alt hvad der skal ud i en sådan, skal konveteres til CP 850. I et ansicode miljø (Pre Delphi 2009) skal man gøre det selv, men i et Unicode miljø kan man bare definere en AnsiString med en bestemt codepage. Så det første jeg gør, er at oprette en datatype.

Det følgende kode skal indsættes i DPR filen:

Type
  DosString = {$IF CompilerVersion <= 18.5}AnsiString{$ELSE} type AnsiString(850){$IFEND};

Så skal der skriver en hjælpetekst. Jeg har valgt den følgende:

function GetHelpText: String;
var
  Buffer: TStringList;
begin
  Buffer := TStringList.Create;
  Buffer.Add('');
  Buffer.Add('');
  Buffer.Add('%s opretter forbindelse til en MSSQL Server. Programmet kan kaldes med nedenstående parametre. ');
  Buffer.Add('Udelades et af parametrene, eller de angivede værdier ugyldige, vises GUI og værdierne skal indtastes manuelt.');
  Buffer.Add('');
  Buffer.Add('');
  Buffer.Add('%s [/MSSQL: ]');
  Buffer.Add('%s [/LOG: ] /MSSQL: "Provider=SQLOLEDB.1;Password=;Persist Security Info=True;User ID=;Initial Catalog=;Data Source="');
Buffer.Add('/LOG: Navn og sti på logfil, hvori log skal skrives. Hvis dette parameter udelades, genererer programmet selv et unikt logfilnavn.');
  Buffer.Add('');
  Buffer.Add('');
  Buffer.Add('Eksempler:');
  Buffer.Add('');
  Buffer.Add('  %s /MSSQL:"Provider=SQLOLEDB.1;Password=pass1234;Persist Security Info=True;User ID=sa;Initial Catalog=P09999x;Data Source=." /LOG:"d:\Logfil.txt"');
  Buffer.Add('');
  Buffer.Add('  %s /MSSQL:"Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password=pass1234;Initial Catalog=IMDB_JSON_SERVER;Data Source=Dellserver" /LOG:"d:\Logfil.txt"');
  Buffer.Add('');
  Buffer.Add('  %s (GUI vises og værdier skal vælges manuelt)');
  Buffer.Add('');
  Buffer.Add('Tryk på ENTER .. .');
  Result := Buffer.Text;
  FreeAndNil(Buffer);
end;

Til sidst skal det bare kaldes:

var
  s: DosString;

begin
  if (ParamCount = 1) and (Trim(ParamStr(1)) = '/?') and (AttachConsole(DWORD(-1))) then
    try
      s := DosString(StringReplace(GetHelpText, '%s', ExtractFileName(ParamStr(0)), [rfReplaceAll]));
{$IF CompilerVersion <= 18.5} CharToOemA(PAnsiChar(s), PAnsiChar(s)); {$IFEND}
      WriteLN(s);
    finally
      FreeConsole;
      Halt(0);
    end;

  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Bemærk at i Delphi 2007 og tidligere er man nødt til selv at konvertere sin AnsiString til CodePage 850. Det gør jeg ved hjælp af CharToOemA-funktionen. Således har jeg opnået første delmål: Hvis man kalder sit program med "/?" fra en dos-prompt vil man få en hjælpetekst ud på skærmen:


Hvis der ikke medgives nogle opstartsparametre, startes programmet op som en almindelig GUI-applikation.

Som det ses, har jeg i mit eksempel, valgt at give en connectionstring med som det ene parameter samt et log-parameter hvortil programmet skal gemme loggen.

Så jeg kunne fx kalde mit program med de følgende parametre:
/MSSQL:"Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password=harlov;Initial Catalog=IMDB_JSON_SERVER;Data Source=Dellserver" /LOG:"C:\Test\Logfil.txt"

Jeg vil ikke gå i detaljer med at parse en connectionstring etc. Det kan alt sammen ses i det komplette eksempel som sædvanen tro ligger til download her. Jeg vil bare her helt kort fortælle, hvordan man så får fat på de parametre, som er angivet til programmet, og det er her Commandline Parseren kommer ind i billedet. Som sagt vil jeg ikke gå i detaljer med den, men blot her liste de offentlige funktioner, der er i parseren:

function Parameters: TStringList; overload;
function Parameters(const aCommandLine: string; const KeepOriginal: Boolean = true): TStringList; overload;

function ParameterByIndex(const Index: Integer): string;
function ParameterByName(const Name: string): string;
function ParameterByNameDef(const Name: string; Default: string): string; overload;
function ParameterByNameDef(const Name: string; Default: Integer): Integer; overload;
function ParameterByNameDef(const Name: string; Default: Boolean): Boolean; overload;

function ParameterArgChars(AArgChars: string): string;

For at vende tilbage til mit program, så gør jeg følgende, når jeg vil teste om den angivede connectionstring er korrekt:

- Stærkt forenklet, for eksemplets skyld - 

ADOConnection1.ConnectionString := ParameterByName('MSSQL');
  try
    ADOConnection1.Open;
    lSQLConnecting.Caption := 'Forbindelse til databasen opnået';
  except
    on e: Exception do
      lSQLConnecting.Caption := 'Forbindelse til databasen IKKE opnået, årsag: ' + e.Message;
  end;


Det færdige resultat ser sådan ud:


For at se hvordan, jeg parser en connectionstring, skriver i loggen, etc., så kig i de de komplette eksempel som sædvanen tro ligger til download her.

Til slut vil jeg gerne reklamere for ERFA-mødet i DAPUG-gruppen den onsdag d. 7. marts 2012. Det komplette program kan ses her på http://www.dapug.dk/ (åbner i et nyt vindue). Er det nogen der er nysgerrige efter at se giraffen (mig), er jeg om eftermiddagen vært ved et 2-timers seminar om REST.

Jens Borrisholt

Ingen kommentarer:

Send en kommentar