Delphi Apache Link Module request canceled out of memory - apache

i have developed an Apache link module in Delphi, which also contains an upload function via a WebActionItem(Multipart form data).
The upload also works correctly as long as the client does not cancel the request.
However, if the request is aborted, the memory of the httpd.exe will increase continuously.
Within a few seconds the memory reaches the maximum 8 GB and the child-process of Apache is killed.
The strange thing is that my upload function is not reached at all if a request get canceled by the client.
Only the initialization of the WebModule is called when a request comes in.
My questions:
Does Apache take care of the upload of the file and passes it to the Delphi WebModule?
How can I intervene if my upload function in the WebModule is not called at all when a request is aborted by the client?
I am very grateful for any hints, because I'm looking for a solution since hours.
Edit:
Example for dpr-File:
library mod_restserver;
uses
{$IFDEF MSWINDOWS}
Winapi.ActiveX,
System.Win.ComObj,
{$ENDIF }
Web.WebBroker,
Web.ApacheApp,
Web.HTTPD24Impl,
Data.DBXCommon,
Datasnap.DSSession,
RESTServer.Service.WebModules in 'RESTServer.Service.WebModules.pas' {webModul: TWebModule};
// httpd.conf-Einträge:
//
(*
LoadModule webbroker_module modules/mod_restserver.dll
<Location /rest>
SetHandler mod_restserver-handler
</Location>
*)
//
// Diese Einträge setzen voraus, dass das Ausgabeverzeichnis für dieses Projekt das apache/modules-Verzeichnis ist.
//
// httpd.conf-Einträge sollten unterschiedlich sein, wenn das Projekt auf eine der folgenden Weisen geändert wird:
// 1. Der Name der Variable TApacheModuleData wird geändert.
// 2. Das Projekt wird umbenannt.
// 3. Das Ausgabeverzeichnis ist nicht das Verzeichnis apache/modules.
// 4. Die Erweiterung der dynamischen Bibliothek ist von der Plattform abhängig. Verwenden Sie für Windows .dll und für Linux .so.
//
// Exportierte Variable deklarieren, damit Apache auf dieses Modul zugreifen kann.
var
GModuleData: TApacheModuleData;
exports
GModuleData name 'webbroker_module';
procedure TerminateThreads;
begin
TDSSessionManager.Instance.Free;
Data.DBXCommon.TDBXScheduler.Instance.Free;
end;
begin
{$IFDEF MSWINDOWS}
CoInitFlags := COINIT_MULTITHREADED;
{$ENDIF}
Web.ApacheApp.InitApplication(#GModuleData);
Application.Initialize;
Application.WebModuleClass := WebModuleClass;
TApacheApplication(Application).OnTerminate := TerminateThreads;
Application.Run;
end.
And example for the webmodule:
unit RESTServer.Service.WebModules;
interface
uses
{$IFDEF MSWINDOWS}
Winapi.ActiveX,
System.Win.ComObj,
{$ENDIF }
System.SysUtils, System.Classes,
Web.HTTPApp, Web.WebFileDispatcher, Web.HTTPProd,
Datasnap.DSHTTPWebBroker, Datasnap.DSServer, DataSnap.DSAuth, Datasnap.DSCommonServer,
IPPeerServer, IdContext, Datasnap.DSHTTP, ReqMulti, JSON, System.IOUtils;
type
TwebModul = class(TWebModule)
dsServer: TDSServer;
procedure WebModule1DefaultHandlerAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
procedure webModulfileUploadAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
var
WebModuleClass: TComponentClass = TwebModul;
implementation
{$R *.dfm}
uses
Web.WebReq, Datasnap.DSSession;
procedure TwebModul.WebModule1DefaultHandlerAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
Response.Content :=
'{"return":-1, "result":0, "msg":"ressource unknown"}';
end;
procedure TwebModul.webModulfileUploadAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
joResponse: TJSONObject;
iCount,
iReturn, iResult: Integer;
sPath, s: String;
aFile: TAbstractWebRequestFile;
ms: TMemoryStream;
begin
joResponse := TJSONObject.Create;
iReturn := 0;
iResult := 0;
try
try
if Request.Files.Count > 0 then
begin
for iCount := 0 to Request.Files.Count - 1 do
begin
aFile := Request.Files.Items[iCount];
ms := TMemoryStream.Create;
try
sPath := 'C:\Data\test.txt';
//if not DirectoryExists(sPath) then
// TDirectory.CreateDirectory(sPath);
aFile.Stream.Position := 0;
ms.CopyFrom(aFile.Stream, aFile.Stream.Size);
ms.SaveToFile(sPath);
finally
ms.free;
end;
Inc(iResult);
end;
end;
except
on E: Exception do
begin
s := E.Message;
iReturn := 2;
iResult := 0;
end;
end;
finally
joResponse.AddPair(TJSONPair.Create('return', TJSONNumber.Create(iReturn)));
joResponse.AddPair(TJSONPair.Create('result', TJSONNumber.Create(iResult)));
Response.ContentType := 'application/json; charset=utf-8';
Response.Content := joResponse.ToJSON;
end;
end;
initialization
CoInitialize(nil);
finalization
CoUninitialize;
end.

Related

Receiving 400 Bad request when making S3 GET request

I am trying to make an AWS version 4 authorization signed GET request to S3, and receive a bad request error 400 Code:InvalidRequest Message:Missing required header for this request: x-amz-content-sha256
If I prefix the header with "Authorization: ", I get error Code:InvalidArgument Message:Unsupported Authorization Type <ArgumentName>Authorization</ArgumentName> <ArgumentValue>Authorization: AWS4-HMAC-SHA256 Credential=XXXXXXXXXXXXXXXXXXX/20200408/eu-west-3/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=vdchzint97uwyt3g%2fjehszrc8zpkbjsx4tfqacsqfow%3d</ArgumentValue>
I'm using Delphi XE5 with Indy's TIdHTTP component. Can anyone tell me what I am doing wrong? I have included my code below.
begin
bucket := 'mybucket.ata-test';
obj := 'test.xml';
region := 'eu-west-3';
service := 's3';
aws := 'amazonaws.com';
YYYYMMDD := FormatDateTime('yyyymmdd', now);
amzDate := FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(Now), TFormatSettings.Create('en-US'));
emptyHash := lowercase(SHA256HashAsHex(''));
host := Format('%s.%s.%s.%s', [bucket, service, region, aws]);
url := Format('%s://%s.%s.%s.%s/%s', ['https', bucket, service, region, aws, obj]);
// *** 1. Build the Canonical Request for Signature Version 4 ***
// HTTPRequestMethod
CanonicalRequest := URLEncodeValue('GET') +#10;
// CanonicalURI
CanonicalRequest := CanonicalRequest + '/' + URLEncodeValue(obj) +#10;
// CanonicalQueryString (empty just a newline)
CanonicalRequest := CanonicalRequest +#10;
// CanonicalHeaders
CanonicalRequest := CanonicalRequest + 'host:' + Trim(host) +#10
+ 'x-amz-content-sha256:' + emptyHash +#10
+ 'x-amz-date:' + Trim(amzDate) +#10;
// SignedHeaders
CanonicalRequest := CanonicalRequest + 'host;x-amz-content-sha256;x-amz-date' +#10;
// HexEncode(Hash(RequestPayload)) - (hash of an empty string)
CanonicalRequest := CanonicalRequest + emptyHash;
// *** 2. Create a String to Sign for Signature Version 4 ***
StringToSign := 'AWS4-HMAC-SHA256' +#10
+ amzDate +#10
+ UTF8String(YYYYMMDD) +'/'+ UTF8String(region) +'/'+ UTF8String(service) +UTF8String('/aws4_request') +#10
+ lowercase(SHA256HashAsHex(CanonicalRequest));
// *** 3. Calculate the Signature for AWS Signature Version 4 ***
DateKey := CalculateHMACSHA256(YYYYMMDD, 'AWS4' + SecretAccessKey);
DateRegionKey := CalculateHMACSHA256(region, DateKey);
DateRegionServiceKey := CalculateHMACSHA256(service, DateRegionKey);
SigningKey := CalculateHMACSHA256('aws4_request', DateRegionServiceKey);
Signature := lowercase(UrlEncodeValue(CalculateHMACSHA256(StringToSign, SigningKey)));
// *** 4. Create Authorisation Header and Add the Signature to the HTTP Request ***
AuthorisationHeader := 'AWS4-HMAC-SHA256 Credential='+AccessIdKey+'/'+YYYYMMDD+'/'+region+'/'+service+'/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature='+signature;
// (Gives <Code>InvalidRequest</Code> <Message>Missing required header for this request: x-amz-content-sha256</Message>)
// Have also tried
// AuthorisationHeader := 'Authorization: AWS4-HMAC-SHA256 Credential='+AccessIdKey+'/'+YYYYMMDD+'/'+region+'/'+service+'/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature='+signature;
// (Gives <Code>InvalidArgument</Code> <Message>Unsupported Authorization Type</Message>)
// *** 5. Add Header and Make Request ***
stm := TMemoryStream.Create;
try
try
Idhttp.Request.CustomHeaders.FoldLines := False;
Idhttp.Request.CustomHeaders.AddValue('Authorization', AuthorisationHeader);
Idhttp.Get(URL, stm);
except
on PE: EIdHTTPProtocolException do begin
s := PE.ErrorMessage;
Raise;
end;
on E: Exception do begin
s := E.Message;
Raise;
end;
end;
stm.Position := 0;
Memo1.Lines.LoadFromStream(stm);
finally
FreeAndNil(stm);
end;
end;
function SHA256HashAsHex(const value: string): String;
/// used for stringtosign
var
sha: TIdHashSHA256;
begin
LoadOpenSSLLibrary;
if not TIdHashSHA256.IsAvailable then
raise Exception.Create('SHA256 hashing is not available!');
sha := TIdHashSHA256.Create;
try
result := sha.HashStringAsHex(value, nil);
finally
sha.Free;
end;
end;
function CalculateHMACSHA256(const value, salt: String): String;
/// used for signingkey
var
hmac: TIdHMACSHA256;
hash: TIdBytes;
begin
LoadOpenSSLLibrary;
if not TIdHashSHA256.IsAvailable then
raise Exception.Create('SHA256 hashing is not available!');
hmac := TIdHMACSHA256.Create;
try
hmac.Key := IndyTextEncoding_UTF8.GetBytes(salt);
hash := hmac.HashValue(IndyTextEncoding_UTF8.GetBytes(value));
Result := EncodeBytes64(TArray<Byte>(hash));
finally
hmac.Free;
end;
end;
A few things I notice in your code:
when creating the YYYYMMDD and amzDate values, you are calling Now() twice, which creates a race condition that has the potential of causing those variables to represent different dates. Unlikely, but possible. To avoid that, you should call Now() only 1 time and save the result to a local TDateTime variable, and then use that variable in all of your FormatDateTime() calls.
dtNow := Now();
YYYYMMDD := FormatDateTime('yyyymmdd', dtNow);
amzDate := FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(dtNow), TFormatSettings.Create('en-US'));
When using TIdHTTP's Request.CustomHeaders property to set a custom Authorization header, make sure that you also set the Request.BasicAuthentication property to False as well, otherwise TIdHTTP may create its own Authorization: Basic ... header using its Request.Username and Request.Password properties. You don't want two Authorization headers in your GET request.
Idhttp.Request.BasicAuthentication := False;
You are using x-amz-content-sha256 and x-amz-date headers in your authorization calculations, but you are not adding those headers to the actual HTTP request. TIdHTTP will add the Host header for you, but you need to add the other headers yourself.
Idhttp.Request.CustomHeaders.AddValue('x-amz-content-sha256', emptyHash);
Idhttp.Request.CustomHeaders.AddValue('x-amz-date', amzDate);
Your SHA256HashAsHex() function is not specifying a byte encoding when calling Indy's TIdHashSHA256.HashStringAsHex() method (in fact, it is going out of its way to explicitly set the encoding to nil). As such, Indy's default byte encoding will be used, which is US-ASCII (unless you set Indy's GIdDefaultTextEncoding variable in the IdGlobal unit to something else). However, your CalculateHMACSHA256() function is explicitly using UTF-8 instead. Your SHA256HashAsHex() function should use IndyTextEncoding_UTF8 to match:
result := sha.HashStringAsHex(value, IndyTextEncoding_UTF8);
the input salt and output value for CalculateHMACSHA256() needs to be binary bytes, not strings, and certainly not base64-encoded or hex-encoded strings. Nothing in the Calculate the Signature for AWS Signature Version 4 documentation mentions the use of base64 at all.
var
DateKey, RegionKey, ServiceKey, SigningKey: TArray<Byte>;
...
// *** 3. Calculate the Signature for AWS Signature Version 4 ***
DateKey := CalculateHMACSHA256(YYYYMMDD, TEncoding.UTF8.GetBytes('AWS4' + SecretAccessKey));
RegionKey := CalculateHMACSHA256(region, DateKey);
ServiceKey := CalculateHMACSHA256(service, RegionKey);
SigningKey := CalculateHMACSHA256('aws4_request', ServiceKey);
Signature := CalculateHMACSHA256Hex(StringToSign, SigningKey);
...
function CalculateHMACSHA256(const value: string; const salt: TArray<Byte>): TArray<Byte>;
/// used for signingkey
var
hmac: TIdHMACSHA256;
hash: TIdBytes;
begin
LoadOpenSSLLibrary;
if not TIdHashSHA256.IsAvailable then
raise Exception.Create('SHA256 hashing is not available!');
hmac := TIdHMACSHA256.Create;
try
hmac.Key := TIdBytes(salt);
hash := hmac.HashValue(IndyTextEncoding_UTF8.GetBytes(value));
Result := TArray<Byte>(hash);
finally
hmac.Free;
end;
end;
function CalculateHMACSHA256Hex(const value: string; const salt: TArray<Byte>): string;
var
hash: TArray<Byte>;
begin
hash := CalculateHMACSHA256(value, salt)
Result := lowercase(ToHex(TIdBytes(hash)));
end;
I just went through a similar problem, I will leave my contribution here, because this post helped me to arrive at the solution.
In my case I needed to generate a signed url with a certain expiration time.
unit Data.Cloud.AmazonAPI.Utils;
interface
//See https://docs.aws.amazon.com/pt_br/AmazonS3/latest/API/sigv4-query-string-auth.html#query-string-auth-v4-signing-example
//Use example:
// MyUlrSigned := GetUrlPreSigned('mybucketname','/MyFolder/MyFile.zip','sa-east-1',3600);
function GetUrlPreSigned(ABucket:string;AObjectName:string;ARegion:string;AExpiresIn:Int64=3600):string;
implementation
uses
System.Classes, System.SysUtils, System.Hash, System.DateUtils;
const
AWS_ACCOUNTNAME = '<AWSAccessKeyId>';
AWS_ACCOUNTKEY = '<AWSSecretAccessKey>';
function SignString(const Signkey: TBytes; const StringToSign: string): TBytes;
begin
Result := THashSHA2.GetHMACAsBytes(StringToSign, Signkey);
end;
function BuildSignature(const StringToSign, DateISO, Region:string; AService: string; LSecretAccessKey:string): string;
function GetSignatureKey(const datestamp, region, serviceName: string): TBytes;
begin
Result := SignString(TEncoding.Default.GetBytes('AWS4'+LSecretAccessKey) ,datestamp);
Result := SignString(Result, region);
Result := SignString(Result, serviceName);
Result := SignString(Result, 'aws4_request');
end;
var
Signature:string;
SigningKey : TBytes;
begin
SigningKey := GetSignatureKey(DateISO, Region, AService);
Result := THash.DigestAsString(SignString(SigningKey, StringToSign));
end;
function GetHashSHA256Hex( HashString: string): string;
var
LBytes: TArray<Byte>;
begin
LBytes := THashSHA2.GetHashBytes(HashString);
Result := THash.DigestAsString(LBytes);
end;
function GetUrlPreSigned(ABucket:string;AObjectName:string;ARegion:string;AExpiresIn:Int64=3600):string;
var
LNow : TDateTime;
LData : string;
LTimeStamp : string;
LAccessKey : string;
LSecretAccessKey : string;
LService : string;
LAws : string;
LHost : string;
LUrl : string;
LQueryParams : string;
LCanonicalRequest : string;
LStringToSign : string;
LSignature : string;
begin
LNow := Now();
LData := FormatDateTime('yyyymmdd', LNow);
LTimeStamp := FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(LNow), TFormatSettings.Create('en-US'));
LAccessKey := AWS_ACCOUNTNAME;
LSecretAccessKey := AWS_ACCOUNTKEY;
if AObjectName.StartsWith('/') then
Delete(AObjectName,1,1);
LService := 's3';
LAws := 'amazonaws.com';
LHost := Format('%s-%s.%s', [LService, ARegion, LAws]);
LUrl := Format('%s://%s-%s.%s/%s/%s', ['https', LService, ARegion, LAws, ABucket, AObjectName]);
LQueryParams := 'X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential='+LAccessKey
+'%2F'+LData
+'%2F'+ARegion
+'%2F'+LService+'%2F'
+'aws4_request'
+'&X-Amz-Date='+LTimeStamp
+'&X-Amz-Expires='+AExpiresIn.ToString
+'&X-Amz-SignedHeaders=host';
//1 - CanonicalRequest
LCanonicalRequest := 'GET' +#10+
'/'+ABucket+'/'+AObjectName +#10
+LQueryParams +#10
+'host:'+LHost+#10
+#10
+'host'+#10
+'UNSIGNED-PAYLOAD';
//2 - StringToSign
LStringToSign := 'AWS4-HMAC-SHA256' +#10
+ LTimeStamp +#10
+ UTF8String(LData)+'/'+ UTF8String(ARegion) +'/'+ UTF8String(LService)+UTF8String('/aws4_request') +#10
+ lowercase(GetHashSHA256Hex(LCanonicalRequest));
//3 - Signature
LSignature := BuildSignature(LStringToSign,LData,ARegion,LService,LSecretAccessKey);
//4 - Signed URL
Result := LUrl+'?'+LQueryParams+'&X-Amz-Signature='+LSignature;
end;
end.

