Inno setup: check for new updates - inno-setup

After reading this post How to read a text file from the Internet resource?, I've adapted the code to what I need but I have some problems.
What I want to do is that when I run the setup, it checks for new updates. 1) If there isn't a new update, don't show any message. 2) And if there is a new update, show a message asking whether you want to download it or not.
This is my code:
procedure InitializeWizard;
var
DxLastVersion: string;
DxSetupVersion: String;
begin
if DownloadFile('http://dex.wotanksmods.com/latestver.txt', DxLastVersion) then
MsgBox(DxLastVersion, mbInformation, MB_YESNO)
else
MsgBox(DxLastVersion, mbError, MB_OK)
end;
Thanks so much in advanced.

Since you've decided to use a common version string pattern, you'll need a function which will parse and compare a version string of your setup and the one downloaded from your site. And because there is no such function built-in in Inno Setup, you'll need to have your own one.
I've seen a few functions for comparing version strings, like e.g. the one used in this script, but I've decided to write my own. It can detect an invalid version string, and treats the missing version chunks as to be 0, which causes comparison of version strings like follows to be equal:
1.2.3
1.2.3.0.0.0
The following script might do what you want (the setup version is defined by the AppVersion directive):
[Setup]
AppName=My Program
AppVersion=1.2.3
DefaultDirName={pf}\My Program
[Code]
const
SetupURL = 'http://dex.wotanksmods.com/setup.exe';
VersionURL = 'http://dex.wotanksmods.com/latestver.txt';
type
TIntegerArray = array of Integer;
TCompareResult = (
crLesser,
crEquals,
crGreater
);
function Max(A, B: Integer): Integer;
begin
if A > B then Result := A else Result := B;
end;
function CompareValue(A, B: Integer): TCompareResult;
begin
if A = B then
Result := crEquals
else
if A < B then
Result := crLesser
else
Result := crGreater;
end;
function AddVersionChunk(const S: string; var A: TIntegerArray): Integer;
var
Chunk: Integer;
begin
Chunk := StrToIntDef(S, -1);
if Chunk <> -1 then
begin
Result := GetArrayLength(A) + 1;
SetArrayLength(A, Result);
A[Result - 1] := Chunk;
end
else
RaiseException('Invalid format of version string');
end;
function ParseVersionStr(const S: string; var A: TIntegerArray): Integer;
var
I: Integer;
Count: Integer;
Index: Integer;
begin
Count := 0;
Index := 1;
for I := 1 to Length(S) do
begin
case S[I] of
'.':
begin
AddVersionChunk(Copy(S, Index, Count), A);
Count := 0;
Index := I + 1;
end;
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
begin
Count := Count + 1;
end;
else
RaiseException('Invalid char in version string');
end;
end;
Result := AddVersionChunk(Copy(S, Index, Count), A);
end;
function GetVersionValue(const A: TIntegerArray; Index,
Length: Integer): Integer;
begin
Result := 0;
if (Index >= 0) and (Index < Length) then
Result := A[Index];
end;
function CompareVersionStr(const A, B: string): TCompareResult;
var
I: Integer;
VerLenA, VerLenB: Integer;
VerIntA, VerIntB: TIntegerArray;
begin
Result := crEquals;
VerLenA := ParseVersionStr(A, VerIntA);
VerLenB := ParseVersionStr(B, VerIntB);
for I := 0 to Max(VerLenA, VerLenB) - 1 do
begin
Result := CompareValue(GetVersionValue(VerIntA, I, VerLenA),
GetVersionValue(VerIntB, I, VerLenB));
if Result <> crEquals then
Exit;
end;
end;
function DownloadFile(const URL: string; var Response: string): Boolean;
var
WinHttpRequest: Variant;
begin
Result := True;
try
WinHttpRequest := CreateOleObject('WinHttp.WinHttpRequest.5.1');
WinHttpRequest.Open('GET', URL, False);
WinHttpRequest.Send;
Response := WinHttpRequest.ResponseText;
except
Result := False;
Response := GetExceptionMessage;
end;
end;
function InitializeSetup: Boolean;
var
ErrorCode: Integer;
SetupVersion: string;
LatestVersion: string;
begin
Result := True;
if DownloadFile(VersionURL, LatestVersion) then
begin
SetupVersion := '{#SetupSetting('AppVersion')}';
if CompareVersionStr(LatestVersion, SetupVersion) = crGreater then
begin
if MsgBox('There is a newer version of this setup available. Do ' +
'you want to visit the site ?', mbConfirmation, MB_YESNO) = IDYES then
begin
Result := not ShellExec('', SetupURL, '', '', SW_SHOW, ewNoWait,
ErrorCode);
end;
end;
end;
end;

