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;
Related
My requirement is to validate the password entered by a user is the correct password with which he had logged-in. So, I have written the below code, but it is always saying that "Not logged-in". Any help?
var
DomainName,UserName,BackwardSlashString,DomainUserName : String;
ServerDetailsInputPage : TInputQueryWizardPage;
hToken, LoginOk : INTEGER;
function LogonUser(lpszUsername,lpszDomain,lpszPassword: string;
dwLogonType,dwLogonProvider: INTEGER; var hToken: INTEGER): INTEGER;
external 'LogonUserA#advapi32.dll stdcall';
procedure InitializeWizard();
begin
DomainName:= ExpandConstant(GetEnv('USERDOMAIN'));
UserName := ExpandConstant( +GetUserNameString);
BackwardSlashString := '\'
DomainUserName := DomainName + BackwardSlashString + UserName;
ServerDetailsInputPage :=
CreateInputQueryPage(wpWelcome,'','','Please enter following data and click Next.');
ServerDetailsInputPage.Add('IP Address',False);
ServerDetailsInputPage.Add('Port Number',False);
ServerDetailsInputPage.Add('Domain Name\User Name',False);
ServerDetailsInputPage.Add('Password',True);
ServerDetailsInputPage.Values[1] := '80';
ServerDetailsInputPage.Values[2] := DomainUserName;
end;
function RunAsUser(): BOOLEAN;
var
Passwd : String;
begin
DomainName := ExpandConstant(GetEnv('USERDOMAIN'));
UserName := ExpandConstant( +GetUserNameString);
Passwd := ServerDetailsInputPage.Values[3];
LoginOk := LogonUser(UserName,DomainName,Passwd,1,0,hToken);
if (not (LoginOk=0)) then
begin
MsgBox('successfully logged-in', mbInformation, MB_OK);
Result := true;
end
else if (LoginOk=0) then
begin
MsgBox('Not logged-in', mbInformation, MB_OK);
Result := false;
end;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
Result := True;
if CurPageID = ServerDetailsInputPage.ID then
begin
if not RunAsUser then
begin
MsgBox('Please enter correct Password!', mbError, MB_OK);
Result:=False;
end;
end;
end;
function InitializeSetup(): Boolean;
var
PrevInstallPath : String;
ResultCode : Integer;
FXStopStatus : Boolean;
begin
Result:=True;
end;
At first, your LogonUser function prototype is wrong as well as its call. You can't mix data types of the function prototype and you can't use arbitrary values in a function call. You can use something like this instead:
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
const
LOGON32_LOGON_INTERACTIVE = 2;
LOGON32_LOGON_NETWORK = 3;
LOGON32_LOGON_BATCH = 4;
LOGON32_LOGON_SERVICE = 5;
LOGON32_LOGON_UNLOCK = 7;
LOGON32_LOGON_NETWORK_CLEARTEXT = 8;
LOGON32_LOGON_NEW_CREDENTIALS = 9;
LOGON32_PROVIDER_DEFAULT = 0;
LOGON32_PROVIDER_WINNT40 = 2;
LOGON32_PROVIDER_WINNT50 = 3;
ERROR_SUCCESS = 0;
ERROR_LOGON_FAILURE = 1326;
function LogonUser(lpszUsername, lpszDomain, lpszPassword: string;
dwLogonType, dwLogonProvider: DWORD; var phToken: THandle): BOOL;
external 'LogonUser{#AW}#advapi32.dll stdcall';
var
ServerDetailsPage: TInputQueryWizardPage;
function TryLogonUser(const Domain, UserName, Password: string;
var ErrorCode: Longint): Boolean;
var
Token: THandle;
begin
Result := LogonUser(UserName, Domain, Password, LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, Token);
ErrorCode := DLLGetLastError;
end;
procedure InitializeWizard;
var
UserName: string;
begin
UserName := AddBackslash(GetEnv('USERDOMAIN')) + GetUserNameString;
ServerDetailsPage := CreateInputQueryPage(wpWelcome,
'', '', 'Please enter following data and click Next.');
ServerDetailsPage.Add('IP Address', False);
ServerDetailsPage.Add('Port Number', False);
ServerDetailsPage.Add('Domain Name\User Name', False);
ServerDetailsPage.Add('Password', True);
ServerDetailsPage.Values[1] := '80';
ServerDetailsPage.Values[2] := UserName;
end;
procedure ParseDomainUserName(const Value: string; var Domain,
UserName: string);
var
DelimPos: Integer;
begin
DelimPos := Pos('\', Value);
if DelimPos = 0 then
begin
Domain := '.';
UserName := Value;
end
else
begin
Domain := Copy(Value, 1, DelimPos - 1);
UserName := Copy(Value, DelimPos + 1, MaxInt);
end;
end;
function ServerDetailsLogonUser: Boolean;
var
Domain: string;
UserName: string;
Password: string;
ErrorCode: Longint;
begin
ParseDomainUserName(ServerDetailsPage.Values[2], Domain, UserName);
Password := ServerDetailsPage.Values[3];
Result := TryLogonUser(Domain, UserName, Password, ErrorCode);
case ErrorCode of
ERROR_SUCCESS:
MsgBox('Logon successful!', mbInformation, MB_OK);
ERROR_LOGON_FAILURE:
MsgBox('The user name or password is incorrect!', mbError, MB_OK);
else
MsgBox('Login failed!' + #13#10 + SysErrorMessage(DLLGetLastError),
mbError, MB_OK);
end;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
Result := True;
if CurPageID = ServerDetailsPage.ID then
Result := ServerDetailsLogonUser;
end;
I try to set specific range of values that accepted from user inputs within installation. For example, port field just accept range from 10000-20000.
I try to use this condition in NextButtonClick or even other. I searched in Pascal documentation, but I didn't find how to do that, else there is no question asked before here to set data validation for specific range.
My code as below:
[Code]
var
AdminDataPage: TInputQueryWizardPage;
Name, SuperPassword, ServerName, ServerPort : String;
function CreateAdminDataPage(): Integer;
begin
AdminDataPage := CreateInputQueryPage(wpSelectDir, 'Required Information', '', '');
AdminDataPage.Add('Name', False);
AdminDataPage.Add('SuperPassword', True);
AdminDataPage.Add('ServerName', False);
AdminDataPage.Add('ServerPort', False);
end;
procedure CreateAdminDataPage();
begin
CreateDataInputPage();
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if CurPageID = AdminDataPage.ID then
begin
Name := AdminDataPage.values[0];
SuperPassword := AdminDataPage.values[1];
ServerName := AdminDataPage.values[2];
ServerPort := AdminDataPage.values[3];
end;
end;
Just validate the input, display error message and make sure the NextButtonClick event function returns False:
function NextButtonClick(CurPageID: Integer): Boolean;
var
ServerPortInt: Integer;
begin
Result := True;
if CurPageID = AdminDataPage.ID then
begin
ServerPort := AdminDataPage.Values[3];
ServerPortInt := StrToIntDef(ServerPort, -1);
if (ServerPortInt < 10000) or (ServerPortInt > 20000) then
begin
MsgBox('Please enter port in range 10000-20000.', mbError, MB_OK);
WizardForm.ActiveControl := AdminDataPage.Edits[3];
Result := False;
end;
end;
end;
We are using Inno Setup(version 5.4.2) as the packaging tool to generate our installer. We are passing some password values as command line arguments. In Inno Setup all command line arguments are logging into installation log automatically with "Setup command line:" entry. Is any way to suppress the "Setup Command Line" logging into log.
No. You can disable only the overall logging. Here's the excerpt from the relevant part of the source code:
...
Log('Setup version: ' + SetupTitle + ' version ' + SetupVersion);
Log('Original Setup EXE: ' + SetupLdrOriginalFilename);
Log('Setup command line: ' + GetCmdTail);
LogWindowsVersion;
...
As you can see there is no condition before the Log procedure call for the command line tail as well as the Log procedure itself does not contain a way to filter out certain log messages. So, at this time the only way to prevent this potential security issue is disabling the overall logging .
Since this might be a security issue, I would suggest you to file a feature request report.
As an alternative, you could ask the user to provide an INI file via the command line instead:
config.ini
[credentials]
username=foo
password=bar
command line invocation
setup.exe /CONFIGFILE=config.ini
PascalScript
Note that some of this is untested, please use it at your own risk.
function CommandLineSwitchIsPresent(Param : String): Boolean;
var
i: Integer;
begin
Result := False;
if not StartsWith('/', Param) then
begin
Param := '/' + Param;
end;
for i:= 0 to ParamCount do
begin
if (CompareText(Param, ParamStr(i)) = 0)
or StartsWith(Param + '=', ParamStr(i)) then
begin
Result := True;
break;
end;
end;
end;
function GetCommandlineParam(Param: String; var OutStr: String): Boolean;
var
ParamNameAndValue: String;
i: Integer;
j: Integer;
begin
Result := False;
Param := Param + '=';
if not StartsWith('/', Param) then
begin
Param := '/' + Param;
end;
for i := 0 to ParamCount do
begin
ParamNameAndValue := ParamStr(i);
if StartsWith(AnsiUppercase(Param), AnsiUppercase(ParamNameAndValue)) then
begin
for j := 0 to Length(ParamNameAndValue) do
begin
if j > Length(Param) then
begin
OutStr := OutStr + ParamNameAndValue[j];
end;
end;
Result := True;
break;
end;
end;
end;
function GetConfig(Section: String; Key: String; ConfigFile: String): String;
begin
if IniKeyExists('credentials', Key, ConfigFile) then
begin
Result := GetIniString('credentials', Key, '', ConfigFile);
end
else begin
RaiseException(
Format(
'%s key not found in [%s] section in %s', [Key, Section, ConfigFile]
);
end;
end;
var
_gConfigFile: String;
procedure DoStuffWithPassword();
var
Username: String;
Password: String;
ShortPath: String;
ExpandedPath: String;
begin
if not GetCommandlineParam('CONFIGFILE', ShortPath) then
begin
RaiseException('CONFIGFILE parameter is required!');
end;
// Go from relative path to absolute
ExpandedPath := ExpandFileName(ShortPath);
if FileExists(ExpandedPath) then
begin
_gConfigFile := ExpandedPath;
end
else begin
RaiseException('CONFIGFILE file ' + ShortPath + ' not found!');
end;
Username := GetConfig('credentials', 'username', _gConfigFile);
Password := GetConfig('credentials', 'password', _gConfigFile);
end;
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?
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;