Delphi memory stream to server

I am working on creating a chat program. But for some reason my stream won't come through. Could someone check my code and tell me what I am doing wrong?
Client side:
procedure TForm1.Button1Click(Sender: TObject);
var
myStream : TMemoryStream;
chat : String;
begin
//Creating a stream
chat := 'bladibla';
myStream := TMemoryStream.Create();
myStream.Size := Length(Chat);
myStream.WriteBuffer(Pointer(Chat)^, Length(Chat));
//Resetting the stream position
myStream.Position := 0;
//Sending the stream
TcpClient1.Active := true;
TcpClient1.SendStream(myStream);
TcpClient1.Active := false;
//Free the stream
myStream.Free;
end;
Server Side:
procedure TForm1.TcpServer1Accept(Sender: TObject;
ClientSocket: TCustomIpClient);
var
chat : string;
begin
//Receives the message from the client
ClientSocket.ReceiveBuf(Pointer(Chat)^,Length(Chat),0);
memo1.Lines.Add(chat);
memo1.Lines.Add('------');
end;
If you're using D2009 or later, then when sending, you're cutting the data in half. Also, given you will ultimately be reading the data with ReceiveBuf, it would probably be sensible to prepend a length marker. Less substantively, you also don't need to set the memory stream's Size up front, and should wrap the stream usage in a try/finally block:
procedure TForm1.Button1Click(Sender: TObject);
var
myStream : TMemoryStream;
chat : String;
Len: Int32;
begin
//Creating a stream
chat := 'bladibla';
myStream := TMemoryStream.Create();
try
Len := Length(Chat);
myStream.WriteBuffer(Len, SizeOf(Len));
myStream.WriteBuffer(Pointer(Chat)^, Len * SizeOf(Char));
//Resetting the stream position
myStream.Position := 0;
//Sending the stream
TcpClient1.Active := true;
TcpClient1.SendStream(myStream);
TcpClient1.Active := false;
finally
//Free the stream
myStream.Free;
end;
end;
In the case of the receiving code, you haven't pre-allocated the Chat buffer. As such, Length(Chat) will be 0. Given my suggested code above, we can read off the length marker first:
procedure TForm1.TcpServer1Accept(Sender: TObject;
ClientSocket: TCustomIpClient);
var
chat : string;
Len: Int32;
begin
//Receives the message from the client
ClientSocket.ReceiveBuf(Len, SizeOf(Len),0);
SetLength(Chat, Len);
ClientSocket.ReceiveBuf(Pointer(Chat)^,Len * SizeOf(Char),0);
memo1.Lines.Add(chat);
memo1.Lines.Add('------');
end;
Lastly... the components used in this code are deprecated, so it might be an idea to investigate Indy instead.

