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.
Related
I'm getting Chinese Text when using Windows 10 pipes in trying to read and write to the command prompt in Delphi. How do I make it in English?
The information from Communicate With Command Prompt Through Delphi is outdated for Delphi 10.3 and I had to modify a few variables to meet the compiler's wants. Changed a few integers to cardinals, etc. It's just outputting gibberish!
Here is the code for the component I'm trying to create based on the above link:
unit gtCommandPrompt;
interface
uses
System.SysUtils, System.Classes, Windows;
type
TTmonitorUpdate = procedure(OutPut: String) of object;
TTmonitor = class(TThread) // pipe monitoring thread for console output
private
iThreadSleep: Cardinal;
TextString: String;
FTTmonitorUpdate: TTmonitorUpdate;
procedure UpdateComponent;
protected
procedure Execute; override;
public
property OnUpdateComponent: TTmonitorUpdate read FTTmonitorUpdate write FTTmonitorUpdate;
end;
TOnReadCommandPrompt = procedure(OutPut: String) of object;
TOnWriteCommandPrompt = procedure(OutPut: String) of object;
TOnError = procedure(OutPut: String) of object;
TCommandPrompt = class(TComponent)
private
{ Private declarations }
ThreadDone: Boolean;
FThreadSleep: Cardinal;
FComponentThread: TTmonitor;
FOnError: TOnError;
FOnReadCommandPrompt : TOnReadCommandPrompt;
FOnWriteCommandPrompt : TOnWriteCommandPrompt;
procedure OnThreadUpdate(OutPut: String);
protected
{ Protected declarations }
public
{ Public declarations }
procedure Start();
procedure Stop();
procedure cmdWriteln(text: String);
constructor Create(AOwner: TComponent); override;
published
{ Published declarations }
property ThreadSleep: Cardinal read FThreadSleep write FThreadSleep default 40;
property OnReadCommandPrompt: TOnReadCommandPrompt read FOnReadCommandPrompt write FOnReadCommandPrompt;
property OnWriteCommandPrompt: TOnWriteCommandPrompt read FOnWriteCommandPrompt write FOnWriteCommandPrompt;
property OnError: TOnError read FOnError write FOnError;
Destructor Destroy; override;
end;
procedure Register;
var
InputPipeRead, InputPipeWrite: THandle;
OutputPipeRead, OutputPipeWrite: THandle;
ErrorPipeRead, ErrorPipeWrite: THandle;
ProcessInfo : TProcessInformation;
implementation
procedure Register;
begin
RegisterComponents('gtDelphi', [TCommandPrompt]);
end;
constructor TCommandPrompt.Create(AOwner: TComponent);
begin
inherited;
ThreadDone := true;
FThreadSleep := 40;
end;
procedure WritePipeOut(OutputPipe: THandle; InString: string);
// writes Instring to the pipe handle described by OutputPipe
var
byteswritten: Cardinal;
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: Cardinal): 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: Cardinal;
PipeSize: Cardinal;
begin
Result := '';
BytesRead := 0;
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 TTmonitor.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: Cardinal;
begin
while not Terminated do
begin
// read regular output stream and put on screen.
TextString := ReadPipeInput(OutputPipeRead, BytesRem);
if TextString <> '' then
Synchronize(UpdateComponent);
// now read error stream and put that on screen.
TextString := ReadPipeInput(ErrorPipeRead, BytesRem);
if TextString <> '' then
Synchronize(UpdateComponent);
sleep(iThreadSleep);
end;
end;
procedure TTmonitor.UpdateComponent;
// synchronize procedure for monitor thread
begin
if assigned(FTTmonitorUpdate) = true then FTTmonitorUpdate(TextString);
end;
procedure TCommandPrompt.OnThreadUpdate(OutPut: String);
// synchronize procedure for monitor thread
begin
if assigned(FOnReadCommandPrompt) = true then FOnReadCommandPrompt(OutPut);
end;
Destructor TCommandPrompt.Destroy;
begin
WritePipeOut(InputPipeWrite, 'EXIT'); // quit the CMD we started
FComponentThread.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);
// Always call the parent destructor after running your own code
inherited;
end;
procedure TCommandPrompt.cmdWriteln(text: String);
begin
WritePipeOut(InputPipeWrite, text);
if assigned(FOnWriteCommandPrompt) = true then FOnWriteCommandPrompt(text);
end;
procedure TCommandPrompt.Stop();
begin
FComponentThread.Terminate;
ThreadDone := true;
end;
procedure TCommandPrompt.Start();
{ 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: Byte; // was integer
Security : TSecurityAttributes;
start : TStartUpInfo;
begin
if ThreadDone = false then
begin
if assigned(FOnError) then FOnError('Start Error: Thread already running!');
exit;
end;
//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
FComponentThread := TTmonitor.Create(true); // don't start yet monitor thread
FComponentThread.Priority := tpHigher;
FComponentThread.iThreadSleep := 40;
FComponentThread.FreeOnTerminate := true;
FComponentThread.OnUpdateComponent := OnThreadUpdate;
ThreadDone := false;
FComponentThread.Start; // start thread;
end;
end;
end.
Thanks whosrdaddy!
Here is the updated code that now works:
unit gtCommandPrompt;
interface
uses
System.SysUtils, System.Classes, Windows;
type
TTmonitorUpdate = procedure(OutPut: AnsiString) of object;
TTmonitor = class(TThread) // pipe monitoring thread for console output
private
iThreadSleep: Cardinal;
TextString: AnsiString;
FTTmonitorUpdate: TTmonitorUpdate;
procedure UpdateComponent;
protected
procedure Execute; override;
public
property OnUpdateComponent: TTmonitorUpdate read FTTmonitorUpdate write FTTmonitorUpdate;
end;
TOnReadCommandPrompt = procedure(OutPut: AnsiString) of object;
TOnWriteCommandPrompt = procedure(OutPut: AnsiString) of object;
TOnError = procedure(OutPut: AnsiString) of object;
TCommandPrompt = class(TComponent)
private
{ Private declarations }
ThreadDone: Boolean;
FThreadSleep: Cardinal;
FComponentThread: TTmonitor;
FOnError: TOnError;
FOnReadCommandPrompt : TOnReadCommandPrompt;
FOnWriteCommandPrompt : TOnWriteCommandPrompt;
procedure OnThreadUpdate(OutPut: AnsiString);
protected
{ Protected declarations }
public
{ Public declarations }
procedure Start();
procedure Stop();
procedure cmdWriteln(text: AnsiString);
constructor Create(AOwner: TComponent); override;
published
{ Published declarations }
property ThreadSleep: Cardinal read FThreadSleep write FThreadSleep default 40;
property OnReadCommandPrompt: TOnReadCommandPrompt read FOnReadCommandPrompt write FOnReadCommandPrompt;
property OnWriteCommandPrompt: TOnWriteCommandPrompt read FOnWriteCommandPrompt write FOnWriteCommandPrompt;
property OnError: TOnError read FOnError write FOnError;
Destructor Destroy; override;
end;
procedure Register;
var
InputPipeRead, InputPipeWrite: THandle;
OutputPipeRead, OutputPipeWrite: THandle;
ErrorPipeRead, ErrorPipeWrite: THandle;
ProcessInfo : TProcessInformation;
implementation
procedure Register;
begin
RegisterComponents('gtDelphi', [TCommandPrompt]);
end;
constructor TCommandPrompt.Create(AOwner: TComponent);
begin
inherited;
ThreadDone := true;
FThreadSleep := 40;
end;
procedure WritePipeOut(OutputPipe: THandle; InString: AnsiString);
// writes Instring to the pipe handle described by OutputPipe
var
byteswritten: Cardinal;
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: Cardinal): AnsiString;
{
reads console output from InputPipe. Returns the input in function
result. Returns bytes of remaining information to BytesRem
}
var
cTextBuffer: array[1..32767] of AnsiChar;
sTextString: AnsiString;
cBytesRead: Cardinal;
cPipeSize: Cardinal;
begin
Result := '';
cBytesRead := 0;
cPipeSize := Sizeof(cTextBuffer);
// check if there is something to read in pipe
PeekNamedPipe(InputPipe, nil, cPipeSize, #cBytesRead, #cPipeSize, #BytesRem);
if cBytesRead > 0 then
begin
ReadFile(InputPipe, cTextBuffer, cPipeSize, cBytesRead, nil);
// a requirement for Windows OS system components
OemToCharA(#cTextBuffer, #cTextBuffer);
sTextString := AnsiString(cTextBuffer);
SetLength(sTextString, cBytesRead);
Result := sTextString;
end;
end;
procedure TTmonitor.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: Cardinal;
begin
while not Terminated do
begin
// read regular output stream and put on screen.
TextString := ReadPipeInput(OutputPipeRead, BytesRem);
if TextString <> '' then
Synchronize(UpdateComponent);
// now read error stream and put that on screen.
TextString := ReadPipeInput(ErrorPipeRead, BytesRem);
if TextString <> '' then
Synchronize(UpdateComponent);
sleep(iThreadSleep);
end;
end;
procedure TTmonitor.UpdateComponent;
// synchronize procedure for monitor thread
begin
if assigned(FTTmonitorUpdate) = true then
begin
try
FTTmonitorUpdate(TextString);
finally
end;
end;
end;
procedure TCommandPrompt.OnThreadUpdate(OutPut: AnsiString);
// synchronize procedure for monitor thread
begin
if assigned(FOnReadCommandPrompt) = true then
try
FOnReadCommandPrompt(OutPut);
finally
end;
end;
Destructor TCommandPrompt.Destroy;
begin
WritePipeOut(InputPipeWrite, 'EXIT'); // quit the CMD we started
FComponentThread.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);
// Always call the parent destructor after running your own code
inherited;
end;
procedure TCommandPrompt.cmdWriteln(text: AnsiString);
begin
WritePipeOut(InputPipeWrite, text);
if assigned(FOnWriteCommandPrompt) = true then
try
FOnWriteCommandPrompt(text);
finally
end;
end;
procedure TCommandPrompt.Stop();
begin
FComponentThread.Terminate;
ThreadDone := true;
end;
procedure TCommandPrompt.Start();
{ 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: Byte; // was integer
Security : TSecurityAttributes;
start : TStartUpInfo;
begin
if ThreadDone = false then
begin
if assigned(FOnError) then
try
FOnError('Start Error: Thread already running!');
finally
end;
exit;
end;
//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
FComponentThread := TTmonitor.Create(true); // don't start yet monitor thread
FComponentThread.Priority := tpHigher;
FComponentThread.iThreadSleep := 40;
FComponentThread.FreeOnTerminate := true;
FComponentThread.OnUpdateComponent := OnThreadUpdate;
ThreadDone := false;
FComponentThread.Start; // start thread;
end;
end;
end.
I rarely use threads and I have question about this class:
unit ExpectingThread;
interface
uses
System.Classes;
type
TExpectingThread = class(TThread)
private
_timeoutMs: Integer;
_buff: string;
_patterns: TArray<string>;
_result: Integer;
function Timeouted(startTime: Cardinal): Boolean;
function ExpectedDetected: Boolean;
protected
procedure Execute; override;
public
constructor Create(patterns: TArray<string>; buff: string; timeoutMs: Integer);
//this method is called from other NOT MAIN thread
procedure BuffUpdate(text: string);
end;
implementation
uses
Winapi.Windows,
System.RegularExpressions;
{ TExpectingThread }
constructor TExpectingThread.Create(patterns: TArray<string>; buff: string; timeoutMs: Integer);
begin
_patterns := patterns;
_timeoutMs := timeoutMs;
_buff := buff;
end;
//this method is called from other NOT MAIN thread
procedure TExpectingThread.BuffUpdate(text: string);
begin
// lock
TThread.Synchronize(Self, procedure
begin
_buff := _buff + text;
end);
// unlock
end;
procedure TExpectingThread.Execute;
var
startTime: Cardinal;
begin
inherited;
startTime := GetTickCount;
while true do
begin
if Timeouted(startTime) then
begin
Self.ReturnValue := 0; // timeouted
Exit;
end;
if ExpectedDetected then
begin
Self.ReturnValue := 1; // found
Exit;
end;
end;
end;
function TExpectingThread.ExpectedDetected: Boolean;
var
regex: TRegEx;
i: Integer;
begin
// lock
result := 0;
for i := 0 to High(_patterns) do
begin
regex.Create(_patterns[i]);
if regex.IsMatch(_buff) then
begin
_result := i;
Exit(true);
end;
end;
// unlock
end;
function TExpectingThread.Timeouted(startTime: Cardinal): Boolean;
var
currentTime: Cardinal;
begin
currentTime := GetTickCount;
result := currentTime - startTime > _timeoutMs;
end;
end.
Thread has to cheacking if any pattern is match to buffer over timeout. But other thread(NOT MAIN) can change buffer by using BuffUpdate method. Did I use Synchronization method correctly?
Synchronize() is specifically designed to work with the main UI thread. You can use it for inter-thread syncing, however ALL threads involved would have to use it. In your example, only the thread(s) that write to _buff are using it, but the thread that reads from _buff is not. So that is a hole in your logic.
That being said, if the main UI thread does not need to touch your data, then Synchronize() is not the best solution to use. You can just wrap the data access with a synchronization object instead, like a TCriticalSection, TMutex, TEvent, TMREWSync, Sytem.TMonitor, etc. For example:
unit ExpectingThread;
interface
uses
System.Classes, System.SyncObjs;
type
TExpectingThread = class(TThread)
private
_timeoutMs: Integer;
_buff: string;
_buffLock: TCriticalSection;
_buffChanged: Boolean;
_patterns: TArray<string>;
_result: Integer;
function Timeouted(startTime: Cardinal): Boolean;
function ExpectedDetected: Boolean;
protected
procedure Execute; override;
public
constructor Create(patterns: TArray<string>; buff: string; timeoutMs: Integer);
destructor Destroy; override;
//this method is called from other NOT MAIN thread
procedure BuffUpdate(text: string);
end;
implementation
uses
Winapi.Windows, System.RegularExpressions;
{ TExpectingThread }
constructor TExpectingThread.Create(patterns: TArray<string>; buff: string; timeoutMs: Integer);
begin
inherited Create(False);
_buffLock := TCriticalSection.Create;
_patterns := patterns;
_timeoutMs := timeoutMs;
_buff := buff;
_buffChanged := True;
end;
destructor TExpectingThread.Destroy;
begin
_buffLock.Free;
inherited;
end;
//this method is called from other NOT MAIN thread
procedure TExpectingThread.BuffUpdate(text: string);
begin
_buffLock.Enter;
try
_buff := _buff + text;
_buffChanged := True;
finally
_buffLock.Leave;
end;
end;
procedure TExpectingThread.Execute;
var
startTime: DWORD;
begin
startTime := GetTickCount;
while not Terminated do
begin
if Timeouted(startTime) then
begin
Self.ReturnValue := 0; // timeouted
Exit;
end;
if ExpectedDetected then
begin
Self.ReturnValue := 1; // found
Exit;
end;
end;
end;
function TExpectingThread.ExpectedDetected: Boolean;
var
i: Integer;
buff: string;
begin
Result := False;
_buffLock.Enter;
try
If not _buffChanged then Exit;
buff := _buff;
UniqueStr(buff);
_buffChanged := False;
finally
_buffLock.Leave;
end;
for i := Low(_patterns) to High(_patterns) do
begin
if TRegEx.IsMatch(buff, _patterns[i]) then
begin
_result := i;
Exit(True);
end;
end;
end;
function TExpectingThread.Timeouted(startTime: Cardinal): Boolean;
var
currentTime: DWORD;
begin
currentTime := GetTickCount;
result := currentTime - startTime > _timeoutMs;
end;
end.
I have a program created in Delphi 7 that uses ftp downloading.
How can i insert into that program to check for a server status?
For example if server is online to produce a green image, if server is offline o produce a red image. Here is the code.
unit Download;
interface
uses
Classes, Wininet, Windows, SysUtils, Dialogs, Forms;
type
GFilesThread = class(TThread)
private
LTemp : Longword;
STemp : string;
FilesToGet : TStringList;
FilesSize : Longword;
CBackup : integer;
CRevision : integer;
CForceCheck : boolean;
CSwitch : integer;
UUrl : string;
USelfParam : string;
Dir: string;
FSource: TStream;
protected
procedure Execute; override;
procedure UpdateFileProgress;
procedure SetFileProgressMax;
procedure UpdateStatusLabel;
procedure UpdateFileDecompStat;
procedure UpdateFilesProgress;
procedure CheckFiles(FList : TStringList);
procedure BZProgress(Sender: TObject);
procedure LockFMain;
procedure UNLockFMain;
procedure GetFiles;
procedure SelfUpdate(SelfVal : string);
procedure UpdateRevision;
procedure ModHosts(Lines : TStringList);
procedure DoUncompressStream(ASource, ADest: TStream);
procedure DoUncompress(const ASource, ADest: TFileName);
function HTTPGetFile(const fileURL, FileName: string; sh_progress: boolean): boolean;
public
property CreateBackup : integer write CBackup;
property UpdatesUrl : string write UUrl;
property LocalRevision : integer write CRevision;
property ForceCheck : boolean write CForceCheck;
end;
implementation
uses Launcher, CheckFiles, BZip2, md5, FileList;
// -------- by 667
procedure GFilesThread.UpdateStatusLabel;
begin
FMain.Label3.Caption:=STemp;
end;
procedure GFilesThread.SetFileProgressMax;
begin
if(CSwitch=0) then
FMain.Gauge1.MaxValue:=LTemp;
if(CSwitch=1) then
FMain.Gauge2.MaxValue:=LTemp;
end;
procedure GFilesThread.UpdateFileProgress;
begin
FMain.Gauge1.Progress:=LTemp;
end;
procedure GFilesThread.UpdateFilesProgress;
begin
FMain.Gauge2.Progress:=LTemp;
end;
procedure GFilesThread.UpdateRevision;
begin
FMain.UpdateRevision(IntToStr(CRevision));
end;
procedure GFilesThread.UpdateFileDecompStat;
begin
FMain.Gauge1.Progress:=LTemp;
end;
procedure GFilesThread.BZProgress(Sender: TObject);
begin
LTemp:=FSource.Position;
Synchronize(UpdateFileDecompStat);
end;
procedure GFilesThread.LockFMain;
begin
Fmain.ImgBtn1.Visible:=False;
Fmain.ImgBtn2.Visible:=False;
Fmain.ImgBtn5.Enabled:=False;
end;
procedure GFilesThread.UNLockFMain;
begin
Fmain.ImgBtn1.Visible:=True;
Fmain.ImgBtn2.Visible:=True;
Fmain.ImgBtn5.Enabled:=True;
end;
// --------- by 667
function GFilesThread.HTTPGetFile(const fileURL, FileName: string; sh_progress: boolean): boolean;
const
BufferSize = 1024;
var
hSession, hURL: HInternet;
Buffer: array[1..BufferSize] of Byte;
BufferLen: Longword;
f: file;
sAppName: string;
begin
Result := False;
sAppName := 'L2ClientUpdater';
LTemp:=0;
hSession := InternetOpen(PChar(sAppName),
INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);
try
hURL := InternetOpenURL(hSession, PChar(fileURL), nil, 0, 0, 0);
if (hURL <> nil) then begin
try
DeleteUrlCacheEntry(PChar(fileURL));
AssignFile(f, FileName);
Rewrite(f,1);
repeat
InternetReadFile(hURL, #Buffer, SizeOf(Buffer), BufferLen);
BlockWrite(f, Buffer, BufferLen);
if (sh_progress) then
begin
LTemp:=LTemp+BufferLen;
Synchronize(UpdateFileProgress);
end;
until
BufferLen = 0;
CloseFile(f);
Result := True;
finally
InternetCloseHandle(hURL);
end;
end;
finally
InternetCloseHandle(hSession);
end;
LTemp:=0;
Synchronize(UpdateFileProgress);
end;
procedure GFilesThread.DoUncompress(const ASource, ADest: TFileName);
var
Source, Dest: TStream;
begin
Source := TFileStream.Create(ASource, fmOpenRead + fmShareDenyWrite);
try
Dest := TFileStream.Create(ADest, fmCreate);
try
DoUncompressStream(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
DeleteFile(ASource);
end;
end;
procedure GFilesThread.DoUncompressStream(ASource, ADest: TStream);
const
BufferSize = 65536;
var
Count: Integer;
Decomp: TBZDecompressionStream;
Buffer: array[0..BufferSize - 1] of Byte;
begin
FSource := ASource;
LTemp:=FSource.Size;
CSwitch:=0;
Synchronize(SetFileProgressMax);
Decomp := TBZDecompressionStream.Create(ASource);
try
Decomp.OnProgress := BZProgress;
while True do
begin
Count := Decomp.Read(Buffer, BufferSize);
if Count <> 0 then ADest.WriteBuffer(Buffer, Count) else Break;
end;
finally
Decomp.Free;
FSource := nil;
LTemp:=0;
Synchronize(UpdateFileDecompStat);
end;
end;
procedure GFilesThread.CheckFiles(FList : TStringList);
var
i: integer;
FParam: TStringList;
FNameLocal: string;
begin
if(FList.Count>0) and (FList[0]<>'FAIL') and (not terminated) then
begin
STemp:='Checking files';
Synchronize(UpdateStatusLabel);
CSwitch:=1;
LTemp:=FList.Count-1;
Synchronize(SetFileProgressMax);
FParam:=TStringList.Create;
for i:=0 to FList.Count-1 do
begin
LTemp:=i;
Synchronize(UpdateFilesProgress);
FParam:=Tokenize(FList[i],'|');
FNameLocal:=Dir+FParam[2];
STemp:='Checking '+FParam[2];
Synchronize(UpdateStatusLabel);
if (not FileExists(FNameLocal)) then
begin
FilesToGet.Add(FList[i]);
FilesSize:=FilesSize+StrToInt(FParam[0]);
end
else
begin
if (MD5Print(MD5File(FNameLocal))<>FParam[1]) then
begin
FilesToGet.Add(FList[i]);
FilesSize:=FilesSize+StrToInt(FParam[0]);
end;
end;
end;
FParam.Free;
LTemp:=0;
Synchronize(UpdateFilesProgress);
STemp:='';
Synchronize(UpdateStatusLabel);
end;
end;
procedure GFilesThread.SelfUpdate(SelfVal : string);
var
FParam: TStringList;
FNameLocal: string;
F:boolean;
begin
if(SelfVal<>'') then
begin
FParam:=TStringList.Create;
FParam:=Tokenize(SelfVal,'|');
FNameLocal:=Dir+FParam[2];
if (MD5Print(MD5File(FNameLocal))<>FParam[1]) then
begin
FilesSize:=FilesSize+StrToInt(FParam[0]);
F:=HTTPGetFile(UUrl+FParam[2]+'.bz2',FNameLocal+'.bz2',True);
if(F) then begin
try
DoUncompress(FNameLocal+'.bz2',Dir+FParam[2]+'.New');
GenKillerBat(FParam[2]);
RunApp(Dir+'Update.bat');
except
STemp:='Update Failed';
DeleteFile(FNameLocal);
end;
end;
end;
FParam.Free;
end;
end;
procedure GFilesThread.ModHosts(Lines : TStringList);
var
Hosts : textfile;
H, HostsStrings, HostLineParam : TStringList;
HostsPath, temp : string;
i, z, funnyFlag : integer;
WindirP : PChar;
Res : cardinal;
begin
WinDirP := StrAlloc(MAX_PATH);
Res := GetWindowsDirectory(WinDirP, MAX_PATH);
if Res > 0 then
begin
if(FileExists(StrPas(WinDirP)+'\system32\drivers\etc\hosts.msn')) then
HostsPath := StrPas(WinDirP)+'\system32\drivers\etc\hosts.msn'
else
HostsPath := StrPas(WinDirP)+'\system32\drivers\etc\hosts';
AssignFile(Hosts,HostsPath);
Reset(Hosts);
HostsStrings:= TStringList.Create;
H:= TStringList.Create;
H.Add('#-------- Added by L2Updater --------');
while (not Eof(Hosts)) do
begin
ReadLn(Hosts, temp);
HostsStrings.Add(Trim(temp));
end ;
Reset(Hosts);
for i:=0 to Lines.Count-1 do
begin
funnyFlag:=0;
HostLineParam:=Tokenize(Lines[i],'|');
for z:=0 to HostsStrings.Count-1 do
begin
if (StrSearch(1,HostsStrings[z],HostLineParam[0])>0) and (HostsStrings[z][1]<>'#') then
begin
if (StrSearch(1,HostsStrings[z],HostLineParam[1]+#9)= 0) and (StrSearch(1,HostsStrings[z],HostLineParam[1]+' ')= 0 ) then
begin
HostsStrings[z]:= '#'+HostsStrings[z];
funnyFlag:=1;
end
else funnyFlag:=2;
end;
end;
if (funnyFlag=1) or (funnyFlag=0) then
H.Add(HostLineParam[1]+#9+HostLineParam[0]);
end;
H.Add('#-----------------');
if H.Count>2 then
begin
Rewrite(Hosts);
STemp:='Applying changes to Hosts';
Synchronize(UpdateStatusLabel);
for i:=0 to HostsStrings.Count-1 do
begin
WriteLn(Hosts,HostsStrings[i]);
end;
for i:=0 to H.Count-1 do
begin
WriteLn(Hosts,H[i]);
end;
STemp:='Hosts file chamged';
Synchronize(UpdateStatusLabel);
end;
H.Free; HostsStrings.Free; HostLineParam.Free;
CloseFile(Hosts);
end;
end;
procedure GFilesThread.GetFiles;
var
FParam : TStringList;
i : integer;
F, error : boolean;
LocalFile, BakFile: string;
begin
error := False;
if (FilesToGet.Count>0) then
begin
FParam:=TStringList.Create;
LTemp:=FilesToGet.Count-1;
CSwitch:=1;
Synchronize(SetFileProgressMax);
i:=0;
while (i < FilesToGet.Count) and (not terminated) do
begin
FParam:=Tokenize(FilesToGet[i],'|');
LocalFile:= Dir+FParam[2];
STemp:='Downloading '+ FParam[2];
Synchronize(UpdateStatusLabel);
CSwitch:=0;
LTemp:= StrToInt(FParam[0]);
Synchronize(SetFileProgressMax);
if (not DirectoryExists(ExtractFilePath(LocalFile))) then
ForceDirectories(ExtractFilePath(LocalFile));
F:=HTTPGetFile(UUrl+ReplaceStr(FParam[2],'\','/')+'.bz2',LocalFile+'.bz2',True);
if (F) then
begin
try
if (CBackup=1) then
begin
BakFile:=Dir+'backup\'+FParam[2];
if (not DirectoryExists(ExtractFilePath(BakFile))) then
ForceDirectories(ExtractFilePath(BakFile));
CopyFile(PChar(LocalFile),PChar(BakFile),false);
end;
STemp:='Extracting '+ FParam[2];
Synchronize(UpdateStatusLabel);
DoUncompress(LocalFile+'.bz2',Dir+FParam[2]);
except
STemp:='Update Failed';
error := True;
end;
end
else
begin
STemp:='Update Failed';
error := True;
Break;
end;
inc(i);
LTemp:=i;
CSwitch:=1;
Synchronize(UpdateFilesProgress);
end;
LTemp:=0;
Synchronize(UpdateFilesProgress);
FParam.Free;
if (not error) then
STemp:='All files have been updated.';
end
else STemp:='';
end;
procedure GFilesThread.Execute;
var
List: TListFile;
CFiles, NFiles, HostsLines : TStringList;
TRev, IsModHosts : integer;
F : boolean;
begin
Dir:=GetCurrentDir+'\';
FilesSize:=0;
Synchronize(LockFMain);
STemp:='Downloading updates list';
Synchronize(UpdateStatusLabel);
if(UUrl[length(UUrl)]<>'/') then UUrl:=UUrl+'/';
F:=HTTPGetFile(UUrl+'files.lst.bz2',Dir+'files.lst.bz2', True);
if (F) then
begin
STemp:='';
Synchronize(UpdateStatusLabel);
try
DoUncompress(Dir+'files.lst.bz2',Dir+'files.lst');
except
STemp:='Update Failed';
Synchronize(UpdateStatusLabel);
DeleteFile(Dir+'files.lst');
end;
if(FileExists(Dir+'files.lst')) then
begin
FilesToGet := TStringList.Create;
List := TListFile.Create(Dir+'files.lst');
CFiles:=TStringList.Create;
TRev:=StrToInt(List.GetKeyValue('settings','Rev'));
IsModHosts:=StrToInt(List.GetKeyValue('settings','ModHosts'));
if (IsModHosts = 1) then
begin
HostsLines:= TStringList.Create;
HostsLines:= List.GetFSection('hosts');
try
ModHosts(HostsLines);
finally
HostsLines.Free;
end;
end;
USelfParam:= List.GetFSection('self')[0];
if(USelfParam<>'FAIL') then SelfUpdate(USelfParam);
CFiles:=List.GetFSection('files_critical');
CheckFiles(CFiles);
CFiles.Free;
if (CForceCheck) or (TRev>CRevision) then
begin
if (CBackup=1) then
begin
DelDir(Dir+'backup');
MkDir(Dir+'backup');
end;
NFiles:=TStringList.Create;
NFiles:=List.GetFSection('files_normal');
CheckFiles(NFiles);
NFiles.Free;
end;
GetFiles;
List.Destroy;
FilesToGet.Free;
DeleteFile(Dir+'files.lst');
if TRev>CRevision then
begin
CRevision:=TRev;
Synchronize(UpdateRevision);
end;
end;
end
else
begin
STemp:='Update Failed';
DeleteFile(Dir+'files.lst');
end;
Synchronize(UpdateStatusLabel);
Synchronize(UNLockFMain);
end;
end.
function CanConnect(const aUserName, aPassword, aHost: String; out aErrm: string): boolean;
var
LocalIDFTP: TIdFTP;
begin
aErrm := '';
LocalIDFTP := TIdFTP.Create(nil);
try
LocalIDFTP.UserName := aUserName;
LocalIDFTP.Password := aPassword;
LocalIDFTP.Host := aHost;
LocalIDFTP.Passive := True;
try
LocalIDFTP.Connect;
LocalIDFTP.Quit;
result := true;
except
on E: Exception do
begin
aErrm := 'Unable to connect to FTP site: ' + E.Message;
Result := FALSE;
end;
end;
finally
if Assigned(LocalIDFTP) then
LocalIDFTP.Free
else
Result := FALSE;
end;
end; {CanConnect}
I'm trying to get a screenshot and send it over the web using ClientSocket and ServerSocket components.
I'm having problems when I try to turn the stream received at ServerSocket into a picture again. Error message "Bitmap Image is not valid!" when performing:
DesktopForm.imgScreen.Picture.Bitmap.LoadFromStream(ms);
I do not know if the problem is in the way sending the image or get in the way.
My server code:
unit UntThreadDesktop;
interface
uses
System.Classes,
System.SysUtils,
System.Win.ScktComp,
WinApi.Windows,
WinApi.ActiveX,
Vcl.Graphics,
Vcl.Imaging.Jpeg,
UntDesktopForm;
type
TThreadDesktop = class(TThread)
private
FSocket: TCustomWinSocket;
FDesktopForm: TDesktopForm;
public
constructor Create(ASocket: TCustomWinSocket);
destructor Destroy; override;
procedure Execute; override;
end;
implementation
uses
UntLibraries;
{ TThreadDesktop }
constructor TThreadDesktop.Create(ASocket: TCustomWinSocket);
begin
inherited Create(true);
FreeOnTerminate := true;
FSocket := ASocket;
end;
destructor TThreadDesktop.Destroy;
begin
inherited;
end;
procedure TThreadDesktop.Execute;
var
text: string;
fileSize: integer;
ms: TMemoryStream;
buf: Pointer;
nBytes: integer;
jpg: TJPEGImage;
begin
inherited;
CoInitialize(nil);
try
// Init DesktopForm
Synchronize(procedure begin
FDesktopForm := TDesktopForm.Create;
FDesktopForm.Show;
end);
ms := TMemoryStream.Create;
try
FSocket.SendText('<|GetScreen|>');
while FSocket.Connected and (not Self.Terminated) and (FDesktopForm <> nil) do
begin
if FSocket.ReceiveLength > 0 then
begin
ms.Clear;
text := string(FSocket.ReceiveText);
text := Copy(text,1, Pos(#0,text)-1);
fileSize := StrToInt(text);
// Receiving file
while FSocket.Connected and (not Self.Terminated) and (FDesktopForm <> nil) do
begin
Synchronize(procedure begin
if FDesktopForm <> nil then
FDesktopForm.panInfo.Caption := 'Total: ' + IntToStr(ms.Size) +
' de ' + IntToStr(fileSize);
end);
try
text := '';
GetMem(buf, FSocket.ReceiveLength);
try
nBytes := FSocket.ReceiveBuf(buf^, FSocket.ReceiveLength);
if nBytes > 0 then
ms.Write(buf^, nBytes);
if (ms.Size = fileSize) or (nBytes <= 0) then
begin
ms.Position := 0;
ms.SaveToFile('C:\Temp\Screen.bmp');
ms.Position := 0;
//jpg := TJPEGImage.Create;
//jpg.LoadFromStream(ms);
// Carrega a imagem
Synchronize(procedure begin
if FDesktopForm <> nil then
//FDesktopForm.imgScreen.Picture.Assign(jpg);
FDesktopForm.imgScreen.Picture.Graphic.LoadFromStream(ms);
end);
end;
finally
FreeMem(buf);
end;
except
end;
end;
end;
TThread.Sleep(10);
end;
finally
ms.Free;
// Close DesktopForm
Synchronize(procedure begin
if FDesktopForm <> nil then
FDesktopForm.Close;
end);
end;
finally
CoUninitialize;
end;
end;
end.
It´s a thread used to receive the image in background.
In the main form of my application server I own a TServerSocket component working with the ServerType property to stThreadBlocking.
In my client application I have TClientSocket component using the property ClientType as ctNonBlocking.
My thread code:
unit UntThreadDesktopClient;
interface
uses
System.Classes,
System.SysUtils,
System.Win.ScktComp,
WinApi.Windows,
WinApi.ActiveX,
Vcl.Imaging.Jpeg,
Vcl.Graphics,
Vcl.Forms;
type
TThreadDesktopClient = class(TThread)
private
FSocket: TClientSocket;
FStream: TMemoryStream;
public
constructor Create(AHostname: string; APort: integer); reintroduce;
destructor Destroy; override;
procedure Execute; override;
private
procedure OnConnect(Sender: TObject; Socket: TCustomWinSocket);
procedure GetScreen(stream: TMemoryStream);
end;
implementation
{ TThreadDesktopClient }
constructor TThreadDesktopClient.Create(AHostname: string; APort: integer);
begin
inherited Create(true);
FreeOnTerminate := true;
FStream := TMemoryStream.Create;
FSocket := TClientSocket.Create(nil);
FSocket.ClientType := ctNonBlocking;
FSocket.Host := AHostname;
FSocket.Port := APort;
FSocket.OnConnect := OnConnect;
FSocket.Open;
end;
destructor TThreadDesktopClient.Destroy;
begin
FStream.Free;
if FSocket.Active then
FSocket.Close;
FSocket.Free;
inherited;
end;
procedure TThreadDesktopClient.Execute;
var
cmd: AnsiString;
begin
inherited;
CoInitialize(nil);
try
while FSocket.Active and not Self.Terminated do
begin
if FSocket.Socket.ReceiveLength > 0 then
begin
cmd := FSocket.Socket.ReceiveText;
if cmd = '<|GetScreen|>' then
begin
FStream.Clear;
GetScreen(FStream);
FStream.Position := 0;
FSocket.Socket.SendText(AnsiString(IntToStr(FStream.Size)) + #0);
FSocket.Socket.SendStream(FStream);
end
else
if cmd = '<|TYPE|>' then
begin
FSocket.Socket.SendText('<|TYPE-DESKTOP|>');
end;
end;
end;
finally
CoUninitialize;
end;
end;
procedure TThreadDesktopClient.OnConnect(Sender: TObject; Socket: TCustomWinSocket);
begin
Start;
end;
procedure TThreadDesktopClient.GetScreen(stream: TMemoryStream);
var
DC: HDC;
bmp: TBitmap;
jpg: TJPEGImage;
begin
DC := GetDC(GetDesktopWindow);
try
bmp := TBitmap.Create;
jpg := TJPEGImage.Create;
try
//bmp.PixelFormat := pf8bit;
bmp.Width := GetDeviceCaps(DC, HORZRES);
bmp.Height := GetDeviceCaps(DC, VERTRES);
//bmp.Width := Screen.Width;
//bmp.Height := Screen.Height;
BitBlt(bmp.Canvas.Handle, 0, 0, bmp.Width, bmp.Height, DC, 0, 0, SRCCOPY);
bmp.Modified := True;
//jpg.Assign(bmp);
//jpg.Compress;
stream.Clear;
//jpg.SaveToStream(stream);
bmp.SaveToStream(stream);
finally
bmp.Free;
jpg.Free;
end;
finally
ReleaseDC(GetDesktopWindow, DC);
end;
end;
end.
For further clarification, I will also post my main thread of the client application and how it is called in the main form from my client application.
unit UntThreadMain;
interface
uses
System.Classes,
System.Win.ScktComp,
WinApi.ActiveX;
type
TThreadMain = class(TThread)
private
FClientSocket: TClientSocket;
public
constructor Create(AHostname: string; APort: integer); reintroduce;
destructor Destroy; override;
procedure Execute; override;
public
procedure OnConnect(Sender: TObject; Socket: TCustomWinSocket);
procedure OnDisconnect(Sender: TObject; Socket: TCustomWinSocket);
procedure OnError(Sender: TObject; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
private
procedure SendInfo;
procedure OpenDesktopChannel;
end;
implementation
uses
UntClientMainForm,
UntThreadDesktopClient;
{ TThreadMain }
constructor TThreadMain.Create(AHostname: string; APort: integer);
begin
inherited Create(true);
FreeOnTerminate := false;
FClientSocket := TClientSocket.Create(nil);
FClientSocket.ClientType := ctNonBlocking;
FClientSocket.Host := AHostname;
FClientSocket.Port := APort;
FClientSocket.OnConnect := OnConnect;
FClientSocket.OnDisconnect := OnDisconnect;
FClientSocket.Open;
end;
destructor TThreadMain.Destroy;
begin
if FClientSocket.Active then
FClientSocket.Close;
FClientSocket.Free;
inherited;
end;
procedure TThreadMain.Execute;
var
cmd: AnsiString;
begin
inherited;
CoInitialize(nil);
try
while FClientSocket.Socket.Connected and not Self.Terminated do
begin
if FClientSocket.Socket.ReceiveLength > 0 then
begin
cmd := FClientSocket.Socket.ReceiveText;
if cmd = '<|TYPE|>' then
FClientSocket.Socket.SendText('<|TYPE-COMMAND|>')
else
if cmd = '<|INFO|>' then
SendInfo
else
if cmd = '<|REQUEST-DESKTOP|>' then
TThreadDesktopClient.Create(FClientSocket.Host, FClientSocket.Port);
end;
end;
finally
CoUninitialize;
end;
end;
procedure TThreadMain.OnConnect(Sender: TObject; Socket: TCustomWinSocket);
begin
Start;
Synchronize(procedure
begin
ClientMainForm.stBar.Panels[1].Text := 'Conectado';
ClientMainForm.btnConectar.Caption := 'Desconectar';
end);
end;
procedure TThreadMain.OnDisconnect(Sender: TObject; Socket: TCustomWinSocket);
begin
Synchronize(procedure
begin
ClientMainForm.stBar.Panels[1].Text := 'Desconectado';
ClientMainForm.btnConectar.Caption := 'Conectar';
end);
end;
procedure TThreadMain.OnError(Sender: TObject; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
ErrorCode := 0;
end;
procedure TThreadMain.SendInfo;
var
cmd: AnsiString;
begin
cmd := '<|INFO|>;NomePC=Tiago-PC;SO=Windows Seven Professiona 64-bit;' +
'CPU=Intel Core i7 3ª Geração';
FClientSocket.Socket.SendText(cmd);
end;
end.
Note that this thread calls the TThreadDesktopClient.
In the main form of the application server, where the TServerSocket, got OnGetThread TServerSocket the method this way:
procedure TMainForm.ServerSocketGetThread(Sender: TObject;
ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread);
begin
SocketThread := TThreadController.Create(false, ClientSocket);
end;
When an image is requested:
procedure TMainForm.pmiAcessarClick(Sender: TObject);
var
nI: integer;
begin
for nI := 0 to Pred(ServerSocket.Socket.ActiveConnections) do
begin
if ServerSocket.Socket.Connections[nI].SocketHandle = cdsClientesId.AsInteger then
ServerSocket.Socket.Connections[nI].SendText('<|REQUEST-DESKTOP|>');
end;
end;
Returning to my client application, this code is used to connect in server (TServerSocket).
procedure TClientMainForm.btnConectarClick(Sender: TObject);
begin
if FThreadMain = nil then
begin
FThreadMain := TThreadMain.Create('localhost', 6550);
end
else
begin
FThreadMain.Terminate;
FThreadMain.Free;
FThreadMain := nil;
end;
end;
So, this is all my code.
When an image is received, I try to load it on TImage get the error message: "Bitmap Image is not valid."
I've tried a few different ways to treat the stream sent by the client application. But it still fails.
Usually got the same error: "Bitmap Image is not valid."
There are a LOT of problems with the code you showed - ranging from a fundamental lack of understanding of how TClientSocket and TServerSocket actually work in general, to a lack of understanding of how to send/receive/parse over TCP/IP. I see very few things in your code that are correct.
You are creating multiple connections on the client side, making each one identify its type (command vs desktop), but your server code is not querying that type or even caring what the type is. It assumes every client is a desktop client and asks for its screen. So you can simplify your code on both sides by simply eliminating that second connection. It is not really needed anyway. You would keep your connections to a minimum to reduce overhead.
I would strongly suggest a re-write of your code.
Try something more like this instead:
Common:
unit UntSocketCommon;
uses
System.Classes,
System.Win.ScktComp;
interface
procedure ReadRawFromSocket(Socket: TWinSocketStream; Buf: Pointer; BufLen: Integer);
function ReadLineFromSocket(Socket: TWinSocketStream): String;
function ReadIntegerFromSocket(Socket: TWinSocketStream): Integer;
procedure ReadStreamFromSocket(Socket: TWinSocketStream; Stream: TStream);
procedure WriteRawFromSocket(Socket: TWinSocketStream; Buf: Pointer; BufLen: Integer);
procedure WriteLineToSocket(Socket: TWinSocketStream; const Value: String);
procedure WriteIntegerToSocket(Socket: TWinSocketStream; Value: Integer);
procedure WriteStreamToSocket(Socket: TWinSocketStream; Stream: TStream);
implementation
procedure ReadRawFromSocket(Socket: TWinSocketStream; Buf: Pointer; BufLen: Integer);
var
PBuf: PByte;
nBytesRead: Integer;
begin
PBuf := PByte(Buf);
while BufLen > 0 do
begin
nBytesRead := Socket.Read(PBuf^, BufLen);
if nBytesRead < 1 then raise Exception.Create('Unable to read from socket');
Inc(PBuf, nBytesRead);
Dec(BufLen, nBytesRead);
end;
end;
function ReadLineFromSocket(Socket: TWinSocketStream): String;
var
Ch: AnsiChar;
Buf: array[0..255] of AnsiChar;
BufLen: Integer;
S: UTF8String;
procedure AppendBuf;
var
OldLen: Integer;
begin
OldLen := Length(S);
SetLength(S, OldLen + BufLen);
Move(Buf[0], S[OldLen], BufLen);
end;
begin
Result := '';
BufLen := 0;
repeat
ReadRawFromSocket(Socket, #Ch, SizeOf(Ch));
if Ch = #10 then Break;
if BufLen = Length(Buf) then
begin
AppendBuf;
BufLen := 0;
end;
Buf[BufLen] := Ch;
Inc(BufLen);
until False;
if BufLen > 0 then AppendBuf;
BufLen := Length(S);
if BufLen > 0 then
begin
if S[BufLen] = #13 then
SetLength(S, BufLen-1);
end;
Result := String(S);
end;
function ReadIntegerFromSocket(Socket: TWinSocketStream): Integer;
begin
ReadRawFromSocket(Socket, #Result, SizeOf(Result));
Result := ntohl(Result);
end;
procedure ReadStreamFromSocket(Socket: TWinSocketStream; Stream: TStream);
var
Size: Integer;
Buf: array[0..1023] of Byte;
nBytes: Integer;
begin
Size := ReadIntegerFromSocket(Socket);
while Size > 0 do
begin
nBytes := Size;
if nBytes > Length(Buf) then nBytes := Length(Buf);
ReadRawFromSocket(Socket, Buf[0], nBytes);
Stream.WriteBuffer(Buf[0], nBytes);
Dec(Size, nBytes);
end;
end;
procedure WriteRawToSocket(Socket: TWinSocketStream; Buf: Pointer; BufLen: Integer);
var
PBuf: PByte;
nBytesWritten: Integer;
begin
PBuf := PByte(Buf);
while BufLen > 0 do
begin
nBytesWritten := Socket.Write(PBuf^, BufLen);
if nBytesWritten < 1 then raise Exception.Create('Unable to write to socket');
Inc(PBuf, nBytesWritten);
Dec(BufLen, nBytesWritten);
end;
end;
procedure WriteLineToSocket(Socket: TWinSocketStream; const Value: String);
var
S: UTF8String;
begin
S := UTF8String(Value + #13#10);
WriteRawToSocket(Socket, PAnsiChar(S), Length(S));
end;
procedure WriteIntegerToSocket(Socket: TWinSocketStream; Value: Integer);
begin
Value := htonl(Value);
WriteRawToSocket(Socket, #Value, SizeOf(Value));
end;
procedure WriteStreamToSocket(Socket: TWinSocketStream; Stream: TStream);
var
Size: Integer;
Buf: array[0..1023] of Byte;
nBytes: Integer;
begin
Size := Stream.Size - Stream.Position;
WriteIntegerToSocket(Socket, Size);
while Size > 0 do
begin
nBytes := Size;
if nBytes > Length(Buf) then nBytes := Length(Buf);
Stream.ReadBuffer(Buf[0], nBytes);
WriteRawToSocket(Socket, Buf[0], nBytes);
Dec(Size, nBytes);
end;
end;
end.
Server:
unit UntThreadDesktop;
interface
uses
System.Classes,
System.Win.ScktComp,
UntDesktopForm;
type
TThreadController = class(TServerClientThread)
private
FDesktopForm: TDesktopForm;
protected
procedure ClientExecute; override;
end;
implementation
uses
System.SysUtils,
WinApi.Windows,
Vcl.Graphics,
UntLibraries,
UntSocketCommon;
{ TThreadDesktop }
procedure TThreadController.ClientExecute;
var
fileSize: Integer;
ms: TMemoryStream;
buf: array[0..1023] of Byte;
nBytes: Integer;
SocketStrm: TWinSocketStream;
begin
SocketStrm := TWinSocketStream.Create(ClientSocket, 5000);
try
// Init DesktopForm
Synchronize(
procedure
begin
FDesktopForm := TDesktopForm.Create;
FDesktopForm.Show;
end
);
try
ms := TMemoryStream.Create;
try
while ClientSocket.Connected and (not Terminated) and (FDesktopForm <> nil) do
begin
ms.Clear;
WriteLineToSocket(SocketStrm, '<|GetScreen|>');
{
ReadStreamFromSocket(SocketStrm, ms);
ms.Position := 0;
ms.SaveToFile('C:\Temp\Screen.bmp');
ms.Position := 0;
Synchronize(
procedure
begin
if FDesktopForm <> nil then
FDesktopForm.imgScreen.Picture.Bitmap.LoadFromStream(ms);
end
);
}
fileSize := ReadIntegerFromSocket(SocketStrm);
while (ms.Size < fileSize) and ClientSocket.Connected and (not Terminated) and (FDesktopForm <> nil) do
begin
Synchronize(
procedure
begin
if FDesktopForm <> nil then
FDesktopForm.panInfo.Caption := 'Total: ' + IntToStr(ms.Size) + ' de ' + IntToStr(fileSize);
end
);
nBytes := fileSize - ms.Size;
if nBytes > Length(Buf) then nBytes := Length(Buf);
ReadRawFromSocket(SocketStrm, buf[0], nBytes);
ms.WriteBuffer(buf[0], nBytes);
if ms.Size = fileSize then
begin
ms.Position := 0;
ms.SaveToFile('C:\Temp\Screen.bmp');
ms.Position := 0;
Synchronize(
procedure
begin
if FDesktopForm <> nil then
FDesktopForm.imgScreen.Picture.Bitmap.LoadFromStream(ms);
end
);
end;
end;
end;
finally
ms.Free;
end;
finally
Synchronize(
procedure
begin
if FDesktopForm <> nil then
FDesktopForm.Close;
end
);
end;
finally
SocketStrm.Free;
end;
end;
end.
procedure TMainForm.ServerSocketGetThread(Sender: TObject;
ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread);
begin
SocketThread := TThreadController.Create(false, ClientSocket);
end;
Client:
unit UntThreadMain;
interface
uses
System.Classes,
System.Win.ScktComp;
type
TThreadMain = class(TThread)
private
FClientSocket: TClientSocket;
FSocketStrm: TWinSocketStream;
procedure SendInfo;
procedure SendScreen;
procedure OnConnect(Sender: TObject; Socket: TCustomWinSocket);
procedure OnDisconnect(Sender: TObject; Socket: TCustomWinSocket);
procedure OnError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer);
protected
procedure Execute; override;
public
constructor Create(AHostname: string; APort: integer); reintroduce;
destructor Destroy; override;
end;
implementation
uses
System.SysUtils,
WinApi.Windows,
Vcl.Graphics,
UntClientMainForm,
UntSocketCommon;
{ TThreadMain }
constructor TThreadMain.Create(AHostname: string; APort: integer);
begin
inherited Create(false);
FreeOnTerminate := false;
FClientSocket := TClientSocket.Create(nil);
FClientSocket.ClientType := ctBlocking;
FClientSocket.Host := AHostname;
FClientSocket.Port := APort;
FClientSocket.OnConnect := OnConnect;
FClientSocket.OnDisconnect := OnDisconnect;
FClientSocket.OnError := OnError;
end;
destructor TThreadMain.Destroy;
begin
FClientSocket.Free;
inherited;
end;
procedure TThreadMain.Execute;
var
SocketStrm: TWinSocketStream;
cmd: String;
begin
FClientSocket.Open;
try
FSocketStrm := TWinSocketStream.Create(FClientSocket.Socket, 5000);
try
while FClientSocket.Socket.Connected and (not Terminated) do
begin
if SocketStrm.WaitForData(1000) then
begin
cmd := ReadLineFromSocket(SocketStrm);
if cmd = '<|INFO|>' then
begin
SendInfo
end
else if cmd = '<|GetScreen|>' then
begin
SendScreen;
end
end;
end;
finally
FSocketStrm.Free;
end;
finally
FClientSocket.Close;
end;
end;
procedure TThreadMain.OnConnect(Sender: TObject; Socket: TCustomWinSocket);
begin
Synchronize(
procedure
begin
ClientMainForm.stBar.Panels[1].Text := 'Conectado';
ClientMainForm.btnConectar.Caption := 'Desconectar';
end
);
end;
procedure TThreadMain.OnDisconnect(Sender: TObject; Socket: TCustomWinSocket);
begin
Synchronize(
procedure
begin
ClientMainForm.stBar.Panels[1].Text := 'Desconectado';
ClientMainForm.btnConectar.Caption := 'Conectar';
end
);
end;
procedure TThreadMain.OnError(Sender: TObject; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
ErrorCode := 0;
Socket.Close;
end;
procedure TThreadMain.SendInfo;
var
cmd: string;
begin
cmd := '<|INFO|>;NomePC=Tiago-PC;SO=Windows Seven Professiona 64-bit;CPU=Intel Core i7 3ª Geração';
WriteLineToSocket(FSocketStrm, cmd);
end;
procedure TThreadMain.SendScreen;
var
DC: HDC;
bmp: TBitmap;
ms: TMemoryStream;
begin
ms := TMemoryStream.Create;
try
bmp := TBitmap.Create;
try
DC := GetDC(0);
try
//bmp.PixelFormat := pf8bit;
bmp.Width := GetDeviceCaps(DC, HORZRES);
bmp.Height := GetDeviceCaps(DC, VERTRES);
//bmp.Width := Screen.Width;
//bmp.Height := Screen.Height;
BitBlt(bmp.Canvas.Handle, 0, 0, bmp.Width, bmp.Height, DC, 0, 0, SRCCOPY);
finally
ReleaseDC(0, DC);
end;
bmp.SaveToStream(ms);
finally
bmp.Free;
end;
ms.Position := 0;
WriteStreamToSocket(FSocketStrm, ms);
finally
ms.Free;
end;
end;
end.
procedure TClientMainForm.btnConectarClick(Sender: TObject);
begin
if FThreadMain = nil then
begin
FThreadMain := TThreadMain.Create('localhost', 6550);
end else
begin
FThreadMain.Terminate;
FThreadMain.WaitFor;
FThreadMain.Free;
FThreadMain := nil;
end;
end;
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.