I am at a loss here. For some weeks now I tried to get a Kerberos ticket with my Delphi application.
I consulted:
How can I get a Kerberos ticket with Delphi? (I honestly don't see why the bounty was rewarded, btw)
Every page around https://msdn.microsoft.com/en-us/library/aa374713%28v=vs.85%29.aspx
Most importantly: https://msdn.microsoft.com/en-us/library/ee498143.aspx
The last page links to example code that I converted to Delphi - I think. On a form I dropped a TWSocket out of Overbytes Socket components and named it mySocket. Rest is done in code. Problem is, that I seem to get the Kerberos TGT, but can't get the Ticket itself through the UDP connection. The Server just wont answer. I also feel there is something fundamentally wrong here, with the break of media during the communication with the server. How come I can use API to get the TGT but have to switch to UDP to get the ticket itself?
Maybe a good start to discuss this is to ignore the code first and tell me if the way I'm going is right or not. Here are my steps:
Call InitSecurityInterface to get the SecurityFunctionTable
Call QuerySecurityPackageInfo for the Kerberos Package to obtain max message size
Call AcquireCredentialsHandle for Kerberos package
Call InitializeSecurityContext with above received CredentialsHandle and the KerberosServer. Receiving some message, that may contain a KerbTicket a Kerb TGT or anything inbetween
Depending on the result of InitializeSecurityContext either use received KerbTicket or open UDP connection to KerbServer to send received buffer from step 4
Use answer message as parameter for new call of InitilizeSecurityContext
Repeat from Step 4 until result is SEC_E_OK
Have I understood correctly? If so, please consider reading my implementation to find my mistake. If not, please explain how it's really done.
Here is the code:
unit ukerber;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
IdSSPI, IdAuthenticationSSPI, Vcl.StdCtrls, OverbyteIcsWndControl,
OverbyteIcsWSocket;
const
krbServer: PAnsiChar = 'krbtgt/mydomain.int';
type
TForm2 = class(TForm)
Button1: TButton;
Edit1: TEdit;
Button2: TButton;
mySocket: TWSocket;
procedure Button1Click(Sender: TObject);
procedure mySocketDataAvailable(Sender: TObject; ErrCode: Word);
function InitPackage(var maxMessage: Cardinal): Boolean;
function SendUDPMessage(outBuf: Pointer; outsize: Cardinal): Boolean;
function GenClientContext(pIn: Pointer; InputSize: Cardinal; pOut: Pointer; Outputsize: PCardinal; var Done: Boolean): Boolean;
procedure Cleanup;
private
{ Private-Deklarationen }
secfunc: SecurityFunctionTableA;
maxMessageSize: Cardinal;
hCredential: SecHandle;
hContext: CtxtHandle;
pOutBuf, pInBuf: PByteArray;
MessageReceived: Boolean;
public
{ Public-Deklarationen }
end;
procedure Pause(Zeit: Longint);
var
Form2: TForm2;
implementation
{$R *.dfm}
// the main method from the C-example
// this starts the ticket acquisition
procedure TForm2.Button1Click(Sender: TObject);
var
sec_State: SECURITY_STATUS;
pszTargetName: PSEC_CHAR;
hNewContext: CtxtHandle;
Output, Input: SecBufferDesc;
outSecBuf, inSecBuf: SecBuffer;
fContextAttr: ULONG;
cbOut, cbIn: Cardinal;
Done: Boolean;
timeOut: Integer;
begin
Done := False;
if InitPackage(maxMessageSize) then
begin
try
pOutBuf := nil;
pInBuf := nil;
GetMem(pOutBuf, maxMessageSize);
GetMem(pInBuf, maxMessageSize);
SecInvalidateHandle(hCredential);
SecInvalidateHandle(hContext);
if not GenClientContext(nil, 0, pOutBuf, #cbOut, Done) then
begin
Cleanup;
Exit;
end;
// ------------
// up to here everything seem to work just fine
// ------------
if not SendUDPMessage(pOutBuf, cbout) then
begin
Cleanup;
Exit;
end;
timeOut := 0;
while not Done and (timeOut <= 100) do
begin
repeat
Pause(1000);
Inc(timeOut);
until MessageReceived or (timeOut >= 100);
if MessageReceived then
begin
cbOut := maxMessageSize;
if not GenClientContext(pInBuf, cbIn, pOutBuf, #cbout, Done) then
begin
Cleanup;
Exit;
end;
if not SendUDPMessage(pOutBuf, cbout) then
begin
Cleanup;
Exit;
end;
end;
end;
if Done then // <<<----------Sadly... never done
begin
// Kerberos-ticket ---->>>> pInBuf
end
else // this happens every time
ShowMessage('Authentification failed due to server timeout');
finally
Cleanup;
end;
end;
end;
procedure TForm2.Cleanup;
begin
secfunc.DeleteSecurityContext(#hcontext);
secfunc.FreeCredentialsHandle(#hCredential);
FreeMem(pInBuf);
FreeMem(pOutBuf);
end;
function TForm2.GenClientContext(pIn: Pointer; InputSize: Cardinal;
pOut: Pointer; Outputsize: PCardinal; var Done: Boolean): Boolean;
var
sec_State: SECURITY_STATUS;
LifeTime: TimeStamp;
OutBuffDesc: SecBufferDesc;
OutSecBuff: SecBuffer;
InBuffDesc: SecBufferDesc;
InSecBuff: SecBuffer;
ContextAttributes: ULONG;
NewContext: Boolean;
KerberosServer: PAnsiChar;
function SetSecHandle: PSecHandle;
begin
if NewContext then
Result := nil
else
Result := #hContext;
end;
function SetInBuffer: PSecBufferDesc;
begin
if NewContext then
Result := nil
else
Result := #InBuffDesc;
end;
begin
if not Assigned(pIn) then
begin
NewContext := True;
// No user athentication needed, so we'll skip that part of the example
sec_State := secfunc.AcquireCredentialsHandleA(
nil,
PAnsiChar('Kerberos'),
SECPKG_CRED_OUTBOUND,
nil,
nil,
nil,
nil,
#hCredential,
#LifeTime
);
if not SEC_SUCCESS(sec_State) then
begin
ShowMessage('AqcuireCredentials failed, Error#: ' + IntToStr(sec_State));
Result := False;
Exit;
end;
end;
// Prepare buffers
// Output
OutBuffDesc.ulVersion := SECBUFFER_VERSION;
OutBuffDesc.cBuffers := 1;
OutBuffDesc.pBuffers := #OutSecBuff;
OutSecBuff.cbBuffer := Outputsize^;
OutSecBuff.BufferType := SECBUFFER_TOKEN;
OutSecBuff.pvBuffer := pOut;
//Input
InBuffDesc.ulVersion := SECBUFFER_VERSION;
InBuffDesc.cBuffers := 1;
InBuffDesc.pBuffers := #InSecBuff;
InSecBuff.cbBuffer := InputSize;
InSecBuff.BufferType := SECBUFFER_TOKEN;
InSecBuff.pvBuffer := pIn;
// KerberosServer := krbServer; // Tried both krbtgt and following...no change
KerberosServer := PAnsiChar('RestrictedKrbHost/FM-DC01.mydomain.int');
sec_State := secfunc.InitializeSecurityContextA(
#hCredential,
SetSecHandle,
KerberosServer,
ISC_REQ_DELEGATE + ISC_REQ_MUTUAL_AUTH,
0,
SECURITY_NATIVE_DREP,
SetInBuffer,
0,
#hContext,
#OutBuffDesc,
#contextAttributes,
#Lifetime
);
if not SEC_SUCCESS(sec_State) then
begin
ShowMessage('init context failed, Error #: ' + IntToStr(sec_State));
Result := False;
Exit;
end;
if (sec_State = SEC_I_COMPLETE_NEEDED) or
(sec_State = SEC_I_COMPLETE_AND_CONTINUE) then
begin
sec_State := secfunc.CompleteAuthToken(#hContext, #OutBuffDesc);
if not SEC_SUCCESS(sec_State) then
begin
ShowMessage('complete failed, Error #: ' + IntToStr(sec_State));
Result := False;
Exit;
end;
end;
Outputsize^ := OutSecBuff.cbBuffer;
// First call of this method results in sec_state = SEC_I_CONTINUE_NEEDED
// which should be OK, but then I have to switch to UDP communication
// and that seems to be buggy.
Done := not ((sec_State = SEC_I_CONTINUE_NEEDED) or (sec_State = SEC_I_COMPLETE_AND_CONTINUE));
Result := True;
end;
function TForm2.InitPackage(var maxMessage: Cardinal): Boolean;
var
sec_State: SECURITY_STATUS;
pPkgInfo: PSecPkgInfoA;
InitSecurityInterfaceA: function: PSecurityFunctionTableA; stdcall;
begin
Result := False;
MessageReceived := False;
try
InitSecurityInterfaceA := GetProcAddress(GetModuleHandle('secur32.dll'), 'InitSecurityInterfaceA');
if Assigned(InitSecurityInterfaceA) then
secfunc := InitSecurityInterfaceA^;
sec_State := secfunc.QuerySecurityPackageInfoA(
PAnsiChar('Kerberos'),
#pPkgInfo
);
if sec_state = SEC_E_OK then
begin
maxMessage := pPkgInfo^.cbMaxToken;
Result := True;
end;
finally
secfunc.FreeContextBuffer(pPkgInfo);
end;
end;
procedure TForm2.mySocketDataAvailable(Sender: TObject; ErrCode: Word);
var
inBuf: array of Byte;
BufLen: Integer;
Length: Integer;
sentSize: Cardinal;
begin
MessageReceived := False;
// Data should pour in here. Hopefully the Kerberos-ticket
// First DWORD is message size, rest is the message itself
Length := mySocket.Receive(#sentsize, SizeOf(DWORD));
if Length <= 0 then
begin
Exit;
end;
// The rest
SetLength(inBuf, SizeOf(sentSize));
Length := mySocket.Receive(#inBuf, SizeOf(inBuf));
if Length >= 0 then
begin
pInBuf := #inBuf;
MessageReceived := True;
end;
end;
function TForm2.SendUDPMessage(outBuf: Pointer; outsize: Cardinal): Boolean;
begin
mySocket.Proto := 'udp';
mySocket.Addr := 'FM-DC01.mydomain.int';
mySocket.Port := '88';
mySocket.Connect;
// send size of message first, then message itself
Result := (mySocket.Send(PByte(#outsize), SizeOf(outsize)) > -1);
if Result then
if mySocket.State = wsConnected then
Result := (mySocket.Send(outBuf, outsize) > -1);
end;
// small method to wait for action, should not be part of the problem
procedure Pause(Zeit: Longint);
var
Tick: DWORD;
Event: THandle;
begin
Event := CreateEvent(nil, False, False, nil);
try
Tick := GetTickCount + DWORD(Zeit);
while (Zeit > 0) and
(MsgWaitForMultipleObjects(1, Event, False, Zeit, QS_ALLINPUT) <> WAIT_TIMEOUT) do
begin
Application.ProcessMessages;
Zeit := Tick - GetTickCount;
end;
finally
CloseHandle(Event);
end;
end;
end.
Related
Recently I finally managed to redirect console application output to TMemo text field of another application using an example from Microsoft: https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output
All the classical examples run a console executable, wait till it ends and then read its STDOUT. I would like to launch a long-running executable that is normally not intended to end, and get its STDOUT stream as soon as new characters become available.
I managed to modify this example so that a read-write part is a loop and runs in a thread (TProcessExecuterThread.Execute). Now I am in doubt whether I should use the thread at all.
Additionally, the host receives not the whole strings till CR-LF even if I get from a pipe one character after other (TProcessExecuterThread.ReadFromPipe).
Finally I am concerned what about ending the host. The guest should then receive a signal to terminate and after some timeout - should be killed. Where (not "how") is it better to organize this?
Here is the console guest application for the test:
{$APPTYPE CONSOLE}
program GuestApp;
uses System.SysUtils;
var i: Integer;
begin
Writeln('Ongoing console output:');
for i := 0 to 65535 do begin //while True do begin
if i mod 2 = 0 then Write('*');
Writeln(Format('Output line %d', [i]));
Sleep(500);
end;
end.
Here is the host application (sorry, it is not short):
unit Executer;
interface
uses Winapi.Windows, System.Classes, System.Generics.Collections;
type
TProcessExecuterThread = class(TThread)
private
FStdInQueue: TQueue<string>;
FhChildStdOutRd: THandle;
FhChildStdInWr: THandle;
FOnStdOutLog: TGetStrProc;
procedure ReadFromPipe();
procedure WriteToPipe();
procedure StdOutLog(msg: string);
protected
procedure Execute(); override;
property hChildStdOutRd: THandle read FhChildStdOutRd write FhChildStdOutRd;
property hChildStdInWr: THandle read FhChildStdInWr write FhChildStdInWr;
property OnstdOutLog: TGetStrProc read FOnStdOutLog write FOnStdOutLog;
end;
TProcessExecuter = class
private const
BUFSIZE = 4096;
private
FhChildStdInRd: THandle;
FhChildStdInWr: THandle;
FhChildStdOutRd: THandle;
FhChildStdOutWr: THandle;
FOnLog: TGetStrProc;
FOnStdOutLog: TGetStrProc;
FExThread: TProcessExecuterThread;
procedure CreateChildProcess(ACmdLine: string);
procedure ErrorExit(AFuncName: string);
procedure Log(msg: string);
procedure StdOutLog(const msg: string);
function KillProcess(dwProcID, Wait: DWORD): Integer;
public
constructor Create();
function RunRedirectedProcess(ACmdLine: string): Integer;
property OnLog: TGetStrProc read FOnLog write FOnLog;
property OnstdOutLog: TGetStrProc read FOnStdOutLog write FOnStdOutLog;
end;
implementation
uses System.SysUtils;
procedure TProcessExecuter.Log(msg: string);
begin
if Assigned(FOnLog) then FOnLog(msg);
end;
procedure TProcessExecuter.StdOutLog(const msg: string);
begin
if Assigned(FOnStdOutLog) then FOnStdOutLog(msg);
end;
// Format a readable error message, display a message box,
// and exit from the application.
procedure TProcessExecuter.ErrorExit(AFuncName: string);
var msg: string;
dw: DWORD;
begin
dw := GetLastError();
msg := Format('%s failed with error %d: %s', [AFuncName, dw, SysErrorMessage(dw)]);
Log(msg);
// ExitProcess(1);
end;
constructor TProcessExecuter.Create();
begin
FhChildStdInRd := 0;
FhChildStdInWr := 0;
FhChildStdOutRd := 0;
FhChildStdOutWr := 0;
FExThread := TProcessExecuterThread.Create();
FExThread.OnstdOutLog := StdOutLog;
end;
// Create a child process that uses the previously created pipes for STDIN and STDOUT.
procedure TProcessExecuter.CreateChildProcess(ACmdLine: string);
var
piProcInfo: TProcessInformation;
siStartInfo: TStartupInfo;
bSuccess: Boolean;
begin
try
bSuccess := False;
FillChar(piProcInfo, SizeOf(TProcessInformation), 0);
FillChar(siStartInfo, SizeOf(TStartupInfo), 0);
siStartInfo.cb := SizeOf(TStartupInfo);
siStartInfo.hStdError := FhChildStdOutWr;
siStartInfo.hStdOutput := FhChildStdOutWr;
siStartInfo.hStdInput := FhChildStdInRd;
siStartInfo.dwFlags := siStartInfo.dwFlags or STARTF_USESTDHANDLES;
bSuccess := CreateProcess(nil, PWideChar(ACmdLine), nil, nil, True, 0, nil, nil, siStartInfo, piProcInfo);
if not bSuccess then begin
ErrorExit('CreateProcess');
Exit;
end
else begin
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
CloseHandle(FhChildStdOutWr);
CloseHandle(FhChildStdInRd);
end;
FExThread.hChildStdOutRd := FhChildStdOutRd;
FExThread.hChildStdInWr := FhChildStdInWr;
except
on ex: Exception do Log(ex.Message);
end;
end;
function TProcessExecuter.RunRedirectedProcess(ACmdLine: string): Integer;
var saAttr: SECURITY_ATTRIBUTES;
i: Integer;
begin
try
Log('->Start of parent execution.');
saAttr.nLength := SizeOf(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle := True;
saAttr.lpSecurityDescriptor := 0;
if not CreatePipe(FhChildStdOutRd, FhChildStdOutWr, #saAttr, 0) then begin
ErrorExit('StdoutRd CreatePipe');
Exit;
end;
if not SetHandleInformation(FhChildStdOutRd, HANDLE_FLAG_INHERIT, 0) then begin
ErrorExit('Stdout SetHandleInformation');
Exit;
end;
if not CreatePipe(FhChildStdInRd, FhChildStdInWr, #saAttr, 0) then begin
ErrorExit('Stdin CreatePipe');
Exit;
end;
if not SetHandleInformation(FhChildStdInWr, HANDLE_FLAG_INHERIT, 0) then begin
ErrorExit('Stdin SetHandleInformation');
Exit;
end;
CreateChildProcess(ACmdLine);
//Read/write loop was here
Log('->End of parent execution.');
if not CloseHandle(FhChildStdInWr) then begin
ErrorExit('StdInWr CloseHandle');
Exit;
end;
Result := 0;
except
on ex: Exception do Log(ex.Message);
end;
end;
procedure TProcessExecuterThread.WriteToPipe();
var dwRead, dwWritten: DWORD;
chBuf: Pointer;
bSuccess: Boolean;
line: string;
bs: Integer;
begin
bSuccess := False;
while FStdInQueue.Count > 0 do begin
line := FStdInQueue.Dequeue();
bs := (Length(line) + 1) * SizeOf(WideChar);
GetMem(chBuf, bs);
try
StrPCopy(PWideChar(chBuf), line);
if not WriteFile(FhChildStdInWr, chBuf^, dwRead, dwWritten, nil) then break;
finally
FreeMem(chBuf, bs);
end;
end;
end;
procedure TProcessExecuterThread.ReadFromPipe();
const BUFSIZE = 1; //4096
var dwRead: DWORD;
//chBuf: array [0 .. BUFSIZE] of CHAR;
chBuf: array [0 .. BUFSIZE] of AnsiChar; // Currently only ANSI is possible
ch: AnsiChar;
bSuccess: Boolean;
begin
bSuccess := False;
while True do begin
//bSuccess := ReadFile(FhChildStdOutRd, chBuf, BUFSIZE, dwRead, nil);
bSuccess := ReadFile(FhChildStdOutRd, ch, 1, dwRead, nil);
if (not bSuccess) or (dwRead = 0) then
break;
//StdOutLog(chBuf);
StdOutLog(ch);
end;
end;
procedure TProcessExecuterThread.StdOutLog(msg: string);
begin
if Assigned(FOnStdOutLog) then
Synchronize(
procedure()
begin
FOnStdOutLog(msg);
end
);
end;
procedure TProcessExecuterThread.Execute;
begin
inherited;
FStdInQueue := TQueue<string>.Create();
try
while not Terminated do begin
WriteToPipe();
ReadFromPipe();
end;
finally
FreeAndNil(FStdInQueue);
end;
end;
end.
If you have two random files e.g. .txt, .csv, .jpg, and you wanted to open two of them and make them take up the screen 50% 50%.
How would you find the window handle that was opened so that you can re size the right one?
I have edited below to be closer to the answer thanks to suggestions from David Heffernan and Rob Kennedy
The code below kind of works if everything goes right but i'm sure there are ways to improve the code.
Using ShellExecuteEx can return a process ID, you can get a window handle off the process ID by using EnumWindows checking against the process id. Then if everything works you can re size the form using MoveWindow
i have an example in the unit uFileStuff below
There are a few issues that i'm not sure can be resolved
Files can be opened in the same application e.g. notepad++.
ShellExecuteEx may not return a process id
EnumWindows may not find the window
Unit uFileStuff
unit uFileStuff;
interface
uses Winapi.Windows, System.SysUtils, Generics.Collections, shellapi, Winapi.Messages, Vcl.Dialogs, Vcl.Forms;
type
PWindowSearch = ^TWindowSearch;
TWindowSearch = record
TargetProcessID: DWord;
ResultList: TList<HWnd>;
end;
TMyFile = class
private
sFileNameAndPath : String;
MyProcessID : DWord;
MyParentProcessID : Dword;
Procedure OpenFile(sFile: String);
procedure UpdateWindowListByProcessID;
public
WindowsLinkedToProcessID : TList<HWnd>;
function GetWindowInformation(Wnd: HWnd) : String;
function GetAllWindowInformation : String;
property ProcessID : Dword read MyProcessID;
property ParentProcessID : Dword read MyParentProcessID;
constructor Create(sFile : String);
destructor Destroy; override;
end;
implementation
constructor TMyFile.Create(sFile: String);
begin
MyProcessID := 0;
MyParentProcessID := 0;
sFileNameAndPath := sFile;
WindowsLinkedToProcessID := TList<HWnd>.Create;
if (sFile <> '') and FileExists(sFile) then
OpenFile(sFileNameAndPath);
end;
destructor TMyFile.Destroy;
begin
WindowsLinkedToProcessID.Free;
Inherited;
end;
function TMyFile.GetAllWindowInformation: String;
var i : Integer;
sMessage : String;
begin
result := '';
for I := 0 to WindowsLinkedToProcessID.Count -1 do begin
sMessage := sMessage + #13#10 + GetWindowInformation(WindowsLinkedToProcessID[i]);
end;
result := result + sMessage;
end;
function TMyFile.GetWindowInformation(Wnd: HWnd): String;
var Buffer: array[0..255] of char;
begin
result := inttostr(Wnd);
SendMessage(Wnd, WM_GETTEXT, 255, LongInt(#Buffer[0]));
if Buffer <> '' then begin
result := result + ', ' + Buffer;
end;
end;
procedure TMyFile.OpenFile(sFile: String);
var i : Integer;
SEInfo: TShellExecuteInfo;
ExitCode: DWORD;
ExecuteFile, ParamString, StartInString, sMessage: string;
begin
ExecuteFile:=sFile;
FillChar(SEInfo, SizeOf(SEInfo), 0);
SEInfo.cbSize := SizeOf(TShellExecuteInfo);
with SEInfo do
begin
fMask := SEE_MASK_NOCLOSEPROCESS;
Wnd := Application.Handle;
lpFile := PChar(sFile);
nShow := SW_SHOWNORMAL;
end;
if ShellExecuteEx(#SEInfo) then
begin
if SEInfo.hProcess > 0 then begin
Sleep(100);
WaitForInputIdle(SEInfo.hProcess, 10000 );
MyProcessID := GetProcessId( SEInfo.hProcess );
UpdateWindowListByProcessID;
end else begin
ShowMessage('No Process ' + SysErrorMessage(GetLastError) );
end;
end else
ShowMessage('Error starting "'+ sFile +'"' + #13#10 + SysErrorMessage(GetLastError));
end;
procedure TMyFile.UpdateWindowListByProcessID;
function SelectWindowByProcessID(Wnd: HWnd; Param: LParam): Bool; stdcall;
var
pSearchRec: PWindowSearch;
WindowPid: DWord;
begin
pSearchRec := PWindowSearch(Param);
Assert(Assigned(pSearchRec));
GetWindowThreadProcessID(Wnd, WindowPid);
if (WindowPid = pSearchRec.TargetProcessID) and IsWindowVisible(Wnd) then
pSearchRec.ResultList.Add(Wnd);
Result := True;
end;
var
SearchRec: TWindowSearch;
begin
if MyProcessID > 0 then begin
SearchRec.TargetProcessID := MyProcessID;
SearchRec.ResultList := WindowsLinkedToProcessID;
EnumWindows(#SelectWindowByProcessID, LParam(#SearchRec));
end;
end;
end.
Form Creating Files on create - has button to open them
unit fFileOpen;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TfrmFileOpen = class(TForm)
btn1: TButton;
procedure btn1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
sApplicationPath : String;
sFile1, sFile2 : String;
public
{ Public declarations }
end;
var
frmFileOpen: TfrmFileOpen;
implementation
uses uFileStuff;
{$R *.dfm}
procedure TfrmFileOpen.btn1Click(Sender: TObject);
var File1 : TMyFile;
File2 : TMyFile;
begin
File1 := TMyFile.Create( sFile1 );
try
if sFile2 <> sFile1 then
File2 := TMyFile.Create( sFile2 )
else
File2 := TMyFile.Create( '' );
try
if (File1.ProcessID > 0) and (File2.ProcessID > 0) then begin
if (File1.ParentProcessID > 0) and (File2.ParentProcessID > 0) and (File1.ParentProcessID = File2.ParentProcessID) then begin
showmessage('Both Files opened in same process');
end else if (File1.WindowsLinkedToProcessID.Count > 0) and (File2.WindowsLinkedToProcessID.Count > 0) then begin
if File1.WindowsLinkedToProcessID.Count > 1 then
ShowMessage('Warning returned more than 1 window Moving the first window' + #13#10 + File1.GetAllWindowInformation);
MoveWindow(File1.WindowsLinkedToProcessID[0], 0, 0, Trunc(Screen.WorkAreaWidth / 2), Screen.WorkAreaHeight, True);
if File2.WindowsLinkedToProcessID.Count > 1 then
ShowMessage('Warning returned more than 1 window Moving the first window' + #13#10 + File2.GetAllWindowInformation);
MoveWindow(File2.WindowsLinkedToProcessID[0], Round(Screen.WorkAreaWidth / 2)+1, 0, Trunc(Screen.WorkAreaWidth / 2), Screen.WorkAreaHeight, True);
end;
end;
finally
File2.Free;
end;
finally
File1.Free;
end;
end;
procedure TfrmFileOpen.FormCreate(Sender: TObject);
var slTemp : TStringList;
img : TBitmap;
begin
ReportMemoryLeaksOnShutdown := true;
sApplicationPath := ExtractFileDir(application.ExeName);
sFile1 := IncludeTrailingPathDelimiter( sApplicationPath ) + 'File1.txt';
sFile2 := IncludeTrailingPathDelimiter( sApplicationPath ) + 'File2.csv';
{
if not FileExists( sFile1 ) then begin
img := TBitmap.Create;
img.SetSize(300,300);
img.SaveToFile( sFile1 );
img.Free;
end; }
if not FileExists(sFile1) then begin
slTemp := TStringList.Create;
slTemp.Add('File1');
slTemp.SaveToFile(sFile1);
slTemp.Free;
end;
if not FileExists(sFile2) then begin
slTemp := TStringList.Create;
slTemp.Add('File2');
slTemp.SaveToFile(sFile2);
slTemp.Free;
end;
end;
end.
I have a server written in Delphi that I would like to add a debug logger to so it can log messages passed to Windows.OutputDebugString() while it is deployed, so clients can send me the log when there are issues. In the end, I want functionality similar to DebugView, but built into the server program itself.
I understand how the OutputDebugString works by writing to a shared memory file and using system wide events to synchronize the program and its debugger, and I have found solutions in C# and C++, but have yet to be able to translate those solutions to Delphi.
My largest problem is not knowing how to interact with the DBWIN_BUFFER_READY and DBWIN_DATA_READY synchronization events with Delphi, or how to reference the specific memory mapped file "DBWIN_BUFFER" that OutputDebugString writes to.
Additionally I have found solutions that implement their own method call instead of Windows.OutputDebugString(), but the program already has hundreds of calls, both in the code we have written and third-party modules we have added in, so these are not an option.
The C++ code you linked to can be translated to Delphi as follows:
//////////////////////////////////////////////////////////////
//
// File: WinDebugMonitor.pas
// Description: Interface of class TWinDebugMonitor
// Created: 2007-12-6
// Author: Ken Zhang
// E-Mail: cpp.china#hotmail.com
//
// Translated: 2015-02-13
// Translator: Remy Lebeau
// E-Mail: remy#lebeausoftware.org
//
//////////////////////////////////////////////////////////////
unit WinDebugMonitor;
interface
uses
Windows;
type
PDbWinBuffer = ^DbWinBuffer;
DbWinBuffer = record
dwProcessId: DWORD;
data: array[0..(4096-sizeof(DWORD))-1] of AnsiChar;
end;
TWinDebugMonitor = class
private
m_hDBWinMutex: THandle;
m_hDBMonBuffer: THandle;
m_hEventBufferReady: THandle;
m_hEventDataReady: THandle;
m_hWinDebugMonitorThread: THandle;
m_bWinDebugMonStopped: Boolean;
m_pDBBuffer: PDbWinBuffer;
function Initialize: DWORD;
procedure Uninitialize;
function WinDebugMonitorProcess: DWORD;
public
constructor Create;
destructor Destroy; override;
procedure OutputWinDebugString(const str: PAnsiChar); virtual;
end;
implementation
// ----------------------------------------------------------------------------
// PROPERTIES OF OBJECTS
// ----------------------------------------------------------------------------
// NAME | DBWinMutex DBWIN_BUFFER_READY DBWIN_DATA_READY
// ----------------------------------------------------------------------------
// TYPE | Mutex Event Event
// ACCESS | All All Sync
// INIT STATE | ? Signaled Nonsignaled
// PROPERTY | ? Auto-Reset Auto-Reset
// ----------------------------------------------------------------------------
constructor TWinDebugMonitor.Create;
begin
inherited;
if Initialize() <> 0 then begin
OutputDebugString('TWinDebugMonitor.Initialize failed.'#10);
end;
end;
destructor TWinDebugMonitor.Destroy;
begin
Uninitialize;
inherited;
end;
procedure TWinDebugMonitor.OutputWinDebugString(const str: PAnsiChar);
begin
end;
function WinDebugMonitorThread(pData: Pointer): DWORD; stdcall;
var
_Self: TWinDebugMonitor;
begin
_Self = TWinDebugMonitor(pData);
if _Self <> nil then begin
while not _Self.m_bWinDebugMonStopped do begin
_Self.WinDebugMonitorProcess;
end;
end;
Result := 0;
end;
function TWinDebugMonitor.Initialize: DWORD;
begin
SetLastError(0);
// Mutex: DBWin
// ---------------------------------------------------------
m_hDBWinMutex := OpenMutex(MUTEX_ALL_ACCESS, FALSE, 'DBWinMutex');
if m_hDBWinMutex = 0 then begin
Result := GetLastError;
Exit;
end;
// Event: buffer ready
// ---------------------------------------------------------
m_hEventBufferReady := OpenEvent(EVENT_ALL_ACCESS, FALSE, 'DBWIN_BUFFER_READY');
if m_hEventBufferReady = 0 then begin
m_hEventBufferReady = CreateEvent(nil, FALSE, TRUE, 'DBWIN_BUFFER_READY');
if m_hEventBufferReady = 0 then begin
Result := GetLastError;
Exit;
end;
end;
// Event: data ready
// ---------------------------------------------------------
m_hEventDataReady := OpenEvent(SYNCHRONIZE, FALSE, 'DBWIN_DATA_READY');
if m_hEventDataReady = 0 then begin
m_hEventDataReady := CreateEvent(nil, FALSE, FALSE, 'DBWIN_DATA_READY');
if m_hEventDataReady = 0 then begin
Result := GetLastError;
end;
end;
// Shared memory
// ---------------------------------------------------------
m_hDBMonBuffer := OpenFileMapping(FILE_MAP_READ, FALSE, 'DBWIN_BUFFER');
if m_hDBMonBuffer = 0 then begin
begin
m_hDBMonBuffer := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE, 0, SizeOf(DbWinBuffer), 'DBWIN_BUFFER');
if m_hDBMonBuffer = 0 then begin
Result := GetLastError;
Exit;
end;
end;
m_pDBBuffer := PDbWinBuffer(MapViewOfFile(m_hDBMonBuffer, SECTION_MAP_READ, 0, 0, 0));
if m_pDBBuffer = nil then begin
Result := GetLastError;
Exit;
end;
// Monitoring thread
// ---------------------------------------------------------
m_bWinDebugMonStopped := False;
m_hWinDebugMonitorThread := CreateThread(nil, 0, #WinDebugMonitorThread, Self, 0, nil);
if m_hWinDebugMonitorThread = 0 then begin
m_bWinDebugMonStopped := True;
Result := GetLastError;
Exit;
end;
// set monitor thread's priority to highest
// ---------------------------------------------------------
SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS);
SetThreadPriority(m_hWinDebugMonitorThread, THREAD_PRIORITY_TIME_CRITICAL);
Result := 0;
end;
procedure TWinDebugMonitor.Uninitialize;
begin
if m_hWinDebugMonitorThread <> 0 then begin
m_bWinDebugMonStopped := True;
WaitForSingleObject(m_hWinDebugMonitorThread, INFINITE);
CloseHandle(m_hWinDebugMonitorThread);
m_hWinDebugMonitorThread := 0;
end;
if m_hDBWinMutex <> 0 then begin
CloseHandle(m_hDBWinMutex);
m_hDBWinMutex := 0;
end;
if m_pDBBuffer <> nil then begin
UnmapViewOfFile(m_pDBBuffer);
m_pDBBuffer := nil;
end;
if m_hDBMonBuffer <> 0 then begin
CloseHandle(m_hDBMonBuffer);
m_hDBMonBuffer := 0;
end;
if m_hEventBufferReady <> 0 then begin
CloseHandle(m_hEventBufferReady);
m_hEventBufferReady := 0;
end;
if m_hEventDataReady <> 0 then begin
CloseHandle(m_hEventDataReady);
m_hEventDataReady := 0;
end;
end;
function TCWinDebugMonitor.WinDebugMonitorProcess: DWORD;
const
TIMEOUT_WIN_DEBUG = 100;
begin
// wait for data ready
Result := WaitForSingleObject(m_hEventDataReady, TIMEOUT_WIN_DEBUG);
if Result = WAIT_OBJECT_0 then begin
OutputWinDebugString(m_pDBBuffer^.data);
// signal buffer ready
SetEvent(m_hEventBufferReady);
end;
end;
program Monitor;
{$APPTYPE CONSOLE}
{$R *.res}
uses
WinDebugMonitor;
type
Monitor = class(TWinDebugMonitor)
public
procedure OutputWinDebugString(const str: PAnsiChar); override;
end;
procedure Monitor.OutputWinDebugString(const str: PAnsiChar);
begin
Write(str);
end;
var
mon: Monitor;
begin
WriteLn('Win Debug Monitor Tool');
WriteLn('----------------------');
mon := Monitor.Create;
try
ReadLn;
finally
mon.Free;
end;
end.
program Output;
{$APPTYPE CONSOLE}
{$R *.res}
uses
SysUtils, Windows, Messages;
var
hConsoleInput: THandle;
function KeyPressed: boolean;
var
NumberOfEvents: Integer;
begin
GetNumberOfConsoleInputEvents(hConsoleInput, NumberOfEvents);
Result := NumberOfEvents > 0;
end;
procedure KeyInit;
var
mode: Integer;
begin
// get input file handle
Reset(Input);
hConsoleInput := TTextRec(Input).Handle;
// checks/sets so mouse input does not work
SetActiveWindow(0);
GetConsoleMode(hConsoleInput, mode);
if (mode and ENABLE_MOUSE_INPUT) = ENABLE_MOUSE_INPUT then
SetConsoleMode(hConsoleInput, mode xor ENABLE_MOUSE_INPUT);
end;
var
i: Integer;
buf: AnsiString;
begin
KeyInit;
WriteLn('Press any key to stop calling OutputDebugString......');
i := 0;
while not KeyPressed do
begin
Inc(i);
buf := Format('Message from process %d, msg id: %d'#10, [ GetCurrentProcessId(), I]);
OutputDebugStringA(PAnsiChar(buf));
end;
Writeln('Total ', i, ' messages sent.');
end.
Your solution is wrong.
Hint: This function is listed under functions for debugging, and it has "Debug" in its name.
Imagine what if two programs did this. OutputDebugString is a global function. It sends a string from ANY process to the debugger. If two programs would use OutputDebugString as their logging solution - you will get a mess from simultaneous output from two processes, and each log will be mixed with other.
Quote from MSDN (as additional proof that your solution is wrong):
Applications should send very minimal debug output and provide a way for the user to enable or disable its use. To provide more detailed tracing, see Event Tracing.
In other words, OutputDebugString is a debugging solution for development builds; it is not a logging system.
Use this (pseudo-code to illustrate the idea):
unit DebugTools;
interface
procedure OutputDebugString(const AStr: String);
implementation
procedure OutputDebugString(const AStr: String);
begin
if IsDebuggerPresent then
Windows.OutputDebugString(PChar(AStr))
else
begin
CritSect.Enter;
try
GlobalLog.Add(AStr);
finally
CritSect.Leave;
end;
end;
end;
end.
Just add this unit to the uses clause for each of your other units - and you will automatically capture "output OutputDebugString" without need to change source code.
I have this code that gets called from an injected DLL from a foreign process. It sould read some memory ranges but I sometimes get a segmentation fault at this line DataBuffer := TCharPointer(Address + CharOffset)^;. So is there any way to check if the memory is readable?
function GetCurrentData(Address: Pointer): PChar;
var
DataBuffer: Char;
CharArray: Array of Char;
CharOffset: Integer;
ReadBytes: longword;
begin
CharOffset := 0;
SetLength(CharArray, 0);
repeat
DataBuffer := TCharPointer(Address + CharOffset)^;
CharOffset := CharOffset + 1;
SetLength(CharArray, CharOffset);
CharArray[CharOffset - 1] := DataBuffer;
until (Ord(DataBuffer) = 0);
Result := PChar(#CharArray[0]);
end;
i also tryed to catch the exception but for some reason this is not working. The host programm still crashes.
unit UnitEventBridgeExports;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Windows, ShellAPI, JwaTlHelp32, SimpleIPC;
type
TCharPointer = ^Char;
const
WOWEXE = 'TestProgramm.exe';
var
IPCClient: TSimpleIPCClient;
PID: DWord;
Process: THandle;
procedure EventCalled;
procedure InitializeWoWEventBridge; stdcall;
implementation
function GetProcessIDByName(Exename: String): DWord;
var
hProcSnap: THandle;
pe32: TProcessEntry32;
begin
Result := 0;
hProcSnap := CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
if hProcSnap <> INVALID_HANDLE_VALUE then
begin
pe32.dwSize := SizeOf(ProcessEntry32);
if Process32First(hProcSnap, pe32) = True then
begin
while Process32Next(hProcSnap, pe32) = True do
begin
if pos(Exename, pe32.szExeFile) <> 0 then
Result := pe32.th32ProcessID;
end;
end;
CloseHandle(hProcSnap);
end;
end;
procedure InitializeEventBridge; stdcall;
begin
IPCClient := TSimpleIPCClient.Create(nil);
IPCClient.ServerID := 'EventBridgeServer';
IPCClient.Active := True;
IPCClient.SendStringMessage('init');
PID := GetProcessIDByName(EXE);
Process := OpenProcess(PROCESS_ALL_ACCESS, False, PID);
end;
function GetCurrentData(Address: Pointer): PChar;
var
DataBuffer: Char;
CharArray: Array of Char;
CharOffset: Integer;
ReadBytes: longword;
CharPointer: TCharPointer;
BreakLoop: Boolean;
begin
CharOffset := 0;
SetLength(CharArray, 0);
BreakLoop := False;
repeat
try
CharPointer := TCharPointer(Address + CharOffset);
DataBuffer := CharPointer^;
CharOffset := CharOffset + 1;
SetLength(CharArray, CharOffset);
CharArray[CharOffset - 1] := DataBuffer;
except
BreakLoop := True;
end;
until (Ord(DataBuffer) = 0) or BreakLoop;
Result := PChar(#CharArray[0]);
end;
procedure EventCalled;
var
TmpAddress: Pointer;
StringData: PChar;
begin
{$ASMMODE intel}
asm
mov [TmpAddress], edi
end;
StringData := GetCurrentData(TmpAddress);
IPCClient.SendStringMessage('update:' + StringData);
//IPCClient.SendStringMessage('update');
end;
end.
Your GetCurrentData() implementation is returning a pointer to a local array that goees out of scope when the function exits, then EventCalled() tries to use that poiner after it is no longer valid. Try this instead:
function GetCurrentData(Address: Pointer): AnsiString;
var
Offset: Integer;
begin
Result := '';
Offset := 0;
repeat
try
if PByte(Longint(Address) + Offset)^ = #0 then Break;
Inc(Offset);
except
Break;
end;
until False;
SetString(Result, PAnsiChar(Address), Offset);
end;
procedure EventCalled;
var
TmpAddress: Pointer;
StringData: AnsiString;
begin
{$ASMMODE intel}
asm
mov [TmpAddress], edi
end;
StringData := GetCurrentData(TmpAddress);
IPCClient.SendStringMessage('update:' + StringData);
//IPCClient.SendStringMessage('update');
end;
IsBadReadPtr API is here to help. You give address and size, and you get the readability back. Raymond Chen suggests to never use it though.
Other than that, VirtualQuery should give you information about the address in question to tell its readability.
Since Ken in comments below re-warned about danger of IsBadReadPtr, I bring it up to the answer to not pass by. Be sure to read the comments and links to Raymdond's blog. Be sure to see also:
Most efficient replacement for IsBadReadPtr?
How to check if a pointer is valid?
in this article delphi.net(prism) support async file io.
Delphi(Native/VCL) has Async File IO Class too?
Have you seen this code? http://pastebin.com/A2EERtyW
It is a good start for ansynchronous file I/O, but personally I would write a wrapper around the standard TStream class to maintain compatibility with VCL/RTL.
EDIT 2: This one looks good, too. http://www.torry.net/vcl/filedrv/other/dstreams.zip
I am posting it here just in case it disappears from Pastebin:
unit xfile;
{$I cubix.inc}
interface
uses
Windows,
Messages,
WinSock,
SysUtils,
Classes;
const
MAX_BUFFER = 1024 * 32;
type
TFileReadEvent = procedure(Sender: TObject; const Buffer; Count: Integer) of object;
TAsyncFile = class
private
FHandle: THandle;
FPosition: Cardinal;
FReadPending: Boolean;
FOverlapped: TOverlapped;
FBuffer: Pointer;
FBufferSize: Integer;
FOnRead: TFileReadEvent;
FEof: Boolean;
FSize: Integer;
function ProcessIo: Boolean;
procedure DoOnRead(Count: Integer);
function GetOpen: Boolean;
public
constructor Create(Filename: string; BufferSize: Integer = MAX_BUFFER);
destructor Destroy; override;
procedure BeginRead;
procedure Seek(Position: Integer);
procedure Close;
property OnRead: TFileReadEvent read FOnRead write FOnRead;
property Eof: Boolean read FEof;
property IsOpen: Boolean read GetOpen;
property Size: Integer read FSize;
end;
function ProcessFiles: Boolean;
implementation
var
Files: TList;
function ProcessFiles: Boolean;
var
i: Integer;
AsyncFile: TAsyncFile;
begin
Result := False;
for i := Files.Count - 1 downto 0 do
begin
AsyncFile := TAsyncFile(Files[i]);
Result := AsyncFile.ProcessIo or Result;
end;
end;
procedure Cleanup;
var
i: Integer;
AsyncFile: TAsyncFile;
begin
for i := Files.Count - 1 downto 0 do
begin
AsyncFile := TAsyncFile(Files[i]);
AsyncFile.Free;
end;
Files.Free;
end;
{ TAsyncFile }
constructor TAsyncFile.Create(Filename: string; BufferSize: Integer);
begin
Files.Add(Self);
FReadPending := False;
FBufferSize := BufferSize;
GetMem(FBuffer, FBufferSize);
FillMemory(#FOverlapped, SizeOf(FOverlapped), 0);
Cardinal(FHandle) := CreateFile(
PChar(Filename), // file to open
GENERIC_READ, // open for reading
0, // do not share
nil, // default security
OPEN_EXISTING, // open existing
FILE_ATTRIBUTE_NORMAL, //or // normal file
//FILE_FLAG_OVERLAPPED, // asynchronous I/O
0); // no attr. template
FSize := FileSeek(FHandle, 0, soFromEnd);
FileSeek(FHandle, 0, soFromBeginning);
FPosition := 0;
end;
destructor TAsyncFile.Destroy;
begin
Files.Remove(Self);
CloseHandle(FHandle);
FreeMem(FBuffer);
inherited;
end;
function TAsyncFile.ProcessIo: Boolean;
var
ReadCount: Cardinal;
begin
Result := False; Exit;
if not FReadPending then
begin
Exit;
end;
if GetOverlappedResult(FHandle, FOverlapped, ReadCount, False) then
begin
FReadPending := False;
DoOnRead(ReadCount);
end
else
begin
case GetLastError() of
ERROR_HANDLE_EOF:
begin
FReadPending := False;
FEof := True;
end;
ERROR_IO_PENDING:
begin
FReadPending := True;
end;
0:
begin
Result := True;
end;
end;
end;
end;
procedure TAsyncFile.BeginRead;
var
ReadResult: Boolean;
ReadCount: Cardinal;
begin
ReadCount := 0;
Seek(FPosition);
ReadResult := ReadFile(FHandle, FBuffer^, FBufferSize, ReadCount, nil);//#FOverlapped);
if ReadResult then
begin
FEof := False;
FReadPending := False;
FPosition := FPosition + ReadCount;
DoOnRead(ReadCount);
end
else
begin
case GetLastError() of
ERROR_HANDLE_EOF:
begin
FReadPending := False;
FEof := True;
end;
ERROR_IO_PENDING:
begin
FReadPending := True;
end;
end;
end;
end;
procedure TAsyncFile.DoOnRead(Count: Integer);
begin
if Assigned(FOnRead) then
begin
FOnRead(Self, FBuffer^, Count);
end;
end;
function TAsyncFile.GetOpen: Boolean;
begin
Result := Integer(FHandle) >= 0;
end;
procedure TAsyncFile.Close;
begin
FileClose(FHandle);
end;
procedure TAsyncFile.Seek(Position: Integer);
begin
FPosition := Position;
FileSeek(FHandle, Position, soFromBeginning);
end;
initialization
Files := Tlist.Create;
finalization
Cleanup;
end.
There is nothing built in to the RTL/VCL that offers asynchronous I/O for files. Incidentally the support in Delphi Prism is down to the .net framework rather than being language based.
You can either code directly against the Win32 API (that's not much fun) or hunt around for a Delphi wrapper to that API. Off the top of my head, I don't know any Delphi wrappers of asynchronous file I/O but they must exist.