Send Info from Installer - Custom page fields and logs - inno-setup

I'm trying to make an Inno Setup installer that is capable of sending some logs, and also a bug report text for which I made a custom page:
In the following code I'm trying to import a function that I found in (MSDN) SmtpMail.Send Method, but without success:
var
ExtraPage : TInputQueryWizardPage;
RichEditViewer: TRichEditViewer;
labelchar,max_char:TLabel;
Installer_bug,Content_bug: TNewRadioButton;
function SetFocus(hWnd: HWND): HWND;external 'SetFocus#user32.dll stdcall';
function GetSystemMetrics (nIndex: Integer): Integer;external 'GetSystemMetrics#User32.dll stdcall setuponly';
//function Send(message: MailMessage); external 'Send#System.Web.dll stdcall setuponly';
procedure isAt(Sender: TObject; var Key: Char);
begin
if Key ='#' then begin
Key := #0;
SetFocus(ExtraPage.Edits[4].Handle);
end;
end;
procedure NumbersOnly(Sender:TObject; var Key: Char);
var s:string;
begin
s := ('1234567890'#8);
if pos(key,s) =0 then
Key:=#0;
end;
procedure OffsetPageItem(Page: TInputQueryWizardPage; Index, Offset: Integer);
begin
//Labels
Page.PromptLabels[Index].SetBounds(10,Offset,55,30);
//Name field
Page.Edits[Index].SetBounds(100,Offset,200,40);
//Resolution field
if Index=1 then
Page.Edits[Index].SetBounds(100,Offset,40,40);
Page.Edits[Index].MaxLength:=4
//x field
if Index=2 then begin
Page.Edits[Index].SetBounds(160,Offset,40,40);
Page.PromptLabels[Index].SetBounds(145,Offset+3,10,20);
Page.Edits[Index].MaxLength:=4
end;
//E-Mail field
if Index=3 then begin
Page.Edits[Index].SetBounds(100,Offset,130,40);
if not (Pos('#',Page.Values[Index])=0) then
Page.Edits[Index+1].SelectAll;
end;
//# field
if Index=4 then begin
Page.Edits[Index].SetBounds(250,Offset,70,40);
Page.PromptLabels[Index].SetBounds(235,Offset+3,10,20);
end;
//Description field
if Index=5 then begin
ExtraPage.PromptLabels[index].SetBounds(10,Offset+15,80,60);
ExtraPage.Edits[Index].Hide;
RichEditViewer := TRichEditViewer.Create(ExtraPage);
RichEditViewer.ScrollBars:=ssVertical;
with RichEditViewer do begin
Parent:=ExtraPage.Surface;
SetBounds(100,Offset+25,300,100);
Text:='Having a bug? Write it here... ';
MaxLength:=400;
end;
end;
end;
procedure InitializeWizard;
var
index:Integer;
begin
ExtraPage := CreateInputQueryPage(wpWelcome, 'E-mail sender','Add The following information!','');
ExtraPage.SubCaptionLabel.Hide;
//index=0;
index:=ExtraPage.Add('Name: ', False);
ExtraPage.Values[index]:=ExpandConstant('{computername}');
OffsetPageItem(ExtraPage,index,10);
//index=1;
index:=ExtraPage.Add('Resolution: ', False);
OffsetPageItem(ExtraPage,index,40);
ExtraPage.Values[index]:=IntToStr(GetSystemMetrics(0));
ExtraPage.Edits[index].OnKeyPress:=#NumbersOnly;
//index=2;
index:=ExtraPage.Add(' x ', False);
OffsetPageItem(ExtraPage,index,40);
ExtraPage.Values[index]:=IntToStr(GetSystemMetrics(1));
ExtraPage.Edits[index].OnKeyPress:=#NumbersOnly;
//index=3;
index:=ExtraPage.Add('E-mail: ', False);
OffsetPageItem(ExtraPage,index,70);
ExtraPage.Edits[index].OnKeyPress:=#isAt;
//index=4;
index:=ExtraPage.Add('#', False);
OffsetPageItem(ExtraPage,index,70);
ExtraPage.Edits[index].OnKeyPress:=#isAt;
//index=5;
index:=ExtraPage.Add('Short Description:'+#10+'(How to reproduce?)', False);
OffsetPageItem(ExtraPage,index,100);
labelchar:=TLabel.Create(WizardForm);
with labelchar do begin
Parent:=ExtraPage.Surface;
SetBounds(10,200,100,30);
Caption:='Max number of'+#10#13+'characters: ';
end;
max_char:=TLabel.Create(WizardForm);
with max_char do begin
Parent:=ExtraPage.Surface;
Font.Style:=[fsBold];
SetBounds(68,213,100,30);
Caption:=IntToStr(400);
end;
Installer_bug:=TNewRadioButton.Create(WizardForm)
with Installer_bug do begin
Parent := ExtraPage.Surface;
SetBounds(100,100,80,20);
Caption:='Installer bug'
end;
Content_bug:=TNewRadioButton.Create(WizardForm)
with Content_bug do begin
Parent := ExtraPage.Surface;
SetBounds(190,100,80,20);
Caption:='Content bug'
end;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
line:String;
// E_mail :MailMessage;
begin
Result := True;
if CurPageID = ExtraPage.ID then begin
line:=ExtraPage.PromptLabels[0].Caption + ExtraPage.Values[0] + #10;
line:=line + ExtraPage.PromptLabels[1].Caption + ExtraPage.Values[1] + ExtraPage.PromptLabels[2].Caption+ExtraPage.Values[2] + #10;
line:=line + ExtraPage.PromptLabels[3].Caption + ExtraPage.Values[3] + ExtraPage.PromptLabels[4].Caption+ExtraPage.Values[4] + #10;
line:=line +'Type: ';
if Installer_bug.Checked then
line:=line + Installer_bug.Caption + #10;
if Content_bug.Checked then
line:=line + Content_bug.Caption + #10;
line:=line + ExtraPage.PromptLabels[5].Caption + #10#13;
line:=line + RichEditViewer.Text;
SaveStringToFile(ExpandConstant('{src}\test.txt'),line,false);
// E_mail:=MailMessage.Create(WizardForm);
// E_mail.From= "test#sdsd.com"
// E_mail.To = "test#gmail.com"
// E_mail.Subject="test";
// SmtpMail.SmtpServer= "MyMailServer";
// SmtpMail.Send(E_mail);
end;
end;

I ran into same issue this is what I did...
bmail:
bmail -s smtp.example.net -t you#example.com -f me#example.net -h ^
-a "mail subject" -b "message text"
You could also write your own mailer to execute.
[Run]
begin
// need to email Version
// blat -to %eMail% -f %eMail% %subj% %server% %debug% %x%
ExtractTemporaryFile('blat.exe');
connExe:=ExpandConstant('{tmp}')+'\blat.exe';
connTxt := ' -to ' + E_mail.From.text + ' -f '+ E_mail.To.Text + E_mail.Subject.Text + 'SmtpMail.SmtpServer.text' + ExpandConstant('{tmp}\log.log');
Log('The Value is connTxt: ' + connTxt );
if Exec(connExe, connTxt, '' , SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
//MsgBox(IntToStr(ResultCode), mbError, mb_Ok);
connTxt := ' -to ' + E_mail.From.text + ' -f '+ E_mail.To.Text + E_mail.Subject.Text + 'SmtpMail.SmtpServer.text' + ExpandConstant('{tmp}\log.log');
ExpandConstant('{tmp}\log.log');
Log('The Value is connTxt: ' + connTxt );
if Exec(connExe, connTxt, '' , SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
MsgBox( 'The '+ E_mail.Subject.Text + ' Email has been sent' , mbInformation, mb_Ok);
result := True;
end;
end;
end;
warning the names where change to address the question and has not been fully test as is.
#TLama is right but why not use the application to send the debug messages?

Related

inno setup: tail a log file to a multiline textbox in the wpInstalling page [duplicate]

I created an Input page that executes a command line app using the created variables from those inputs. Naturally, the cmd window pop ups on my screen. I would like to know if there is any way to embed the cmd window (or the output) on my Inno Setup installer page.
I'm running Inno Setup 5.6.1 (because of Windows XP compatibility), but I'm OK if I have to switch to the last version.
[Code]
var
MAIL: TInputQueryWizardPage;
Final: TWizardPage;
BotonIniciar: Tbutton;
procedure BotonIniciarOnClick(Sender: TObject);
begin
WizardForm.NextButton.Onclick(nil);
Exec(ExpandConstant('{tmp}\imapsync.exe'),'MAIL.Values[0]','', SW_SHOW,
ewWaitUntilTerminated, ResultCode);
end;
procedure InitializeWizard;
begin
MAIL := CreateInputQueryPage(wpWelcome, '', '', '');
MAIL.Add('Please input your information', False);
BotonIniciar := TNewButton.Create(MAIL);
BotonIniciar.Caption := 'Iniciar';
BotonIniciar.OnClick := #BotonIniciarOnClick;
BotonIniciar.Parent := WizardForm;
BotonIniciar.Left := WizardForm.NextButton.Left - 250 ;
BotonIniciar.Top := WizardForm.CancelButton.Top - 10;
BotonIniciar.Width := WizardForm.NextButton.Width + 60;
BotonIniciar.Height := WizardForm.NextButton.Height + 10;
end;
I'm might be missing some parts of the code, but I think it's understandable.
Fist I create the input page, then I create a button with the OnClick property that calls to the BotonIniciarOnClick procedure.
Actually, the code works great. But as I said I'm having a floating cmd window.
I would like to see something like this:
It's just a random image I took from google.
What I want to see is similar to a standard "show details" option on an installer.
You can redirect the command output to a file and monitor the file for changes, loading them to list box (or maybe a memo box).
var
ProgressPage: TOutputProgressWizardPage;
ProgressListBox: TNewListBox;
function SetTimer(
Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
external 'SetTimer#user32.dll stdcall';
function KillTimer(hWnd: LongWord; uIDEvent: LongWord): BOOL;
external 'KillTimer#user32.dll stdcall';
var
ProgressFileName: string;
function BufferToAnsi(const Buffer: string): AnsiString;
var
W: Word;
I: Integer;
begin
SetLength(Result, Length(Buffer) * 2);
for I := 1 to Length(Buffer) do
begin
W := Ord(Buffer[I]);
Result[(I * 2)] := Chr(W shr 8); { high byte }
Result[(I * 2) - 1] := Chr(Byte(W)); { low byte }
end;
end;
procedure UpdateProgress;
var
S: AnsiString;
I, L, Max: Integer;
Buffer: string;
Stream: TFileStream;
Lines: TStringList;
begin
if not FileExists(ProgressFileName) then
begin
Log(Format('Progress file %s does not exist', [ProgressFileName]));
end
else
begin
try
// Need shared read as the output file is locked for writing,
// so we cannot use LoadStringFromFile
Stream :=
TFileStream.Create(ProgressFileName, fmOpenRead or fmShareDenyNone);
try
L := Stream.Size;
Max := 100*2014;
if L > Max then
begin
Stream.Position := L - Max;
L := Max;
end;
SetLength(Buffer, (L div 2) + (L mod 2));
Stream.ReadBuffer(Buffer, L);
S := BufferToAnsi(Buffer);
finally
Stream.Free;
end;
except
Log(Format('Failed to read progress from file %s - %s', [
ProgressFileName, GetExceptionMessage]));
end;
end;
if S <> '' then
begin
Log('Progress len = ' + IntToStr(Length(S)));
Lines := TStringList.Create();
Lines.Text := S;
for I := 0 to Lines.Count - 1 do
begin
if I < ProgressListBox.Items.Count then
begin
ProgressListBox.Items[I] := Lines[I];
end
else
begin
ProgressListBox.Items.Add(Lines[I]);
end
end;
ProgressListBox.ItemIndex := ProgressListBox.Items.Count - 1;
ProgressListBox.Selected[ProgressListBox.ItemIndex] := False;
Lines.Free;
end;
// Just to pump a Windows message queue (maybe not be needed)
ProgressPage.SetProgress(0, 1);
end;
procedure UpdateProgressProc(
H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
begin
UpdateProgress;
end;
procedure BotonIniciarOnClick(Sender: TObject);
var
ResultCode: Integer;
Timer: LongWord;
AppPath: string;
AppError: string;
Command: string;
begin
ProgressPage :=
CreateOutputProgressPage(
'Installing something', 'Please wait until this finishes...');
ProgressPage.Show();
ProgressListBox := TNewListBox.Create(WizardForm);
ProgressListBox.Parent := ProgressPage.Surface;
ProgressListBox.Top := 0;
ProgressListBox.Left := 0;
ProgressListBox.Width := ProgressPage.SurfaceWidth;
ProgressListBox.Height := ProgressPage.SurfaceHeight;
// Fake SetProgress call in UpdateProgressProc will show it,
// make sure that user won't see it
ProgressPage.ProgressBar.Top := -100;
try
Timer := SetTimer(0, 0, 250, CreateCallback(#UpdateProgressProc));
ExtractTemporaryFile('install.bat');
AppPath := ExpandConstant('{tmp}\install.bat');
ProgressFileName := ExpandConstant('{tmp}\progress.txt');
Log(Format('Expecting progress in %s', [ProgressFileName]));
Command := Format('""%s" > "%s""', [AppPath, ProgressFileName]);
if not Exec(ExpandConstant('{cmd}'), '/c ' + Command, '', SW_HIDE,
ewWaitUntilTerminated, ResultCode) then
begin
AppError := 'Cannot start app';
end
else
if ResultCode <> 0 then
begin
AppError := Format('App failed with code %d', [ResultCode]);
end;
UpdateProgress;
finally
// Clean up
KillTimer(0, Timer);
ProgressPage.Hide;
DeleteFile(ProgressFileName);
ProgressPage.Free();
end;
if AppError <> '' then
begin
// RaiseException does not work properly while
// TOutputProgressWizardPage is shown
RaiseException(AppError);
end;
end;
Above was tested with a batch file like:
#echo off
echo Starting
echo Doing A...
echo Extracting something...
echo Doing B...
echo Extracting something...
timeout /t 1 > nul
echo Doing C...
echo Extracting something...
echo Doing D...
echo Extracting something...
timeout /t 1 > nul
echo Doing E...
echo Extracting something...
echo Doing F...
echo Extracting something...
timeout /t 1 > nul
...
If you want to display the output as part of the installation process, instead of on a button click, see:
Execute a batch file after installation and display its output on a custom page before Finished page in Inno Setup

Indy 10.6 using tcpserver on Linux system with admin rights throws Gtk-WARNING when client disconnects

I'm setting up a new system using Indy 10.6 tcpserver on a Raspberry PI with the latest Raspbian loaded. I am running the app from the GUI desktop via a terminal bash script with sudo. Everything works fine until a client connects, then when it disconnects I get Gtk-WARNINGs, and some times Gtk-CRITICALs and I don't know why. Here's my code, it only allows one client connection at a time, then it deactivates the server and restarts it after each connection is done:
Procedure TFK20Elevator.ASpeedBtn1Click(Sender: TObject);
Begin //start the server
Server.Active := False;
Server.Bindings.Clear;
Server.Bindings.Add.IPVersion := Id_IPv4;
Server.Bindings.Add.IP := LIP;
Server.Bindings.Add.Port := DefPort + StrToIntDef(UnitID, 0);
Try
Server.Active := True;
Except
On E: Exception Do
Memo1.Lines.Add(E.Message);
End;
If Not Server.Active Then
Exit;
ASpeedBtn1.Enabled := False;
ASpeedBtn2.Enabled := True;
AStatus1.SimpleText := 'Server bound to ' + LIP + ':' + IntToStr(DefPort + StrToIntDef(UnitID, 0));
End;
Procedure TFK20Elevator.ServerConnect(AContext: TIdContext);
Begin
If Connected Then
Begin
Abort();
Exit;
End;
AStatus1.SimpleText := 'Connecting to> ' + AContext.Binding.PeerIP + ' - Authenticating...';
Memo1.Lines.Clear;
Manager := False;
EncDecSIdx := 1;
RetryTimer.Enabled := False;
RetryTimer.Interval := 3000;
Authenticating := True;
AuthTimer.Enabled := True;
StayAlive.Enabled := True;
End;
Procedure TFK20Elevator.ServerException(AContext: TIdContext; AException: Exception);
Begin
If AnsiContainsText(AException.Message, 'Gracefully') Then
AStatus1.SimpleText := 'Server bound to ' + LIP + ':' + IntToStr(DefPort + StrToIntDef(UnitID, 0)) //closed gracefully message
Else
Begin //show the exception
Memo1.Lines.Add('An exception happend! - ' + AException.Message);
RetryTimer.Enabled := True;
End;
Manager := False;
Authenticating := False;
End;
Procedure TFK20Elevator.ServerExecute(AContext: TIdContext);
//EncStr and DecStr simply encode/decode, respectively, a standard
// string into/from a key encrypted hex string, i.e. '00' to 'FF'
// for each character in the string
Var
S, UserName, Password: String;
I, N: Integer;
Begin
S := AContext.Connection.IOHandler.ReadLn(IndyTextEncoding_OSDefault, IndyTextEncoding_OSDefault); //get the data
If S = Heart Then //if message is the client heart beat, return to client
Begin //just a heart beat, reset timer
StayAlive.Enabled := False;
AContext.Connection.IOHandler.WriteLn(Heart, IndyTextEncoding_OSDefault, IndyTextEncoding_OSDefault);
StayAlive.Enabled := True;
Exit;
End;
S := PCommon.DecStr(S, EncDecStr, EncDecSIdx); //not heart beat, decompress
If Authenticating Then
Begin //test log in
If Length(S) > 3 Then
Begin
I := Pos('|', S);
If (I > 1) And (Length(S) > I) Then
Begin
UserName := Copy(S, 1, I - 1);
Password := Copy(S, I + 1, Length(S) - I);
If UserName = ManUser Then
Begin
If Password = ManPass Then
Begin
AuthTimer.Enabled := False;
Manager := True;
Authenticating := False;
AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP +
':' + IntToStr(DefPort + StrToIntDef(UnitID, 0)) + 'M',
EncDecStr, EncDecSIdx), IndyTextEncoding_OSDefault,
IndyTextEncoding_OSDefault);
AStatus1.SimpleText := 'Connecting to> ' + AContext.Binding.PeerIP + ' as Manager';
Connected := True;
End
Else
AuthTimerTimer(Self);
End
Else If UserName = GenUser Then
Begin
If Password = GenPass Then
Begin
AuthTimer.Enabled := False;
Authenticating := False;
AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP +
':' + IntToStr(DefPort + StrToIntDef(UnitID, 0)) + 'U',
EncDecStr, EncDecSIdx), IndyTextEncoding_OSDefault,
IndyTextEncoding_OSDefault);
AStatus1.SimpleText := 'Connecting to> ' + AContext.Binding.PeerIP + ' as General User';
Connected := True;
End
Else
AuthTimerTimer(Self);
End
Else
AuthTimerTimer(Self);
End
Else
AuthTimerTimer(Self);
End
Else
AuthTimerTimer(Self);
End
Else
Begin //test for commands
If Copy(S, 1, Length(AssignID)) = AssignID Then
Begin //command to assign a new unit id
NewLoc := DefLocation;
NewUnit := DefUnitNum;
I := Pos('-', S, 1);
If (I > 0) And (I < Length(S)) Then
Begin
N := Pos('-', S, I + 1);
If (N > 0) And (N < Length(S)) Then
Begin
NewLoc := Copy(S, I + 1, N - I - 1);
NewUnit := Copy(S, N + 1, Length(S) - N);
End;
End;
Label15.Caption := NewLoc;
Label16.Caption := NewUnit;
FmtStr(LIP, '%.3d', [StrToInt(NewUnit)]);
LIP := '192.168.6' + Copy(LIP, 1, 1) + '.' + Copy(LIP, 2, 2); //wifi ip
Memo1.Lines.Add('--> ' + S + '-' + LIP);
AContext.Connection.IOHandler.WriteLn(PCommon.EncStr(Rebooting, EncDecStr, EncDecSIdx),
IndyTextEncoding_OSDefault, IndyTextEncoding_OSDefault);
Memo1.Lines.Add('<-- ' + Rebooting);
TestTimer.Enabled := True;
End;
End;
End;
Procedure TFK20Elevator.ASpeedBtn2Click(Sender: TObject);
Begin //shut down the server with optional restart if not rebooting
AuthTimer.Enabled := False;
RetryTimer.Enabled := False;
StayAlive.Enabled := False;
TestTimer.Enabled := False;
DropClient;
Try
Server.Active := False;
Except
On E: Exception Do
Memo1.Lines.Add('Error disconnecting server - ' + E.Message);
End;
If Server.Active Then
Exit;
ASpeedBtn1.Enabled := True;
ASpeedBtn2.Enabled := False;
AStatus1.SimpleText := 'Server not running...';
Manager := False;
Authenticating := False;
Connected := False;
RetryTimer.Enabled := Not SysReboot;
End;
Procedure TFK20Elevator.ServerDisconnect(AContext: TIdContext);
Begin
StayAlive.Enabled := False;
RetryTimer.Enabled := False;
DropClient;
AStatus1.SimpleText := 'Client disconnected...';
Manager := False;
Authenticating := False;
Connected := False;
RetryTimer.Enabled := Not SysReboot;
End;
Procedure TFK20Elevator.DropClient; //make sure buffers are cleared
Var
I: Integer;
SC: TIdContext;
Begin
If Server.Active Then
Begin
Application.ProcessMessages;
With Server.Contexts.LockList Do
Try
Memo1.Lines.Add('Disconnecting...');
For I := Count - 1 DownTo 0 Do
Begin
SC := TIdContext(Items[I]);
If SC = Nil Then
Continue;
SC.Connection.IOHandler.WriteBufferClear;
SC.Connection.IOHandler.InputBuffer.Clear;
SC.Connection.IOHandler.Close;
If SC.Connection.Connected Then
SC.Connection.Disconnect;
Memo1.Lines.Add('Disconnecting client ' + IntToStr(I + 1) + ' of ' + IntToStr(Count));
End;
Finally
Server.Contexts.UnlockList;
Memo1.Lines.Add('Disconnected');
End;
End;
End;
Procedure TFK20Elevator.StayAliveTimer(Sender: TObject);
Begin //server reset timer if client stops sending heart beat
StayAlive.Enabled := False;
AStatus1.SimpleText := 'Client timed out!';
If ASpeedBtn2.Enabled Then
ASpeedBtn2Click(Self);
End;
Procedure TFK20Elevator.AuthTimerTimer(Sender: TObject);
Begin //login authorization timeout timer
AuthTimer.Enabled := False;
ASpeedBtn2Click(Self);
Application.ProcessMessages;
ASpeedBtn1Click(Self);
End;
Server.Bindings.Add.IPVersion := Id_IPv4;
Server.Bindings.Add.IP := LIP;
Server.Bindings.Add.Port := DefPort + StrToIntDef(UnitID, 0);
This is a bug in your code. You are not opening just 1 listening socket, you are actually opening 3 listening sockets! Every call to Bindings.Add() will tell TIdTCPServer to create a separate listening socket, and each Binding object has its own IP/Port settings.
What you are really doing with the above code is:
creating an IPv4 Binding on IP 0.0.0.0 on port TIdTCPServer.DefaultPort.
creating another Binding on IP LIP on port TIdTCPServer.DefaultPort using Indy's default IP version (which happens to be IPv4, unless you recompile Indy with IdIPv6 defined in IdCompilerDefines.inc).
creating yet another Binding on IP 0.0.0.0 or ::1 on port DefPort+UnitID depending on Indy's default IP version.
For what you are attempting to do, you need to call Bindings.Add() one time only, eg:
var
Binding : TIdSocketHandle;
Binding := Server.Bindings.Add;
Binding.IPVersion := Id_IPv4;
Binding.IP := LIP;
Binding.Port := DefPort + StrToIntDef(UnitID, 0);
That being said, TIdTCPServer is a multi-threaded component. Its various events (OnConnect, OnDisconnect, OnExecute, OnException, and OnListenException) are fired in the context of worker threads created internally by TIdTCPServer. Your event handlers are directly accessing UI controls from outside the context of the main UI thread. That causes all kinds of problems, and must never be done.
If your event handlers need to access your UI, they must synchronize with the main UI thread, such as with TThread.Synchronize() or TThread.Queue(), or Indy's own TIdSync or TIdNotify class, or any other inter-thread synching mechanism of your choosing.
Also, when you go to manually drop clients with DropClient(), it is doing things to the contexts that it has no business doing. You don't even need to drop the clients manually anyway, as TIdTCPServer handles that for you while it is being deactivated.
With all of that said, try something more like this:
interface
uses
Classes, Form, SysUtils, StdCtrls, ExtCtrls, Buttons, IdTCPServer, IdContext;
type
TFK20Elevator = class(TForm)
Server: TIdTCPServer;
ASpeedBtn1: TSpeedButton;
ASpeedBtn2: TSpeedButton;
Memo1: TMemo;
AStatus1: TStatusBar;
AuthTimer: TTimer;
RetryTimer: TTimer;
StayAlive: TTimer;
TestTimer: TTimer;
...
procedure ASpeedBtn1Click(Sender: TObject);
procedure ASpeedBtn2Click(Sender: TObject);
procedure StayAliveTimer(Sender: TObject);
procedure AuthTimerTimer(Sender: TObject);
procedure ServerConnect(AContext: TIdContext);
procedure ServerDisconnect(AContext: TIdContext);
procedure ServerException(AContext: TIdContext; AException: Exception);
procedure ServerExecute(AContext: TIdContext);
...
private
DefPort: Integer;
UnitID: string;
Manager: Boolean;
Authenticating: Boolean;
EncDecSIdx: Integer;
...
procedure DropClient;
procedure ConnectedNotify(const APeerIP: string);
procedure DisconnectedNotify;
procedure ErrorNotify(const AMessage: string);
procedure HeartNotify;
procedure ManagerLoggedInNotify(const APeerIP: string);
procedure GeneralUserLoggedInNotify(const APeerIP: string);
procedure FailedAuthNotify;
procedure RebootNotify(const Data: string);
...
end;
var
FK20Elevator: TFK20Elevator;
implementation
uses
IdGlobal, IdSync;
const
Heart: string = ...;
AssignID: string = ...;
...
procedure TFK20Elevator.ASpeedBtn1Click(Sender: TObject);
var
Binding: TIdSocketHandle;
begin
//start the server
Server.Active := False;
Server.Bindings.Clear;
Binding := Server.Bindings.Add;
Binding.IPVersion := Id_IPv4;
Binding.IP := LIP;
Binding.Port := DefPort + StrToIntDef(UnitID, 0);
Server.MaxConnections := 1;
try
Server.Active := True;
except
on E: Exception do
begin
Memo1.Lines.Add('Error activating server - ' + E.Message);
Exit;
end;
end;
AStatus1.SimpleText := 'Server bound to ' + Binding.IP + ':' + IntToStr(Binding.Port);
ASpeedBtn1.Enabled := False;
ASpeedBtn2.Enabled := True;
end;
procedure TFK20Elevator.ASpeedBtn2Click(Sender: TObject);
begin
//shut down the server with optional restart if not rebooting
AuthTimer.Enabled := False;
RetryTimer.Enabled := False;
StayAlive.Enabled := False;
TestTimer.Enabled := False;
try
Server.Active := False;
except
on E: Exception do
begin
Memo1.Lines.Add('Error deactivating server - ' + E.Message);
Exit;
end;
end;
Manager := False;
Authenticating := False;
AStatus1.SimpleText := 'Server not running...';
ASpeedBtn1.Enabled := True;
ASpeedBtn2.Enabled := False;
RetryTimer.Enabled := not SysReboot;
end;
procedure TFK20Elevator.StayAliveTimer(Sender: TObject);
begin
//client stopped sending heart beats
StayAlive.Enabled := False;
Memo1.Lines.Add('Client timed out!');
DropClient;
end;
procedure TFK20Elevator.AuthTimerTimer(Sender: TObject);
begin
//login authorization timeout
AuthTimer.Enabled := False;
Memo1.Lines.Add('Authentication timed out!');
DropClient;
end;
procedure TFK20Elevator.DropClient;
begin
with Server.Contexts.LockList do
try
if Count > 0 then
TIdContext(Items[0]).Connection.Disconnect;
finally
Server.Contexts.UnlockList;
end;
end;
type
TMyNotifyMethod = procedure(const AStr: string) of object;
TMyNotify = class(TIdNotify)
protected
FMethod: TMyNotifyMethod;
FStr: string;
procedure DoNotify; override;
public
class procedure NotifyStr(AMethod: TMyNotifyMethod; const AStr: string);
end;
procedure TMyNotify.DoNotify;
begin
FMethod(FStr);
end;
class procedure TMyNotify.NotifyStr(AMethod: TMyNotifyMethod; const AStr: string);
begin
with Create do
begin
FMethod := AMethod;
FStr := AStr;
Notify;
end;
end;
procedure TFK20Elevator.ConnectedNotify(const APeerIP: string);
begin
if not Server.Active then Exit;
AStatus1.SimpleText := 'Connecting to> ' + APeerIP + ' - Authenticating...';
Memo1.Lines.Clear;
RetryTimer.Enabled := False;
RetryTimer.Interval := 3000;
AuthTimer.Enabled := True;
StayAlive.Enabled := True;
end;
procedure TFK20Elevator.DisconnectedNotify;
begin
StayAlive.Enabled := False;
RetryTimer.Enabled := False;
if Server.Active then
begin
with Server.Bindings[0] do
AStatus1.SimpleText := 'Client Disconnected. Server bound to ' + IP + ':' + IntToStr(Port);
end;
RetryTimer.Enabled := Not SysReboot;
end;
procedure TFK20Elevator.ErrorNotify(const AMessage: string);
begin
Memo1.Lines.Add('An exception happened! - ' + AMessage);
RetryTimer.Enabled := True;
end;
procedure TFK20Elevator.HeartNotify;
begin
StayAlive.Enabled := False;
StayAlive.Enabled := True;
end;
procedure TFK20Elevator.ManagerLoggedInNotify(const APeerIP: string);
begin
AuthTimer.Enabled := False;
AStatus1.SimpleText := 'Connecting to> ' + APeerIP + ' as Manager';
end;
procedure TFK20Elevator.GeneralUserLoggedInNotify(const APeerIP: string);
begin
AuthTimer.Enabled := False;
AStatus1.SimpleText := 'Connecting to> ' + APeerIP + ' as General User';
end;
procedure TFK20Elevator.FailedAuthNotify;
begin
//login authorization failed
AuthTimer.Enabled := False;
end;
procedure TFK20Elevator.RebootNotify(const Data: string);
var
Tmp, S, NewLoc, NewUnit, LIP: string;
begin
Tmp := Data;
S := Fetch(Tmp, #10);
NewLoc := Fetch(Tmp, #10);
NewUnit := Tmp;
Label15.Caption := NewLoc;
Label16.Caption := NewUnit;
FmtStr(LIP, '%.3d', [StrToInt(NewUnit)]);
LIP := '192.168.6' + Copy(LIP, 1, 1) + '.' + Copy(LIP, 2, 2); //wifi ip
Memo1.Lines.Add('--> ' + S + '-' + LIP);
Memo1.Lines.Add('<-- ' + Rebooting);
TestTimer.Enabled := True;
end;
procedure TFK20Elevator.ServerConnect(AContext: TIdContext);
begin
Manager := False;
Authenticating := True;
EncDecSIdx := 1;
TMyNotify.NotifyStr(#ConnectedNotify, AContext.Binding.PeerIP);
// Note: OSDefault is platform-specific. On Linux, it is UTF-8, so
// you should use UTF-8 explicitly instead, so as to provide
// better compatibility across platforms, especially if you ever
// move this server code to another platform in the future...
//
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_OSDefault; // IndyTextEncoding_UTF8
AContext.Connection.IOHandler.DefAnsiEncoding := IndyTextEncoding_OSDefault; // IndyTextEncoding_UTF8
end;
procedure TFK20Elevator.ServerDisconnect(AContext: TIdContext);
begin
Manager := False;
Authenticating := False;
TIdNotify.NotifyMethod(#DisconnectedNotify);
end;
procedure TFK20Elevator.ServerException(AContext: TIdContext; AException: Exception);
begin
if not (AException is EIdConnClosedGracefully) then
TMyNotify.NotifyStr(#ErrorNotify, AException.Message);
end;
procedure TFK20Elevator.ServerExecute(AContext: TIdContext);
var
S, Tmp, UserName, Password: String;
begin
S := AContext.Connection.IOHandler.ReadLn; //get the data
if S = Heart then
begin
//just a heart beat, return to client and reset timer
AContext.Connection.IOHandler.WriteLn(Heart);
TIdNotify.NotifyMethod(#HeartNotify);
Exit;
end;
//not heart beat, decompress
//EncStr and DecStr simply encode/decode, respectively, a standard
// string into/from a key encrypted hex string, i.e. '00' to 'FF'
// for each character in the string
S := PCommon.DecStr(S, EncDecStr, EncDecSIdx);
if Authenticating then
begin
//test log in
UserName := Fetch(S, '|');
Password := S;
if (UserName = ManUser) and (Password = ManPass) then
begin
Authenticating := False;
Manager := True;
AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP + ':' + IntToStr(AContext.Binding.Port) + 'M', EncDecStr, EncDecSIdx));
TMyNotify.NotifyStr(#ManagerLoggedInNotify, AContext.Binding.PeerIP);
end
else if (UserName = GenUser) and (Password = GenPass) then
begin
Authenticating := False;
AContext.Connection.IOHandler.WriteLn(EncStr(AContext.Binding.PeerIP + ':' + IntToStr(AContext.Binding.Port) + 'U', EncDecStr, EncDecSIdx));
TMyNotify.NotifyStr(#GeneralUserLoggedInNotify, AContext.Binding.PeerIP);
end else
begin
TIdNotify.NotifyMethod(#FailedAuthNotify);
AContext.Connection.Disconnect;
end;
Exit;
end;
//test for commands
if TextStartsWith(S, AssignID) then
begin
//command to assign a new unit id
Tmp := S;
Fetch(Tmp, '-');
NewLoc := Fetch(Tmp, '-');
NewUnit := Tmp;
if (NewLoc = '') or (NewUnit = '') then
begin
NewLoc := DefLocation;
NewUnit := DefUnitNum;
end;
AContext.Connection.IOHandler.WriteLn(PCommon.EncStr(Rebooting, EncDecStr, EncDecSIdx));
TMyNotify.NotifyStr(#RebootNotify, S + #10 + NewLoc + #10 + NewUnit);
end;
end;
Lastly, I would suggest you consider getting rid of all your global variables and heartbeat/authentication timers from the main UI thread. Do timeout handling inside of the OnExecute event itself instead, such as making use of the client's ReadTimeout property and/or CheckForDataOnSource() method. Use the TIdContext.Data property (or derive a new class from TIdServerContext and assign it to the TIdTCPServer.ContextClass property) to keep track of per-connection values, like the last time a heartbeat was received, or whether the client is still authenticating (actually, you should handle authentication in the OnConnect before OnExecute even starts running), or whether the client is logged in as a manager, etc. That will reduce the amount of things that needs to be synchronized with the main UI thread, and avoid any timing issues introduced by delays in sync processing.

Passing values into a procedure from NextButtonClicked method Inno Setup

Currently I'm trying to make an installer utility that sets the static IP for the Wi-Fi on Inno Setup. I'm having trouble finding a way to make the correct command insert when the Next button of custom page is pressed. The NextButtonClick method is called correctly however when I actually hit the next button on my page it merely exits without executing the query.
procedure InitializeWizard();
var
Page: TInputQueryWizardPage;
ipAddress, subnetMask, defaultGateway, prefferredDNSServer: String;
ResultCode: Integer;
begin
Page := CreateInputQueryPage(wpWelcome,
'Set Network Config', 'A window for setting the wifi configuration',
'Please indicate the IP address and press next when finished.');
{ Add items (False means it's not a password edit) }
Page.Add('IP Address:', False);
Page.Add('Subnet mask:', False);
Page.Add('Default gateway:', False);
Page.Add('Preferred DNS server:', False);
{ Set initial values (optional) }
Page.Values[0] := ExpandConstant('0.0.0.0');
ipAddress := Page.Values[0]
Page.Values[1] := ExpandConstant('0.0.0.0');
subnetMask := Page.Values[1]
Page.Values[2] := ExpandConstant('0.0.0.0');
defaultGateway := Page.Values[2]
Page.Values[3] := ExpandConstant('0.0.0.0');
prefferredDNSServer := Page.Values[3]
if NextButtonClick(Page.ID) then
begin
Exec('cmd.exe',
'/k ' + 'netsh interface ip set address "Wi-Fi" static ' + ipAddress + ' ' +
subnetMask + ' ' + defaultGateway + ' ' + prefferredDNSServer,
'', SW_SHOW, ewWaitUntilTerminated, ResultCode)
end;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
ipAddress, subnetMask, defaultGateway, prefferredDNSServer: String;
ResultCode: Integer;
begin
Result := True
Log('NextButtonClick(' + IntToStr(CurPageID) + ') called');
case CurPageID of
100:
{ ipAddress := getParams(ipAddress); }
{ setWifi(ipAddress, subnetMask, defaultGateway, prefferredDNSServer); }
Result:= True;
end;
end;
You do not call the NextButtonClick function yourself. It's an event function, so it's called by Inno Setup.
The code should be like:
var
NetPage: TInputQueryWizardPage;
procedure InitializeWizard();
begin
NetPage :=
CreateInputQueryPage(wpWelcome,
'Set Network Config', 'A window for setting the wifi configuration',
'Please indicate the IP address and press next when finished.');
{ Add items (False means it's not a password edit) }
NetPage.Add('IP Address:', False);
NetPage.Add('Subnet mask:', False);
NetPage.Add('Default gateway:', False);
NetPage.Add('Preferred DNS server:', False);
{ Set initial values (optional) }
NetPage.Values[0] := ExpandConstant('0.0.0.0');
NetPage.Values[1] := ExpandConstant('0.0.0.0');
NetPage.Values[2] := ExpandConstant('0.0.0.0');
NetPage.Values[3] := ExpandConstant('0.0.0.0');
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
ipAddress, subnetMask, defaultGateway, prefferredDNSServer: String;
ResultCode: Integer;
Command: string;
begin
Result := True;
if CurPageID = NetPage.ID then
begin
ipAddress := NetPage.Values[0];
subnetMask := NetPage.Values[1];
defaultGateway := NetPage.Values[2];
prefferredDNSServer := NetPage.Values[3];
Command :=
'netsh interface ip set address "Wi-Fi" static ' +
ipAddress + ' ' + subnetMask + ' ' + defaultGateway + ' ' + prefferredDNSServer;
Exec('cmd.exe', '/C ' + Command, '', SW_SHOW, ewWaitUntilTerminated, ResultCode)
end;
end;
Though, in general, you should do any changes to the target system, until the user confirms the installation by clicking "Install" on the "Ready to Install" page.
While you can use CurPageID = wpReady for that, a more idiomatic approach is using CurStepChanged(ssInstall) (or ssPostInstall):
procedure CurStepChanged(CurStep: TSetupStep);
var
ipAddress, subnetMask, defaultGateway, prefferredDNSServer: String;
ResultCode: Integer;
Command: string;
begin
if CurStep = ssInstall then
begin
ipAddress := NetPage.Values[0];
subnetMask := NetPage.Values[1];
defaultGateway := NetPage.Values[2];
prefferredDNSServer := NetPage.Values[3];
Command :=
'netsh interface ip set address "Wi-Fi" static ' +
ipAddress + ' ' + subnetMask + ' ' + defaultGateway + ' ' + prefferredDNSServer;
Exec('cmd.exe', '/C ' + Command, '', SW_SHOW, ewWaitUntilTerminated, ResultCode)
end;
end;

how to pass a variable value to an xml file?

this is my code to enter a port number from user.upon installing i want to get the port number changed in apache tomcat server.xml file.
Iam passing apache tomcat zip file also using files section and unzip it in run section
var
javaVersion: String;
javaPath: String;
//port number code
function SetFocus(hWnd: HWND): HWND;
external 'SetFocus#user32.dll stdcall';
var
SerialPage: TWizardPage;
SerialEdits: array of TEdit;
const
CF_TEXT = 1;
VK_BACK = 8;
SC_EDITCOUNT = 1;
SC_CHARCOUNT = 4;
procedure OnSerialEditChange(Sender: TObject);
var
I: Integer;
CanContinue: Boolean;
begin
CanContinue := True;
for I := 0 to GetArrayLength(SerialEdits) - 1 do
if Length(SerialEdits[I].Text) < SC_CHARCOUNT then
begin
CanContinue := False;
Break;
end;
WizardForm.NextButton.Enabled := CanContinue;
end;
function GetSerialNumber(Param: String): string;
var
I: Integer;
begin
Result := '';
for I := 0 to GetArrayLength(SerialEdits) - 1 do
Result := Result + SerialEdits[I].Text ;
end;
procedure OnSerialEditKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
Edit: TEdit;
EditIndex: Integer;
begin
Edit := TEdit(Sender);
EditIndex := Edit.TabOrder - SerialEdits[0].TabOrder;
if (EditIndex = 0) and (Key = Ord('V')) and (Shift = [ssCtrl]) then
begin
if TryPasteSerialNumber then
Key := 0;
end
else
if (Key >= 32) and (Key <= 255) then
begin
if Length(Edit.Text) = SC_CHARCOUNT - 1 then
begin
if EditIndex < GetArrayLength(SerialEdits) - 1 then
SetFocus(SerialEdits[EditIndex + 1].Handle)
else
SetFocus(WizardForm.NextButton.Handle);
end;
end
else
if Key = VK_BACK then
if (EditIndex > 0) and (Edit.Text = '') and (Edit.SelStart = 0) then
SetFocus(SerialEdits[EditIndex - 1].Handle);
end;
procedure CreateSerialNumberPage;
var
I: Integer;
Edit: TEdit;
DescLabel: TLabel;
EditWidth: Integer;
begin
SerialPage := CreateCustomPage(wpWelcome, 'Serial number validation',
'Enter the valid serial number');
DescLabel := TLabel.Create(SerialPage);
DescLabel.Top := 16;
DescLabel.Left := 0;
DescLabel.Parent := SerialPage.Surface;
DescLabel.Caption := 'Enter the valid serial number and continue with the installation...';
DescLabel.Font.Style := [fsBold];
SetArrayLength(SerialEdits, SC_EDITCOUNT);
EditWidth := (SerialPage.SurfaceWidth - ((SC_EDITCOUNT - 1) * 8)) div SC_EDITCOUNT;
for I := 0 to SC_EDITCOUNT - 1 do
begin
Edit := TEdit.Create(SerialPage);
Edit.Top := 40;
Edit.Left := I * (EditWidth + 8);
Edit.Width := EditWidth;
Edit.CharCase := ecUpperCase;
Edit.MaxLength := SC_CHARCOUNT;
Edit.Parent := SerialPage.Surface;
Edit.OnChange := #OnSerialEditChange;
Edit.OnKeyDown := #OnSerialEditKeyDown;
SerialEdits[I] := Edit;
end;
end;
procedure CurPageChanged(CurPageID: Integer);
begin
if CurPageID = SerialPage.ID then
WizardForm.NextButton.Enabled := False;
end;
procedure InitializeWizard;
begin
CreateSerialNumberPage;
end ;
i want to replace the port number which was entered by user in tomcats server.xml using tokens
<Connector port="##portnumber##" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
Here's a script I've made for you. I've changed the way of entering port number and shown how to modify attribute values in XML files. Also notice the usage of the AfterInstall function:
#define TomcatDest "{app}\tomcat"
#define TomcatFullPath TomcatDest + "\apache-tomcat-7.0.42"
#define TomcatSrvConfigFile TomcatFullPath + "\conf\server.xml"
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
[Files]
Source: "unzip.exe"; DestDir: "{tmp}\installertemps"
Source: "apache-tomcat-7.0.42.zip"; DestDir: "{tmp}\installertemps"
[Run]
Filename: "{tmp}\installertemps\unzip.exe"; Parameters: " ""{tmp}\installertemps\apache-tomcat-7.0.42.zip"" -d ""{#TomcatDest}"" "; AfterInstall: UpdateConfigFile(ExpandConstant('{#TomcatSrvConfigFile}'))
[Code]
const
DefaultPort = 8080;
var
ConfigPage: TInputQueryWizardPage;
procedure SaveAttrValueToXML(const FileName, NodePath, Attribute,
Value: string);
var
XMLNode: Variant;
XMLDocument: Variant;
begin
XMLDocument := CreateOleObject('Msxml2.DOMDocument');
try
XMLDocument.async := False;
XMLDocument.load(FileName);
if (XMLDocument.parseError.errorCode <> 0) then
MsgBox('The XML file could not be parsed. ' +
XMLDocument.parseError.reason, mbError, MB_OK)
else
begin
XMLDocument.setProperty('SelectionLanguage', 'XPath');
XMLNode := XMLDocument.selectSingleNode(NodePath);
XMLNode.setAttribute(Attribute, Value);
XMLDocument.save(FileName);
end;
except
MsgBox('An error occured!' + #13#10 + GetExceptionMessage,
mbError, MB_OK);
end;
end;
procedure InitializeWizard;
begin
ConfigPage := CreateInputQueryPage(wpSelectDir, 'Tomcat configuration',
'Description', 'SubCaption');
ConfigPage.Add('Port:', False);
ConfigPage.Values[0] := IntToStr(DefaultPort);
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
PortNumber: Integer;
begin
Result := True;
if CurPageID = ConfigPage.ID then
begin
PortNumber := StrToIntDef(ConfigPage.Values[0], -1);
// modify the statement to allow users enter only valid port numbers;
// currently the value of -1 means that there is not even a number entered
// in the edit box
if (PortNumber = -1) then
begin
Result := False;
MsgBox('You''ve entered invalid port number. The setup cannot continue...', mbError, MB_OK);
end;
end;
end;
procedure UpdateConfigFile(const FileName: string);
begin
SaveAttrValueToXML(FileName, '//Server/Service/Connector', 'port',
ConfigPage.Values[0]);
end;

Inno Setup - How to edit a specific line from a text file during setup?

I need to edit a specific line from a text file using Inno Setup. I need my installer to find this line ("appinstalldir" "C:MYXFOLDER\\apps\\common\\App70") and use the directory path from the installer.
This is the code I am trying to use:
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssDone then
begin
SaveStringToFile(
ExpandConstant('{app}\app70.txt'),
'directory's path' + '\\apps\\common\\App70', True);
end;
end;
This is my text file:
"App"
{
"appID" "70"
{
"appinstalldir" "C:MYXFOLDER\\apps\\common\\App70"
}
}
This code can do it. But note, that this code doesn't check, if the value for the tag is enclosed by quote chars, once it finds a tag specified by TagName parameter, it cuts off the rest of the line and appends the value given by TagValue parameter:
function ReplaceValue(const FileName, TagName, TagValue: string): Boolean;
var
I: Integer;
Tag: string;
Line: string;
TagPos: Integer;
FileLines: TStringList;
begin
Result := False;
FileLines := TStringList.Create;
try
Tag := '"' + TagName + '"';
FileLines.LoadFromFile(FileName);
for I := 0 to FileLines.Count - 1 do
begin
Line := FileLines[I];
TagPos := Pos(Tag, Line);
if TagPos > 0 then
begin
Result := True;
Delete(Line, TagPos + Length(Tag), MaxInt);
Line := Line + ' "' + TagValue + '"';
FileLines[I] := Line;
FileLines.SaveToFile(FileName);
Break;
end;
end;
finally
FileLines.Free;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
NewPath: string;
begin
if CurStep = ssDone then
begin
NewPath := ExpandConstant('{app}') + '\apps\common\App70';
StringChangeEx(NewPath, '\', '\\', True);
if ReplaceValue(ExpandConstant('{app}\app70.txt'), 'appinstalldir',
NewPath)
then
MsgBox('Tag value has been replaced!', mbInformation, MB_OK)
else
MsgBox('Tag value has not been replaced!.', mbError, MB_OK);
end;
end;

Resources