I have a project which does financial reports and I want to let user to be able to get this reports through the internet
I tried using TIdHTTPServer which is an Indy component to make my application to work as an HTTP Server and to let it to be able
receive request -> process the request -> send back the result of the request process
using a special port.
now my problem is that I'm getting a lot of Access Violation errors and random exceptions
it looks like about threads problem or I don't know because if I process the same request without using the TIdHTTPServer I don't get any problem
i'm using the OnCommandGet Event to process the request and send the result back to user inside the context stream.
what I need is a demonstration on how to use it with TADODataSet and TADOConnection
for example I need the user to be able to send a request and the TIdHTTPServer takes the request (for example call a stored procedure using to ADODataSet and take the result as XML file and send it back to the user)
please help....thank you.
one possibility how a Server could work ...
unit Unit3;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,IDContext, IdBaseComponent, IdComponent, IdCustomTCPServer, IdTCPServer, StdCtrls, DB, ADODB;
type
TForm3 = class(TForm)
IdTCPServer1: TIdTCPServer;
Memo1: TMemo;
Button1: TButton;
DummyConnection: TADOConnection;
procedure Button1Click(Sender: TObject);
procedure IdTCPServer1Execute(AContext: TIdContext);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
var
Form3: TForm3;
implementation
uses ComObj,AdoInt,ActiveX;
{$R *.dfm}
function SendStream(AContext: TIdContext; AStream: TStream): Boolean;
begin
Result := False;
try
AContext.Connection.IOHandler.Write(AStream.Size); // sending length of Stream first
AContext.Connection.IOHandler.WriteBufferOpen;
AContext.Connection.IOHandler.Write(AStream, AStream.Size);
AContext.Connection.IOHandler.WriteBufferFlush;
finally
AContext.Connection.IOHandler.WriteBufferClose;
end;
Result := True;
end;
procedure TForm3.Button1Click(Sender: TObject);
begin
IdTCPServer1.Active := true;
end;
{ Clientside function
Function RecordsetFromXMLStream(Stream:TStream): _Recordset;
var
RS: Variant;
begin
RS := CreateOleObject('ADODB.Recordset');
RS.Open(TStreamAdapter.Create(Stream) as IUnknown);
Result := IUnknown(RS) as _Recordset;
end;
}
Procedure RecordsetToXMLStream(const Recordset: _Recordset;Stream:TStream);
var
RS: Variant;
begin
if Recordset = nil then Exit;
RS := CreateOleObject('ADODB.Recordset');
RS := Recordset;
RS.Save(TStreamAdapter.Create(stream) as IUnknown, adPersistXML);
Stream.Position := 0;
end;
Procedure GetQueryStream(Const s,ConStr:String;ms:TMemoryStream);
var
AC:TAdoConnection;
ads:TAdodataset;
begin
AC:=TAdoConnection.Create(nil);
try
ads:=TAdodataset.Create(nil);
try
ads.Connection := AC;
AC.ConnectionString := ConStr;
ads.CommandText := s;
ads.Open;
RecordsetToXMLStream(ads.Recordset,ms);
finally
ads.Free
end;
finally
AC.Free
end;
end;
procedure TForm3.IdTCPServer1Execute(AContext: TIdContext);
var
cmd:String;
ms:TMemoryStream;
begin
CoInitialize(nil);
AContext.Connection.IOHandler.Readln(cmd);
ms:=TMemoryStream.Create;
try
GetQueryStream('Select * from Adressen',DummyConnection.ConnectionString,ms);
ms.Position := 0;
SendStream(AContext,ms);
AContext.Connection.Socket.CloseGracefully;
finally
ms.Free;
CoUninitialize;
end;
end;
end.
Related
I'm trying to create application that support session using idhttp 10.5.7 on Delphi XE, but the session always changed every time i call the request.
But then I use the same code in Delphi 2006 using indy 10.1.5 the session maintained successfully, even when i call the request multiple times it only create 1 session.
How do I solve this problem in Delphi XE?
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, IdCookieManager, IdBaseComponent, IdComponent, IdTCPConnection,
IdTCPClient, IdHTTP, StdCtrls,iduri,IdCookie;
type
TForm1 = class(TForm)
HTTP: TIdHTTP;
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
function get_dataweb(url: string): string;
procedure OnNewCookie(ASender: TObject; ACookie: TIdCookieRFC2109; var VAccept: Boolean);
{ Private declarations }
public
Stream : TStringStream;
cookie : TIdCookieManager;
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var hasil : string;
begin
hasil := get_dataweb('http://localhost/web/tes.php');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Stream := TStringStream.Create;
cookie := TIdCookieManager.Create;
cookie.OnNewCookie := OnNewCookie;
HTTP.CookieManager := cookie;
end;
procedure TForm1.OnNewCookie(ASender: TObject; ACookie: TIdCookieRFC2109; var VAccept: Boolean);
begin
memo1.Lines.Add(ACookie.CookieText);
end;
function TForm1.get_dataweb(url:string):string;
var hasil : string;
begin
stream.Position := 0;
stream.Size := 0;
http.AllowCookies := True;
http.HandleRedirects := True;
HTTP.ReadTimeout := 0;
HTTP.ConnectTimeout := 0;
HTTP.Request.CustomHeaders.clear;
HTTP.Request.CustomHeaders.Add('user:user');
HTTP.Request.CustomHeaders.Add('password:password');
HTTP.request.useragent := 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0';
try
http.get(url,Stream);
hasil := Stream.DataString;
except
on E: EIdHTTPProtocolException do
begin
hasil := E.ErrorMessage;
result := hasil;
exit;
end;
on E: Exception do
begin
ShowMessage('Failed, ' + E.Message + #13#10 + ' ' + hasil);
end;
end;
result := hasil;
end;
end.
3 times i clicked the button, 3 times new session created
Thankyou for the suggestion Remy, I have upgraded my Indy to 10.6.2 and the problem solved. I followed this tutorial successfully
I want to send email in other unit with different thread with indy10.0.52
I have source code
unit ThreadEmail;
interface
uses Classes, SysUtils, IdGlobal, IdMessage, IdIOHandler, IdIOHandlerSocket,
IdSSLOpenSSL, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient,
IdMessageClient, IdSMTP, IdExplicitTLSClientServerBase, IdSMTPBase,
IdIOHandlerStack, IdSSL, ExtCtrls;
type
TThreadEmail = class(TThread)
private
run : boolean;
counter : Integer;
target : Integer;
IdSMTP: TIdSMTP;
IdSSLIOHandlerSocketOpenSSL: TIdSSLIOHandlerSocketOpenSSL;
Messages : Array [0..10] of TIdMessage;
procedure checkRun();
protected
procedure Execute; override;
public
constructor Create(timerInS:Integer;host:string;port:integer;username,password:String;readTimeout : integer = 0);reintroduce;
function expressSend(recipients,subject,body:string;from:String='';replayTo:String='') :boolean;
function makeEmail(recipients,subject,body:string;from:String='';replayTo:String=''): boolean;
procedure SendAllEmail();
end;
implementation
constructor TThreadEmail.Create(timerInS:Integer;host:string;port:integer;username,password:String;readTimeout : integer = 0);
var b: byte;
begin
inherited Create(False);
Priority:= tpNormal;
FreeOnTerminate:= True;
IdSMTP := TIdSMTP.Create;
IdSSLIOHandlerSocketOpenSSL := TIdSSLIOHandlerSocketOpenSSL.Create();
for b:=low(Messages) to high(messages) do Messages[b] := nil;
IdSMTP.IOHandler := IdSSLIOHandlerSocketOpenSSL;
IdSMTP.UseTLS := utUseImplicitTLS;
IdSMTP.Host := host;
IdSMTP.Port := port;
IdSMTP.Username := username;
IdSMTP.Password := password;
IdSSLIOHandlerSocketOpenSSL.DefaultPort := 0;
IdSSLIOHandlerSocketOpenSSL.Destination := host+':'+inttostr(port);
IdSSLIOHandlerSocketOpenSSL.Host := host;
IdSSLIOHandlerSocketOpenSSL.MaxLineAction := maException;
IdSSLIOHandlerSocketOpenSSL.Port := port;
IdSSLIOHandlerSocketOpenSSL.ReadTimeout := readTimeout;
IdSSLIOHandlerSocketOpenSSL.SSLOptions.Method := sslvSSLv3;
IdSSLIOHandlerSocketOpenSSL.SSLOptions.Mode := sslmClient;
run:=true;
//target := timerInS*10;
end;
function TThreadEmail.expressSend(recipients,subject,body:string;from:String='';replayTo:String='') : boolean;
var IdMessage: TIdMessage;
begin
Result := false;
IdMessage := TIdMessage.Create();
IdMessage.Recipients.EMailAddresses := recipients;
IdMessage.Subject := subject;
IdMessage.Body.Text := body;
if from <> '' then IdMessage.From.Address := from;
if replayTo <> '' then IdMessage.ReplyTo.EMailAddresses := from;
try
IdSMTP.Connect();
IdSMTP.Send(IdMessage);
Result := true;
finally
IdSMTP.Disconnect();
end;
end;
function TThreadEmail.makeEmail(recipients,subject,body:string;from:String='';replayTo:String='') : boolean;
var b: byte;
begin
Result := false;
for b:=low(Messages) to high(messages) do
if Messages[b] = nil then
begin
Result := true;
Messages[b]:= TIdMessage.Create();
Messages[b].Recipients.EMailAddresses := recipients;
Messages[b].Subject := subject;
Messages[b].Body.Text := body;
if from <> '' then Messages[b].From.Address := from;
if replayTo <> '' then Messages[b].ReplyTo.EMailAddresses := from;
end;
if not(result) then
begin
SendAllEmail();
makeEmail(recipients,subject,body,from,replayTo);
end;
end;
procedure TThreadEmail.SendAllEmail();
var b: byte;
begin
try
IdSMTP.Connect();
for b:=low(Messages) to high(messages) do
if run and (Messages[b] <> nil) then
begin
try
IdSMTP.Send(Messages[b]);
finally
Messages[b].Free;
Messages[b] := nil;
end
end;
finally
IdSMTP.Disconnect();
end;
end;
procedure TThreadEmail.checkRun();
begin
Dec(counter);
if counter <= 0 then SendAllEmail();
end;
procedure TThreadEmail.Execute;
var b: byte;
begin
while run do
begin
sleep(100);
checkRun();
end;
IdSMTP.Free;
IdSSLIOHandlerSocketOpenSSL.Free;
for b:=low(Messages) to high(messages) do
if Messages[b] <> nil then Messages[b].Free;
end;
end.
and in mainfrom that i create
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ThreadEmail;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var ThreadEmail : TThreadEmail;
begin
ThreadEmail := ThreadEmail.Create(10,'smtp.gmail.com',465,'xxx.gmail.com','xxx',2000);
ThreadEmail.expressSend('xxx#yahoo.com','TES','TES');
end;
When button1 clicked, it always "create access violation error", Why it happend? can anyone help me? as a info, i sucesed send a email before, but i want to make a singgle unit that can run alone.
thanks
ThreadEmail := ThreadEmail.Create(10,'s....
this should be :
ThreadEmail := TThreadEmail.Create(10,'s....
Not sure if that's just a typo? It will definitely cause an AV if not.
In any case, ThreadEmail.expressSend will not run in your TThread's thread the way you are calling it. When you run a TThread the code in its Execute method will run in a separate thread. Any of the public member methods, however, can be called on an instance just like public methods of any class and they are executed on the thread that calls them.
To get this to work you need to have the Execute method performing the calls to send the email message. The UI thread needs to trigger action in the Execute method and not perform the action itself; this can be done by any number of means (having Execute synchronize with WaitForSingleObject, via message passing, etc).
The rest of your code looks rather broken. Your Execute code is not really going to work as it is - this loop :
while run do
begin
sleep(100);
checkRun();
end;
will never terminate as it seems you don't set run to false anywhere. Furthermore, counter does not seem to get set anywhere (nor do I really understand its purpose) so this will just SendAllEmail() every 100ms or so.
The makeEmail function will never terminate (stack overflow) since it calls itself recursively with the original arguments and the logic guarantees re-entry on each pass. It also looks like it will send whatever message eleven times on each recursion (since all 11 elements of Messages will be nil after initialization and after each call to SendAllEmail().
Even if you fix this - if you are calling makeEmail externally (ie: from the UI or another thread) then this will likely end up with all sorts of cross-thread errors since both Execute and the calling thread will be trying to call SendAllEmail at the same time. This code will need some work.
How can I get the url from a running instance of Chrome using Delphi?
I'm trying to do a Delphi application that gets the url of the active tab of the browser (IE, Mozilla, etc.). I'm using this code that works for IE:
procedure TForm1.GetCurrentURL (var URL, Title : string);
var
DDEClient : TDDEClientConv;
s : string;
begin
s := '';
try
DDEClient := TDDEClientConv.Create(self);
with DDEClient do
begin
if SetLink('IExplore','WWW_GetWindowInfo') then
s := RequestData('0xFFFFFFFF,sURL,sTitle')
else
if SetLink('Netscape','WWW_GetWindowInfo') then
s := RequestData('0xFFFFFFFF,sURL,sTitle')
else
if SetLink('Mosaic','WWW_GetWindowInfo') then
s := RequestData('0xFFFFFFFF,sURL,sTitle')
else
if SetLink('Netscp6','WWW_GetWindowInfo') then
s := RequestData('0xFFFFFFFF,sURL,sTitle')
else
if SetLink('Mozilla','WWW_GetWindowInfo') then
s := RequestData('0xFFFFFFFF,sURL,sTitle')
else
if SetLink('Firefox','WWW_GetWindowInfo') then
s := RequestData('0xFFFFFFFF,sURL,sTitle');
end;
if s <> '' then
begin
delete(s,1,1);
URL := copy(s,1,pos('","',s)-1);
delete(s,1,pos('","',s)+2);
Title := copy(s,1,pos('"',s) - 1);
end;
exit;
except
MessageDlg('URL attempt failed!',mtError,[mbOK],0);
end;
end;
But this code doesn't work with Chrome.
Thanks.
Here is how I have done it before for retrieving the URL from the active tab. You could probably extend this to include all of Chrome's tabs.
One other note, as you can see this grabs the first handle to chrome.exe that it finds. To have this accommodate multiple instances of Chrome running, you would need to adjust this to get a handle to each Chrome instance.
I put this together pretty quick, so don't consider this "production" quality. Just create a new vcl application and drop a TMemo and a TButton on the form and assign the Button1Click to TButton's OnClick event.
unit main;
interface
uses
Windows,
Messages,
SysUtils,
Classes,
Controls,
Forms,
StdCtrls;
type
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
function GetActivePageUrlFromChrome(Handle: HWnd; Param: LParam): Bool; stdcall;
var
Form1 : TForm1;
implementation
{$R *.dfm}
function GetActivePageUrlFromChrome(Handle: HWnd; Param: LParam): Bool; stdcall;
var
List: TStrings;
hWndChrome, hWndChromeChild: HWND;
Buffer : array[0..255] of Char;
begin
List := TStrings(Param);
//get the window caption
SendMessage(Handle, WM_GETTEXT, Length(Buffer), integer(#Buffer[0]));
//look for the chrome window with "Buffer" caption
hWndChrome := FindWindow('Chrome_WidgetWin_0', Buffer);
if hWndChrome <> 0 then
begin
hWndChromeChild := FindWindowEx(hWndChrome, 0, 'Chrome_AutocompleteEditView', nil);
if hWndChromeChild <> 0 then
begin
SendMessage(hWndChromeChild, WM_GETTEXT, Length(Buffer), integer(#Buffer));
List.Add(Buffer);
end;
end;
Result := True;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
slChromeUrl : TStringList;
begin
slChromeUrl := TStringList.Create;
try
EnumWindows(GetActivePageUrlFromChrome, LParam(slChromeUrl));
Memo1.Lines.AddStrings(slChromeUrl);
finally
FreeAndNil(slChromeUrl);
end;
end;
end.
Error:
try
EnumWindows(GetActivePageUrlFromChrome, LParam(slChromeUrl));
Memo1.Lines.AddStrings(slChromeUrl);
Works:
try
EnumWindows(#GetActivePageUrlFromChrome, LParam(slChromeUrl));
Memo1.Lines.AddStrings(slChromeUrl);
Apparently there's an issue open to request DDE support by chrome/chromium, keep a look-out there if a future version would provide it:
http://code.google.com/p/chromium/issues/detail?id=70184
Using: Delphi 2010 and the JEDI Windows API and JWSCL
I am trying to assign the Logon As A Service privilege to a user using LsaAddAccountRights function but it does not work ie. after the function returns, checking in Group Policy Editor shows that the user still does not have the above mentioned privilege.
I'm running the application on Windows XP.
Would be glad if someone could point out what is wrong in my code:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, JwaWindows, JwsclSid;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function AddPrivilegeToAccount(AAccountName, APrivilege: String): DWORD;
var
lStatus: TNTStatus;
lObjectAttributes: TLsaObjectAttributes;
lPolicyHandle: TLsaHandle;
lPrivilege: TLsaUnicodeString;
lSid: PSID;
lSidLen: DWORD;
lTmpDomain: String;
lTmpDomainLen: DWORD;
lTmpSidNameUse: TSidNameUse;
lPrivilegeWStr: String;
begin
ZeroMemory(#lObjectAttributes, SizeOf(lObjectAttributes));
lStatus := LsaOpenPolicy(nil, lObjectAttributes, POLICY_LOOKUP_NAMES, lPolicyHandle);
if lStatus <> STATUS_SUCCESS then begin
Result := LsaNtStatusToWinError(lStatus);
Exit;
end;
try
lTmpDomainLen := DNLEN; // In 'clear code' this should be get by LookupAccountName
SetLength(lTmpDomain, lTmpDomainLen);
lSidLen := SECURITY_MAX_SID_SIZE;
GetMem(lSid, lSidLen);
try
if LookupAccountName(nil, PChar(AAccountName), lSid, lSidLen, PChar(lTmpDomain),
lTmpDomainLen, lTmpSidNameUse) then begin
lPrivilegeWStr := APrivilege;
lPrivilege.Buffer := PChar(lPrivilegeWStr);
lPrivilege.Length := Length(lPrivilegeWStr) * SizeOf(Char);
lPrivilege.MaximumLength := lPrivilege.Length;
lStatus := LsaAddAccountRights(lPolicyHandle, lSid, #lPrivilege, 1);
Result := LsaNtStatusToWinError(lStatus);
end
else
Result := GetLastError;
finally
FreeMem(lSid);
end;
finally
LsaClose(lPolicyHandle);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
AddPrivilegeToAccount('Sam', 'SeServiceLogonRight');
end;
end.
Thanks in advance.
To be able to use LsaAddAccountRights you should open policy handle with additional POLICY_CREATE_ACCOUNT flag (POLICY_CREATE_ACCOUNT | POLICY_LOOKUP_NAMES) in LsaOpenPolicy or use MAXIMUM_ALLOWED instead of both flags.
im new to delphi. and also almost new to programming world.
i was made some simple post software which using idhttp module.
but when execute it , it not correctly working.
this simple program is check for my account status.
if account login successfully it return some source code which include 'top.location ='
in source, and if login failed it return not included 'top.location ='
inside account.txt is follow first and third account was alived account
but only first account can check, after first account other account can't check
i have no idea what wrong with it
ph896011 pk1089
fsadfasdf dddddss
ph896011 pk1089
following is source of delphi
if any one help me much apprecated!
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, IdBaseComponent, IdComponent, IdTCPConnection,
IdTCPClient, IdHTTP, IdCookieManager, ExtCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
IdHTTP1: TIdHTTP;
Memo1: TMemo;
IdCookieManager1: TIdCookieManager;
lstAcct: TListBox;
result: TLabel;
Edit1: TEdit;
Timer1: TTimer;
procedure Button1Click(Sender: TObject);
//procedure FormCreate(Sender: TObject);
//procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
AccList: TStringList;
IdCookie: TIdCookieManager;
CookieList: TList;
StartCnt: Integer;
InputCnt: Integer;
WordList: TStringList;
WordNoList: TStringList;
WordCntList: TStringList;
StartTime: TDateTime;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
//temp: String;
lsttemp: TStringList;
sl : tstringlist;
//userId,userPass: string;
begin
InputCnt:= 0;
WordList := TStringList.Create;
CookieList := TList.create;
IdCookie := TIdCookieManager.Create(self);
if FileExists(ExtractFilePath(Application.ExeName) + 'account.txt') then
WordList.LoadFromFile(ExtractFilePath(Application.ExeName) + 'account.txt');
WordNoList:= TStringList.Create;
WordCntList := TStringList.Create;
lsttemp := TStringList.create;
sl :=Tstringlist.Create;
try
try
for i := 0 to WordList.Count -1 do
begin
ExtractStrings([' '], [' '], pchar(WordList[i]), lsttemp);
WordNoList.add(lsttemp[0]);
//ShowMessage(lsttemp[0]);
WordCntList.add(lsttemp[1]);
//ShowMessage(lsttemp[1]);
sl.Add('ID='+ lsttemp[0]);
sl.add('PWD=' + lsttemp[1]);
sl.add('SECCHK=0');
IdHTTP1.HandleRedirects := True;
IdHTTP1.Request.ContentType := 'application/x-www-form-urlencoded';
memo1.Text:=idhttp1.Post('http://user.buddybuddy.co.kr/Login/Login.asp',sl);
if pos('top.location =',Memo1.Text)> 0 then
begin
application.ProcessMessages;
ShowMessage('Alive Acc!');
//result.Caption := 'alive acc' ;
sleep(1000);
Edit1.Text := 'alive acc';
lsttemp.Clear;
Memo1.Text := '';
//memo1.Text := IdHTTP1.Get('https://user.buddybuddy.co.kr/Login/Logout.asp');
Sleep(1000);
end;
if pos('top.location =', memo1.Text) <> 1 then
begin
application.ProcessMessages;
ShowMessage('bad');
Edit1.Text := 'bad';
//edit1.Text := 'bad';
lsttemp.Clear;
memo1.Text := '';
sleep(1000) ;
end;
Edit1.Text := '';
end;
finally
lsttemp.free;
end;
StartCnt := lstAcct.items.Count;
StartTime := Now;
finally
sl.Free;
end;
end;
end.
Right before:
sl.Add('ID='+ lsttemp[0]);
Do:
sl.Clear;
On the first run your "SL" holds the two POST parameters, but unless you clear it on the second run, you just keep adding parameters, confusing the HTTP server you're trying to connect to!
That might not be your only problem, but that's surely one of the problems.