Related

Find JSON file with specific name pattern and contents in Inno Setup

I have a working script, where the installer finds the specific file inside specific path but I need to slightly change it. I noticed that when I reinstall the application, the name of the file which I want to get the info from is different every time, so it's not that important as I thought.
There are some things which are not changing although - the path to that variable filename, which I have defined already using the constant and can be used again and also the file extension. So if the path doesn't change, the search process could be a lot more quicker just for that. The file format is JSON and code for it is already applied in the script.
Here is the JSON structure sample:
"ChunkDbs": [],
"CompatibleApps": [],
"DisplayName": "Application Name",
"InstallLocation": "D:\\Program Files (x86)\\ApplicationName",
"InstallTags": [],
"InstallComponents": [],
The only solution for this is searching all the files in specific path with specific extension. There are some variables I need to use here, the first is already mentioned in the code as InstallLocation which is the proper install path, the next variable which I need to define is DisplayName, which contains the application name and probably another one that define the file extension.
So I need to find the right file, containing the specific string inside DisplayName parameter, compare if it's the same as in the defined one and then read the installation path from InstallLocation parameter.
Here is the code I have so far:
[Setup]
DefaultDirName={code:GetInstallLocation}
[Code]
#include "JsonParser.pas"
function ParseJsonAndLogErrors(
var JsonParser: TJsonParser; const Source: WideString): Boolean;
var
I: Integer;
begin
ParseJson(JsonParser, Source);
Result := (Length(JsonParser.Output.Errors) = 0);
if not Result then
begin
Log('Error parsing JSON');
for I := 0 to Length(JsonParser.Output.Errors) - 1 do
begin
Log(JsonParser.Output.Errors[I]);
end;
end;
end;
function GetJsonRoot(Output: TJsonParserOutput): TJsonObject;
begin
Result := Output.Objects[0];
end;
function FindJsonValue(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Value: TJsonValue): Boolean;
var
I: Integer;
begin
for I := 0 to Length(Parent) - 1 do
begin
if Parent[I].Key = Key then
begin
Value := Parent[I].Value;
Result := True;
Exit;
end;
end;
Result := False;
end;
function FindJsonString(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Str: TJsonString): Boolean;
var
JsonValue: TJsonValue;
begin
Result :=
FindJsonValue(Output, Parent, Key, JsonValue) and
(JsonValue.Kind = JVKString);
if Result then
begin
Str := Output.Strings[JsonValue.Index];
end;
end;
function MultiByteToWideChar(
CodePage: UINT; dwFlags: DWORD; const lpMultiByteStr: AnsiString;
cchMultiByte: Integer; lpWideCharStr: string; cchWideChar: Integer): Integer;
external 'MultiByteToWideChar#kernel32.dll stdcall';
function LoadStringFromFileInCP(
FileName: string; var S: string; CP: Integer): Boolean;
var
Ansi: AnsiString;
Len: Integer;
begin
Result := LoadStringFromFile(FileName, Ansi);
if Result then
begin
Len := MultiByteToWideChar(CP, 0, Ansi, Length(Ansi), S, 0);
SetLength(S, Len);
MultiByteToWideChar(CP, 0, Ansi, Length(Ansi), S, Len);
end;
end;
const
CP_UTF8 = 65001;
var
InstallLocation: string;
<event('InitializeSetup')>
function InitializeSetupParseConfig(): Boolean;
var
Json: string;
ConfigPath: string;
JsonParser: TJsonParser;
JsonRoot: TJsonObject;
S: TJsonString;
begin
Result := True;
ConfigPath := ExpandConstant('{commonappdata}\Data\FEE8D728379C5E.dat');
Log(Format('Reading "%s"', [ConfigPath]));
if not LoadStringFromFileInCP(ConfigPath, Json, CP_UTF8) then
begin
MsgBox(Format('Error reading "%s"', [ConfigPath]), mbError, MB_OK);
Result := True;
end
else
if not ParseJsonAndLogErrors(JsonParser, Json) then
begin
MsgBox(Format('Error parsing "%s"', [ConfigPath]), mbError, MB_OK);
Result := True;
end
else
begin
JsonRoot := GetJsonRoot(JsonParser.Output);
if not FindJsonString(JsonParser.Output, JsonRoot, 'InstallLocation', S) then
begin
MsgBox(Format('Cannot find InstallLocation in "%s"', [ConfigPath]),
mbError, MB_OK);
Result := False;
end
else
begin
InstallLocation := S;
Log(Format('Found InstallLocation = "%s"', [InstallLocation]));
end;
ClearJsonParser(JsonParser);
end;
end;
function GetInstallLocation(Param: string): string;
begin
Result := InstallLocation;
end;
Thank you for help
I've written my code prototype to show how I see the code to make what I want (the most intuitive / clearest way):
const
MyDisplayName = 'MyReqAppName';
MyPath = 'C:\some\path\*.dat';
var
RealDisplayName: string;
InstallLocation: string;
function FindParameters();
begin
RealDisplayName := 'DisplayName';
InstallLocation := 'InstallLocation';
Find(RealDisplayName in MyPath);
if (RealDisplayName = MyDisplayName) then
Find(InstallLocation);
else
repeat
Find(RealDisplayName in MyPath);
until(RealDisplayName = MyDisplayName);
end;
To find a files with a specific extension, use FindFirst and FindNext functions.
For each matching file you find, inspect its contents using the code you already have.
You will need to adapt the code further, as it's not clear to me what you want to do in case of various kinds of errors.
var
Path: string;
FindRec: TFindRec;
Json: string;
ConfigPath: string;
JsonParser: TJsonParser;
JsonRoot: TJsonObject;
S: TJsonString;
DisplayName: string;
begin
Path := 'C:\some\path';
if FindFirst(Path + '\*.dat', FindRec) then
begin
repeat
Log('Found: ' + FindRec.Name);
ConfigPath := Path + '\' + FindRec.Name;
Log(Format('Reading "%s"', [ConfigPath]));
if not LoadStringFromFileInCP(ConfigPath, Json, CP_UTF8) then
begin
Log(Format('Error reading "%s"', [ConfigPath]));
end
else
if not ParseJsonAndLogErrors(JsonParser, Json) then
begin
Log(Format('Error parsing "%s"', [ConfigPath]));
end
else
begin
JsonRoot := GetJsonRoot(JsonParser.Output);
if not FindJsonString(JsonParser.Output, JsonRoot, 'DisplayName', S) then
begin
Log(Format('Cannot find DisplayName in "%s"', [ConfigPath]));
end
else
if DisplayName <> MyDisplayName then
begin
Log(Format('DisplayName is "%s", not what we want', [DisplayName]));
end
else
begin
Log(Format('DisplayName is "%s", what we want', [DisplayName]));
if not FindJsonString(
JsonParser.Output, JsonRoot, 'InstallLocation', S) then
begin
Log(Format('Cannot find InstallLocation in "%s"', [ConfigPath]));
end
else
begin
InstallLocation := S;
Log(Format('Found InstallLocation = "%s"', [InstallLocation]));
end;
end;
ClearJsonParser(JsonParser);
end;
until not FindNext(FindRec);
FindClose(FindRec);
end;
end;