DWScript: Issue updating to current development version

This weekend, I updated my code base from DWScript SVN. I used Preview 2.7 and now I'm using up-to-date trunk version.
I recompile my application and now the OnAfterInitUnitTable is no more triggered. Actually TdwsUnit.InitUnitTable is not called at all.
BTW: TDWSunit is created at runtime by code and then two classes are exposed using ExposeRTTI. In need to expose one instance of each class.
What are - now - the prerequisites to have OnAfterInitUnitTable triggered?
Any help appreciated.
EDIT: Sample code to reproduce:
program ExposeTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
SysUtils, Classes, TypInfo,
dwsRTTIExposer, dwsExprs, dwsComp;
type
TScriptApplication = class(TPersistent)
end;
TTestClass = class(TThread)
private
FScript : IdwsProgram;
FDelphiWebScript : TDelphiWebScript;
FUnit : TdwsUnit;
FScriptApplication : TScriptApplication;
FSuccess : Boolean;
procedure ExposeInstancesAfterInitTable(Sender: TObject);
public
constructor Create;
destructor Destroy; override;
procedure Execute; override;
end;
var
Test : TTestClass;
{ TTestClass }
constructor TTestClass.Create;
begin
inherited Create(TRUE);
FScriptApplication := TScriptApplication.Create;
FDelphiWebScript := TDelphiWebScript.Create(nil);
FUnit := TdwsUnit.Create(nil);
FUnit.UnitName := 'Test';
FUnit.Script := FDelphiWebScript;
FUnit.ExposeRTTI(TypeInfo(TScriptApplication), [eoNoFreeOnCleanup]);
FUnit.OnAfterInitUnitTable := ExposeInstancesAfterInitTable;
end;
destructor TTestClass.Destroy;
begin
FreeAndNil(FScriptApplication);
FreeAndNil(FUnit);
FreeAndNil(FDelphiWebScript);
inherited;
end;
procedure TTestClass.Execute;
begin
WriteLn('Test 1');
FSuccess := FALSE;
FScript := FDelphiWebScript.Compile('Unit Test; var I: Integer; I := 0;');
if FSuccess then
WriteLn(' Success')
else
WriteLn(' Failure');
WriteLn('Test 2');
FSuccess := FALSE;
FScript := FDelphiWebScript.Compile('var I: Integer; I := 0;');
if FSuccess then
WriteLn(' Success')
else
WriteLn(' Failure');
WriteLn('Test Done');
end;
procedure TTestClass.ExposeInstancesAfterInitTable(Sender: TObject);
begin
FUnit.ExposeInstanceToUnit('Application', 'TScriptApplication', FScriptApplication);
WriteLn('OnAfterInitUnitTable called');
FSuccess := TRUE;
end;
begin
Test := TTestClass.Create;
Test.Start;
Sleep(1000);
WriteLn('Hit enter to quit');
ReadLn;
Test.Free;
end.
EDIt2: Other version to show the new issue using suggestion by Eric Grange in answer 1 below;
program ExposeTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
SysUtils, Classes, TypInfo,
dwsRTTIExposer, dwsFunctions, dwsExprs, dwsComp;
type
TScriptApplication = class(TPersistent)
published
procedure Demo;
end;
TTestClass = class(TThread)
private
FScript : IdwsProgram;
FDelphiWebScript : TDelphiWebScript;
FUnit : TdwsUnit;
FScriptApplication : TScriptApplication;
FSuccess : Boolean;
procedure ExposeInstancesAfterInitTable(Sender: TObject);
function NeedUnitHandler(const UnitName : UnicodeString;
var UnitSource : UnicodeString): IdwsUnit;
public
constructor Create;
destructor Destroy; override;
procedure Execute; override;
end;
var
Test : TTestClass;
{ TTestClass }
constructor TTestClass.Create;
begin
inherited Create(TRUE);
FScriptApplication := TScriptApplication.Create;
FDelphiWebScript := TDelphiWebScript.Create(nil);
FDelphiWebScript.OnNeedUnit := NeedUnitHandler;
FUnit := TdwsUnit.Create(nil);
FUnit.UnitName := 'Test';
FUnit.Script := FDelphiWebScript;
FUnit.ExposeRTTI(TypeInfo(TScriptApplication), [eoNoFreeOnCleanup]);
FUnit.OnAfterInitUnitTable := ExposeInstancesAfterInitTable;
end;
destructor TTestClass.Destroy;
begin
FreeAndNil(FScriptApplication);
FreeAndNil(FUnit);
FreeAndNil(FDelphiWebScript);
inherited;
end;
procedure TTestClass.Execute;
begin
WriteLn('Test 1');
FSuccess := FALSE;
FScript := FDelphiWebScript.Compile('Unit Test; var I: Integer; I := 0;');
WriteLn(FScript.Msgs.AsInfo);
if FSuccess then
WriteLn(' Success')
else
WriteLn(' Failure');
WriteLn('Test 2');
FSuccess := FALSE;
FScript := FDelphiWebScript.Compile('uses Other;');
WriteLn(FScript.Msgs.AsInfo);
if FSuccess then
WriteLn(' Success')
else
WriteLn(' Failure');
WriteLn('Test Done');
end;
procedure TTestClass.ExposeInstancesAfterInitTable(Sender: TObject);
begin
FUnit.ExposeInstanceToUnit('Application', 'TScriptApplication', FScriptApplication);
WriteLn('OnAfterInitUnitTable called');
FSuccess := TRUE;
end;
function TTestClass.NeedUnitHandler(
const UnitName : UnicodeString;
var UnitSource : UnicodeString): IdwsUnit;
begin
Result := nil;
if SameText(UnitName, 'Other') then
UnitSource := 'unit Other;' + #13#10 +
'procedure Func;' + #13#10 +
'begin' + #13#10 +
' Application.Demo;' + #13#10 +
'end;' + #13#10
else
UnitSource := '';
end;
{ TScriptApplication }
procedure TScriptApplication.Demo;
begin
end;
begin
Test := TTestClass.Create;
Test.Start;
Sleep(1000);
WriteLn('Hit enter to quit');
ReadLn;
Test.Free;
end.
When encountering a "unit" as main program, the compiler currently assumes it's just a compilation for IDE purposes, ie. to check for syntax errors, build a symbol map, provide suggestions, etc. and the resulting program isn't fully initialized as a consequence.
So if you want to compile the unit and make an executable program, you can have a main program that'll just be something like:
uses Test;
This will compile a program comprised of your unit, for which executions can be created and where functions can be called though exec.Info, classes can be instantiated, etc.
Edit2: For the second test case, it works if "uses Test;" is added. For full cross-compilability with Delphi, you'll also need interface/implementation sections (when targeting script only, they are not necessary)
unit Other;
interface
uses Test;
procedure Func;
implementation
procedure Func;
begin
Application.Demo;
end;
and if RTTI is generated for the methods with the $RTTI directive, at least with
{$RTTI EXPLICIT METHODS([vcPublished])}
TScriptApplication = class(TPersistent)
published
procedure Demo;
end;
otherwise you get an error about "Demo" not being found.

Delphi XE2 Indy10 udp client-server interchange using SendBuffer-ReceiveBuffer

I use Delphi XE2 and Indy10 UDP protocol.
I can't receive server echo on client side if I use ReceiveBuffer method.
I got "Socket Error # 10040" although I send back very small echo
message from server to client.
Console application to illustrate my problem is below.
Thanks in advance.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, IdGlobal, IdBaseComponent, IdComponent, IdSocketHandle,
IdUDPClient, IdUDPServer, IdUDPBase, IdStack;
type
TUDP_Serv = class(TIdUDPServer)
procedure udpSvUDPRead(AThread: TIdUDPListenerThread;
AData: TIdBytes; ABinding: TIdSocketHandle);
end;
var
udpServer: TUDP_Serv;
udpCl: TIdUDPClient;
bSnd, bRcv: TBytes;
s: string;
k: integer;
//==============================================================================
procedure TUDP_Serv.udpSvUDPRead(AThread: TIdUDPListenerThread; AData: TIdBytes;
ABinding: TIdSocketHandle);
begin
writeln(' Server read: ' + ToHex(AData, length(AData)));
with ABinding do SendTo(PeerIP, PeerPort, AData);
end;
//==============================================================================
begin
try
udpServer := TUDP_Serv.Create;
with udpServer do begin
OnUDPRead := udpSvUDPRead; DefaultPort := 20001; BufferSize := 2048;
ThreadedEvent := true; Active := True;
if Active then writeln('Server started on port: ' + IntToStr(DefaultPort));
end;
udpCl := TIdUDPClient.Create;
with udpCl do begin
BufferSize := 2048; Host := '127.0.0.1'; Port := 20001;
end;
SetLength(bSnd, 5); bSnd[0] := $31; bSnd[1] := $0;
bSnd[2] := $33; bSnd[3] := $0; bSnd[4] := $0;
repeat
writeln(' Client send: ' + ToHex(bSnd, length(bSnd)));
with udpCl do SendBuffer(Host, Port, bSnd); sleep(100);
try
k := udpCl.ReceiveBuffer(bRcv, 10);
if k > 0 then writeln(' Client read: ' + ToHex(bRcv, length(bRcv)));
except
on E: exception do begin
writeln(Format(' Client read err: %s',[E.Message]));
end;
end;
readln(s);
until s <> '';
except
on E: Exception do begin
Writeln(E.ClassName, ': ', E.Message); readln(s);
end;
end;
end.
Screen output:
Server started on prot: 20001
Client send: 3100330000
Server read: 3100330000
Client read err: Socket Error # 10040
Message too long.
10040 is WSAEMSGSIZE, which means the buffer you tried to receive into was smaller than the actual size of the message that was received.
You are not allocating any memory for bRcv before calling ReceiveBuffer(), so you are trying to receive into a 0-byte buffer, hense the error. You need to pre-allocate bRcv to at least the same size as your messages, if not larger.
ReceiveBuffer() does not allocate a new TBytes for each received message. You have to allocate the buffer yourself beforehand and then ReceiveBuffer() will merely fill it in, returning how many bytes were actually received into it.