How to send records containing strings between applications

So, I have a class that uses WM_COPYDATA to allow applications to communicate.
type
TMyRec = record
Name: string[255]; // I want just string
Age: integer;
Birthday: TDateTime;
end;
function TAppCommunication.SendRecord(const ARecordType: ShortString; const ARecordToSend: Pointer; ARecordSize: Integer): Boolean;
var
_Stream: TMemoryStream;
begin
_Stream := TMemoryStream.Create;
try
_Stream.WriteBuffer(ARecordType, 1 + Length(ARecordType));
_Stream.WriteBuffer(ARecordToSend^, ARecordSize);
_Stream.Position := 0;
Result := SendStreamData(_Stream, TCopyDataType.cdtRecord);
finally
FreeAndNil(_Stream);
end;
end;
function TAppCommunication.SendStreamData(const AStream: TMemoryStream;
const ADataType: TCopyDataType): Boolean;
var
_CopyDataStruct: TCopyDataStruct;
begin
Result := False;
if AStream.Size = 0 then
Exit;
_CopyDataStruct.dwData := integer(ADataType);
_CopyDataStruct.cbData := AStream.Size;
_CopyDataStruct.lpData := AStream.Memory;
Result := SendData(_CopyDataStruct);
end;
function TAppCommunication.SendData(const ADataToSend: TCopyDataStruct)
: Boolean;
var
_SendResponse: integer;
_ReceiverHandle: THandle;
begin
Result := False;
_ReceiverHandle := GetRemoteReceiverHandle;
if (_ReceiverHandle = 0) then
Exit;
_SendResponse := SendMessage(_ReceiverHandle, WM_COPYDATA,
WPARAM(FLocalReceiverForm.Handle), LPARAM(#ADataToSend));
Result := _SendResponse <> 0;
end;
Sender application:
procedure TSenderMainForm.BitBtn1Click(Sender: TObject);
var
_AppCommunication: TAppCommunication;
_ms: TMemoryStream;
_Rec: TMyRec;
_Record: TAttrData;
begin
_AppCommunication := TAppCommunication.Create('LocalReceiverName', OnAppMessageReceived);
_ms := TMemoryStream.Create;
try
_AppCommunication.SetRemoteReceiverName('LocalReceiverNameServer');
_AppCommunication.SendString('ąčęėįšųūž123');
_AppCommunication.SendInteger(998);
_AppCommunication.SendDouble(0.95);
_Rec.Name := 'Edijs';
_Rec.Age := 29;
_Rec.Birthday := EncodeDate(1988, 10, 06);
_Record.Len := 1988;
_AppCommunication.SendRecord(TTypeInfo(System.TypeInfo(TMyRec)^).Name, #_Rec, SizeOf(_Rec));
finally
FreeAndNil(_ms);
FreeAndNil(_AppCommunication);
end;
end;
Receiver app:
procedure TReceiverMainForm.OnAppMessageReceived(const ASender
: TPair<HWND, string>; const AReceivedData: TCopyDataStruct;
var AResult: integer);
var
_MyRec: TMyRec;
_RecType: ShortString;
_RecData: Pointer;
begin
...
else
begin
if (AReceivedData.dwData) = Ord(TCopyDataType.cdtRecord) then
begin
_RecType := PShortString(AReceivedData.lpData)^;
_RecData := PByte(AReceivedData.lpData)+1+Length(_RecType);
if (_RecType = TTypeInfo(System.TypeInfo(TMyRec)^).Name) then
begin
_MyRec := TMyRec(_RecData^);
ShowMessage(_MyRec.Name + ', Age: ' + IntToStr(_MyRec.Age) + ', birthday: ' +
DateToStr(_MyRec.Birthday));
end;
end;
AResult := -1;
end;
end;
The problem is that crash occur when I change Name: string[255]; to Name: string; in TMyRec. How do I overcome this? I do not want to edit all my records to change string to something else and I want to have one function to send all kind of records (as far as my idea goes none of them will contain objects).
EDITED:
Used answer provided by Remy and made some tweaks so I would by able to send any kind of record using only one SendRecord function:
function TAppCommunication.SendRecord(const ARecordToSend, ARecordTypInfo: Pointer): Boolean;
var
_Stream: TMemoryStream;
_RType: TRTTIType;
_RFields: TArray<TRttiField>;
i: Integer;
begin
_Stream := TMemoryStream.Create;
try
_RType := TRTTIContext.Create.GetType(ARecordTypInfo);
_Stream.WriteString(_RType.ToString);
_RFields := _RType.GetFields;
for i := 0 to High(_RFields) do
begin
if _RFields[i].FieldType.TypeKind = TTypeKind.tkUString then
_Stream.WriteString(_RFields[i].GetValue(ARecordToSend).ToString)
else if _RFields[i].FieldType.TypeKind = TTypeKind.tkInteger then
_Stream.WriteInteger(_RFields[i].GetValue(ARecordToSend).AsType<integer>)
else if _RFields[i].FieldType.TypeKind = TTypeKind.tkFloat then
_Stream.WriteDouble(_RFields[i].GetValue(ARecordToSend).AsType<Double>)
end;
_Stream.Position := 0;
Result := SendStreamData(_Stream, TCopyDataType.cdtRecord);
finally
FreeAndNil(_Stream);
end;
end;
Sender:
_AppCommunication.SendRecord(#_Rec, System.TypeInfo(TMyRec));
A ShortString has a fixed size of 256 bytes max (1 byte length + up to 255 AnsiChars), so it is easy to embed in records and send as-is.
A String, on the other hand, is a pointer to dynamically allocated memory for an array of Chars. So, it requires a little more work to serialize back and forth.
To do what you are asking, you can't simply replace ShortString with String without also changing everything else in between to account for that difference.
You already have the basic framework to send variable-length strings (send the length before sending the data), so you can expand on that to handle string values, eg:
type
TMyRec = record
Name: string;
Age: integer;
Birthday: TDateTime;
end;
TStreamHelper = class helper for TStream
public
function ReadInteger: Integer;
function ReadDouble: Double;
function ReadString: String;
...
procedure WriteInteger(Value: Integer);
procedure WriteDouble(Strm: Value: Double);
procedure WriteString(const Value: String);
end;
function TStreamHelper.ReadInteger: Integer;
begin
Self.ReadBuffer(Result, SizeOf(Integer));
end;
function TStreamHelper.ReadDouble: Double;
begin
Self.ReadBuffer(Result, SizeOf(Double));
end;
function TStreamHelper.ReadString: String;
var
_Bytes: TBytes;
_Len: Integer;
begin
_Len := ReadInteger;
SetLength(_Bytes, _Len);
Self.ReadBuffer(PByte(_Bytes)^, _Len);
Result := TEncoding.UTF8.GetString(_Bytes);
end;
...
procedure TStreamHelper.WriteInteger(Value: Integer);
begin
Self.WriteBuffer(Value, SizeOf(Value));
end;
procedure TStreamHelper.WriteDouble(Value: Double);
begin
Self.WriteBuffer(Value, SizeOf(Value));
end;
procedure TStreamHelper.WriteString(const Value: String);
var
_Bytes: TBytes;
_Len: Integer;
begin
_Bytes := TEncoding.UTF8.GetBytes(Value);
_Len := Length(_Bytes);
WriteInteger(_Len);
Self.WriteBuffer(PByte(_Bytes)^, _Len);
end;
function TAppCommunication.SendRecord(const ARecord: TMyRec): Boolean;
var
_Stream: TMemoryStream;
begin
_Stream := TMemoryStream.Create;
try
_Stream.WriteString('TMyRec');
_Stream.WriteString(ARecord.Name);
_Stream.WriteInteger(ARecord.Age);
_Stream.WriteDouble(ARecord.Birthday);
_Stream.Position := 0;
Result := SendStreamData(_Stream, TCopyDataType.cdtRecord);
finally
FreeAndNil(_Stream);
end;
end;
// more overloads of SendRecord()
// for other kinds of records as needed...
procedure TSenderMainForm.BitBtn1Click(Sender: TObject);
var
...
_Rec: TMyRec;
begin
...
_Rec.Name := 'Edijs';
_Rec.Age := 29;
_Rec.Birthday := EncodeDate(1988, 10, 06);
_AppCommunication.SendRecord(_Rec);
...
end;
type
TReadOnlyMemoryStream = class(TCustomMemoryStream)
public
constructor Create(APtr: Pointer; ASize: NativeInt);
function Write(const Buffer; Count: Longint): Longint; override;
end;
constructor TReadOnlyMemoryStream.Create(APtr: Pointer; ASize: NativeInt);
begin
inherited Create;
SetPointer(APtr, ASize);
end;
function TReadOnlyMemoryStream.Write(const Buffer; Count: Longint): Longint;
begin
Result := 0;
end;
procedure TReceiverMainForm.OnAppMessageReceived(const ASender : TPair<HWND, string>; const AReceivedData: TCopyDataStruct; var AResult: integer);
var
...
_Stream: TReadOnlyMemoryStream;
_MyRec: TMyRec;
_RecType: String;
begin
...
else
begin
if (AReceivedData.dwData = Ord(TCopyDataType.cdtRecord)) then
begin
_Stream := TReadOnlyMemoryStream(AReceivedData.lpData, AReceivedData.cbData);
try
_RecType := _Stream.ReadString;
if (_RecType = 'TMyRec') then
begin
_MyRec.Name := _Stream.ReadString;
_MyRec.Age := _Stream.ReadInteger;
_MyRec.Birthday := _Stream.ReadDouble;
ShowMessage(_MyRec.Name + ', Age: ' + IntToStr(_MyRec.Age) + ', birthday: ' + DateToStr(_MyRec.Birthday));
end;
finally
_Stream.Free;
end;
end;
AResult := -1;
end;
end;

Inno Setup - How to add multiple arc files to decompress?

I am using this code: Inno Setup - How to add cancel button to decompressing page? (answer of Martin Prikryl) to decompress an arc file with Inno Setup.
I want to have the possibility of decompress more than one arc file to install files from components selection (for example). But still show on overall progress bar for all extractions. whole Is this possible?
This is modification of my answer to Inno Setup - How to add cancel button to decompressing page?
Prerequisities are the same, refer to the other answer.
In the ExtractArc, call AddArchive for each archive you want to extract.
[Files]
Source: unarc.dll; Flags: dontcopy
[Code]
const
ArcCancelCode = -10;
function FreeArcExtract(
Callback: LongWord;
Cmd1, Cmd2, Cmd3, Cmd4, Cmd5, Cmd6, Cmd7, Cmd8, Cmd9, Cmd10: PAnsiChar): Integer;
external 'FreeArcExtract#files:unarc.dll cdecl';
const
CP_UTF8 = 65001;
function WideCharToMultiByte(CodePage: UINT; dwFlags: DWORD;
lpWideCharStr: string; cchWideChar: Integer; lpMultiByteStr: AnsiString;
cchMultiByte: Integer; lpDefaultCharFake: Integer;
lpUsedDefaultCharFake: Integer): Integer;
external 'WideCharToMultiByte#kernel32.dll stdcall';
function GetStringAsUtf8(S: string): AnsiString;
var
Len: Integer;
begin
Len := WideCharToMultiByte(CP_UTF8, 0, S, Length(S), Result, 0, 0, 0);
SetLength(Result, Len);
WideCharToMultiByte(CP_UTF8, 0, S, Length(S), Result, Len, 0, 0);
end;
var
ArcTotalSize: Integer;
ArcTotalExtracted: Integer;
ArcExtracted: Integer;
ArcCancel: Boolean;
ArcProgressPage: TOutputProgressWizardPage;
function FreeArcCallback(
AWhat: PAnsiChar; Int1, Int2: Integer; Str: PAnsiChar): Integer;
var
What: string;
begin
What := AWhat;
if What = 'origsize' then
begin
Log(Format('Adding archive with files with total size %d MB', [Int1]));
ArcTotalSize := ArcTotalSize + Int1;
end
else
if What = 'write' then
begin
if ArcTotalSize > 0 then
begin
ArcProgressPage.SetProgress(ArcTotalExtracted + Int1, ArcTotalSize);
end;
ArcExtracted := Int1;
end
else
begin
// Just to pump message queue more often (particularly for 'read' callbacks),
// to get more smooth progress bar
if (ArcExtracted > 0) and (ArcTotalSize > 0) then
begin
ArcProgressPage.SetProgress(ArcTotalExtracted + ArcExtracted, ArcTotalSize);
end;
end;
if ArcCancel then Result := ArcCancelCode
else Result := 0;
end;
procedure FreeArcCmd(
Cmd1, Cmd2, Cmd3, Cmd4, Cmd5, Cmd6, Cmd7, Cmd8, Cmd9, Cmd10: string);
var
ArcResult: Integer;
begin
ArcCancel := False;
ArcResult :=
FreeArcExtract(
CreateCallback(#FreeArcCallback),
GetStringAsUtf8(Cmd1), GetStringAsUtf8(Cmd2), GetStringAsUtf8(Cmd3),
GetStringAsUtf8(Cmd4), GetStringAsUtf8(Cmd5), GetStringAsUtf8(Cmd6),
GetStringAsUtf8(Cmd7), GetStringAsUtf8(Cmd8), GetStringAsUtf8(Cmd9),
GetStringAsUtf8(Cmd10));
if ArcCancel then
begin
RaiseException('Extraction cancelled');
end
else
if ArcResult <> 0 then
begin
RaiseException(Format('Extraction failed with code %d', [ArcResult]));
end;
end;
var
ArcArchives: array of string;
procedure AddArchive(ArchivePath: string);
begin
SetArrayLength(ArcArchives, GetArrayLength(ArcArchives) + 1);
ArcArchives[GetArrayLength(ArcArchives) - 1] := ArchivePath;
FreeArcCmd('l', '--', ArchivePath, '', '', '', '', '', '', '');
end;
procedure UnPackArchives(DestPath: string);
var
I: Integer;
ArchivePath: string;
begin
Log(Format('Total size of files to be extracted is %d MB', [ArcTotalSize]));
ArcTotalExtracted := 0;
for I := 0 to GetArrayLength(ArcArchives) - 1 do
begin
ArcExtracted := 0;
ArchivePath := ArcArchives[I];
Log(Format('Extracting %s', [ArchivePath]));
FreeArcCmd('x', '-o+', '-dp' + DestPath, '-w' + DestPath, '--', ArchivePath,
'', '', '', '');
ArcTotalExtracted := ArcTotalExtracted + ArcExtracted;
end;
end;
procedure UnpackCancelButtonClick(Sender: TObject);
begin
ArcCancel := True;
end;
procedure ExtractArc;
var
PrevCancelButtonClick: TNotifyEvent;
begin
ArcProgressPage :=
CreateOutputProgressPage('Decompression', 'Decompressing archive...');
ArcProgressPage.SetProgress(0, 100);
ArcProgressPage.Show;
try
WizardForm.CancelButton.Visible := True;
WizardForm.CancelButton.Enabled := True;
PrevCancelButtonClick := WizardForm.CancelButton.OnClick;
WizardForm.CancelButton.OnClick := #UnpackCancelButtonClick;
try
AddArchive(ExpandConstant('{src}\test1.arc'));
AddArchive(ExpandConstant('{src}\test2.arc'));
Log('Arc extraction starting');
UnPackArchives(ExpandConstant('{app}'));
except
MsgBox(GetExceptionMessage(), mbError, MB_OK);
end;
finally
Log('Arc extraction done');
ArcProgressPage.Hide;
WizardForm.CancelButton.OnClick := PrevCancelButtonClick;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
ExtractArc;
end;
end;
For CreateCallback function, you need Inno Setup 6. If you are stuck with Inno Setup 5, you can use WrapCallback function from InnoTools InnoCallback library.

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;

In Delphi can a string be converted to a set

For instance
Font.Style = StringToSet('[fsBold, fsUnderline]');
of course there would need to be some typeinfo stuff in there, but you get the idea. I'm using Delphi 2007.
check this code, is not exactly the same syntax which you propose , but works setting the value of a set from a string.
uses
TypInfo;
procedure StringToSet(Const Values,AProperty:string;Instance: TObject);
begin
if Assigned(GetPropInfo(Instance.ClassInfo, AProperty)) then
SetSetProp(Instance,AProperty,Values);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
StringToSet('[fsBold, fsUnderline, fsStrikeOut]','Style',Label1.Font);
end;
Also see my old post: SetToString, StringToSet for a solution (Delphi 2007, IIRC) without a need for published property RTTI:
uses
SysUtils, TypInfo;
function GetOrdValue(Info: PTypeInfo; const SetParam): Integer;
begin
Result := 0;
case GetTypeData(Info)^.OrdType of
otSByte, otUByte:
Result := Byte(SetParam);
otSWord, otUWord:
Result := Word(SetParam);
otSLong, otULong:
Result := Integer(SetParam);
end;
end;
procedure SetOrdValue(Info: PTypeInfo; var SetParam; Value: Integer);
begin
case GetTypeData(Info)^.OrdType of
otSByte, otUByte:
Byte(SetParam) := Value;
otSWord, otUWord:
Word(SetParam) := Value;
otSLong, otULong:
Integer(SetParam) := Value;
end;
end;
function SetToString(Info: PTypeInfo; const SetParam; Brackets: Boolean): AnsiString;
var
S: TIntegerSet;
TypeInfo: PTypeInfo;
I: Integer;
begin
Result := '';
Integer(S) := GetOrdValue(Info, SetParam);
TypeInfo := GetTypeData(Info)^.CompType^;
for I := 0 to SizeOf(Integer) * 8 - 1 do
if I in S then
begin
if Result <> '' then
Result := Result + ',';
Result := Result + GetEnumName(TypeInfo, I);
end;
if Brackets then
Result := '[' + Result + ']';
end;
procedure StringToSet(Info: PTypeInfo; var SetParam; const Value: AnsiString);
var
P: PAnsiChar;
EnumInfo: PTypeInfo;
EnumName: AnsiString;
EnumValue, SetValue: Longint;
function NextWord(var P: PAnsiChar): AnsiString;
var
I: Integer;
begin
I := 0;
// scan til whitespace
while not (P[I] in [',', ' ', #0,']']) do
Inc(I);
SetString(Result, P, I);
// skip whitespace
while P[I] in [',', ' ',']'] do
Inc(I);
Inc(P, I);
end;
begin
SetOrdValue(Info, SetParam, 0);
if Value = '' then
Exit;
SetValue := 0;
P := PAnsiChar(Value);
// skip leading bracket and whitespace
while P^ in ['[',' '] do
Inc(P);
EnumInfo := GetTypeData(Info)^.CompType^;
EnumName := NextWord(P);
while EnumName <> '' do
begin
EnumValue := GetEnumValue(EnumInfo, EnumName);
if EnumValue < 0 then
begin
SetOrdValue(Info, SetParam, 0);
Exit;
end;
Include(TIntegerSet(SetValue), EnumValue);
EnumName := NextWord(P);
end;
SetOrdValue(Info, SetParam, SetValue);
end;
Example usage:
var
A: TAlignSet;
S: AnsiString;
begin
// set to string
A := [alClient, alLeft, alTop];
S := SetToString(TypeInfo(TAlignSet), A, True);
ShowMessage(Format('%s ($%x)', [S, Byte(A)]));
// string to set
S := '[alNone, alRight, alCustom]';
StringToSet(TypeInfo(TAlignSet), A, S);
ShowMessage(Format('%s ($%x)', [SetToString(TypeInfo(TAlignSet), A, True), Byte(A)]));
end;
You have right function name already - StringToSet. However, usage is tricky:
procedure TForm1.FormClick(Sender: TObject);
type PFontStyles = ^TFontStyles; // typecast helper declaration
var Styles: Integer; // receives set bitmap after parsing
{$IF SizeOf(TFontStyles) > SizeOf(Integer)}
{$MESSAGE FATAL 'Panic. RTTI functions will work with register-sized sets only'}
{$IFEND}
begin
Styles := StringToSet( // don't forget to use TypInfo (3)
PTypeInfo(TypeInfo(TFontStyles)), // this kludge is required for overload (1)
'[fsBold, fsUnderline]'
);
Font.Style := PFontStyles(#Styles)^; // hack to bypass strict typecast rules (2)
Update(); // let form select amended font into Canvas
Canvas.TextOut(0, 0, 'ME BOLD! ME UNDERLINED!');
end;
(1) because initially borland limited this function family to PropInfo pointers and TypeInfo() intrinsic returns untyped pointer, hence the typecast
(2) typecasting requires types to be of same size, hence the referencing and dereferencing to different type (TFontStyles is a Byte)
Nitpicker special: (3) This snippet works out of the box in D2010+. Earlier versions has required dependency missing - namely StringToSet(TypeInfo: PTypeInfo; ... overload (see docwiki link above). This problem is solvable by copypasting (yeah, but TTypeInfo is lower-level than TPropInfo) original function and doing 2 (two) minor edits. By obvious reasons i'm not going to publish copyrighted code, but here is the relevant diff:
1c1,2
< function StringToSet(PropInfo: PPropInfo; const Value: string): Integer;
---
> {$IF RTLVersion < 21.0}
> function StringToSet(TypeInfo: PTypeInfo; const Value: string): Integer; overload;
37c38
< EnumInfo := GetTypeData(PropInfo^.PropType^)^.CompType^;
---
> EnumInfo := GetTypeData(TypeInfo)^.CompType^;
47a49
> {$IFEND}

Resources