How do set the timeout on a wcf service caller in Delphi?

I have a straightforward call to a wcf service hosted by iis I'm Delphi 2010
The operation being called on the service could take several minutes
What is the best way of avoiding a timeout error in Delphi?
I deliberately put a Thread.Sleep inside my WCF Service force it to wait for 31 seconds
After 30 seconds I got the error
Project raised exception class ESOAPHTTPException with message 'The handle is in the wrong state for the requested operation - URL:http://10.1.1.4/STC.WcfServices.Host/FlexProcurementService.svc - SOAPAction:http://navsl.stcenergy.com/FlexProcurement/FlexProcurementService/GetPassthroughSummaryGridReportData'.
This turned out to be a bug in Delphi 2010 which I have applied the patch for, so now I get the error operation timed out
function GetFlexProcurementService(const objServiceInfo: TWCFService; UseWSDL: Boolean; Addr: string; HTTPRIO: THTTPRIO): FlexProcurementService;
var
RIO: THTTPRIO;
begin
Result := nil;
if (Addr = '') then
begin
if UseWSDL then
Addr := objServiceInfo.WSDL
else
Addr := objServiceInfo.URL;
end;
if HTTPRIO = nil then
RIO := THTTPRIO.Create(nil)
else
RIO := HTTPRIO;
try
Result := (RIO as FlexProcurementService);
if UseWSDL then
begin
RIO.WSDLLocation := Addr;
RIO.Service := objServiceInfo.Svc;
RIO.Port := objServiceInfo.Prt;
end else
RIO.URL := Addr;
finally
if (Result = nil) and (HTTPRIO = nil) then
RIO.Free;
end;
end;
Paul
uses wininet;
...
function SetTimeout(const HTTPReqResp: THTTPReqResp; Data: Pointer; NumSecs : integer) : boolean;
var
TimeOut: Integer;
begin
// Sets the receive timeout. i.e. how long to wait to 'receive' the response
TimeOut := (NumSecs * 1000);
try
InternetSetOption(Data, INTERNET_OPTION_RECEIVE_TIMEOUT, Pointer(#TimeOut), SizeOf(TimeOut));
InternetSetOption(Data, INTERNET_OPTION_SEND_TIMEOUT, Pointer(#TimeOut), SizeOf(TimeOut));
except on E:Exception do
raise Exception.Create(Format('Unhandled Exception:[%s] while setting timeout to [%d] - ',[E.ClassName, TimeOut, e.Message]));
end;
end;
In the RIO OnBeforePost:
procedure TEETOUpsertWrapper.OnBeforePost(const HTTPReqResp: THTTPReqResp; Data: Pointer); begin
SetTimeout(HTTPReqResp, Data, 5 * 60);
end;