Exit from Inno Setup installation from [Code] - inno-setup

Is it possible to exit the installation from a function in the [Code] section of an installer created with Inno Setup?
I'm not interested in setting the exit code, what I want to do is perform a custom check for a requirement, and exit the installation if that requirement was not previously installed.

To prevent the installer from running, when prerequisites test fails, just return False from the InitializeSetup. This will exit the installer even before the wizard shows.
function InitializeSetup(): Boolean;
begin
Result := True;
if not PrerequisitesTest then
begin
SuppressibleMsgBox('Prerequisites test failed', mbError, MB_OK, IDOK);
Result := False;
end;
end;
If you need to test prerequisites right before the installation starts only (i.e. the InitializeSetup is too early), you can call the Abort function from the CurStepChanged(ssInstall):
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
begin
if not PrerequisitesTest then
begin
SuppressibleMsgBox('Prerequisites test failed', mbError, MB_OK, IDOK);
Abort;
end;
end;
end;
Though for this scenario, consider using the PrepareToInstall event function mechanism, instead of exiting the setup.
function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
Result := '';
if not PrerequisitesTest then
begin
Result := 'Prerequisites test failed';
end;
end;
If you need to force terminate the installer any other time, use the ExitProcess WinAPI call:
procedure ExitProcess(uExitCode: Integer);
external 'ExitProcess#kernel32.dll stdcall';
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if CurPageID = wpReady then
begin
if not PrerequisitesTest then
begin
SuppressibleMsgBox('Prerequisites test failed', mbError, MB_OK, IDOK);
ExitProcess(1);
end;
end;
Result := True;
end;
Though this is rather unsafe exit, so use it only as the last resort approach. If you have any external DLL loaded, you might need to unload it first, to avoid crashes. This also does not cleanup the temporary directory.

You can use Abort() if you are in these events:
InitializeSetup
InitializeWizard
CurStepChanged(ssInstall)
InitializeUninstall
CurUninstallStepChanged(usAppMutexCheck)
CurUninstallStepChanged(usUninstall)

The way I do it is:
procedure ExitProcess(exitCode:integer);
external 'ExitProcess#kernel32.dll stdcall';
And the way of using it is:
[Code]
if .... then begin
ExitProcess(0);
end;

This is a write up of what I fiddled out of my Inno 5.6.1 today and the sources you can find at https://github.com/jrsoftware/issrc [ref1]
A possibly useful catch-all solution to "Exit from [Code]"
TL;DR example:
[Code]
var _ImmediateInnoExit_was_invoked_flag: Boolean; // Inno/Pascal Script initializes all Boolean to False.
procedure ImmediateInnoExit();
var MainFormRef: TForm;
begin
_ImmediateInnoExit_was_invoked_flag := True;
try
MainFormRef := MainForm(); // calls GetMainForm() in Inno pascal code, which will raise an internal exception if the form is not yet initialized.
Log('INFO: ImmediateInnoExit: Calling MainForm.Close()!');
Log('NOTE: If the Event Fn CancelButtonClick is not coded to auto-Confirm, this will display the cancel dialog in the GUI case!');
Log('NOTE: Code will stall inside the Close() function while the Cancel confirmation dialog is displayed.');
MainFormRef.Close(); // this is only effective if the Wizard is visible, but we cann call it even when running siently (as long as the Wizard is initialized)
Log('NOTE: MainForm.Close() invoked. (If confirmed, setup will exit.)');
except
Log('INFO: ImmediateInnoExit did not resolve MainForm -> assuming we were call in an InitializeSetup() context before the Main form has been created!');
end;
Log('INFO: ImmediateInnoExit: Calling Abort() -> EAbort!');
Log('NOTE: Will exit the current scope.');
Log('NOTE: In GUI mode, it will just jump up to the Delphi event loop (and be ignored there). (But the WizardForm.Close() call should lead to exit!)');
Log('NOTE: In Silent Mode, it will be caught and exit the setup.');
Abort(); // Raise EAbort
end;
// This is called when the user clicks the cancel button or the [x] Close button
// the close/cancel can be invoked from code via WizardForm.Close!
procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean);
begin
Log(Format('IN: CancelButtonClick(%d <- Cancel=[%d], Confirm=[%d])', [CurPageID, Cancel, Confirm]));
Confirm := not _ImmediateInnoExit_was_invoked_flag; // if Confirm==False we don't get the dialog prompt.
Log(Format('IN: CancelButtonClick(%d -> [%d], [%d])', [CurPageID, Cancel, Confirm]));
end;
And now to what the point of the above code is:
Anatomy of Inno Setup Exit/Cancel and Abort
Abort
The Inno docs for Abort state:
Description:
Escapes from the current execution path without reporting an error.
Abort raises a special "silent exception" which operates like any
other exception, but does not display an error message to the end
user.
Remarks:
Abort does not cause Setup or Uninstall to exit unless it's called
from one of these event functions (or another function invoked by
them):
InitializeSetup InitializeWizard CurStepChanged(ssInstall)
InitializeUninstall CurUninstallStepChanged(usAppMutexCheck)
CurUninstallStepChanged(usUninstall)
Abort() behaviour explained
The reason the Abort function bevahes in this way is because, internally, Inno raises an EAbort exception, and that exception is treated specially by the Delphi UI loop. Only in the functions listed, the Inno devs have either added special treatment for EAbort (like in the case of CurStepChanged(ssInstall)[ref2]), --
-- or the function os not called via the UI loop, like in the case of InitializeSetup, which is called from the main program in Setup.dpr, and any direct EAbortis handled there specifically in the exceptblock there.
In all other Inno event function (e.g. NextButtonClick etc.) the EAbortexception will reach the main program/UI loop and be ignored there.
Which leads us nicely to:
Abort() behaviour, when running /SILENT (or /VERSILENT)
When Inno runs silently, it does not display the wizard form UI. The "Wizard" / Inno's progress is then not driven by the UI loop, but by WizardForm.ClickThroughPages, which is invoked under same toplevel try/except block as e.g. InitializeSetup. [ref3]
Because of this, if Inno is being called silently, Abort() will exit setup from every most [Code] functions, and the list given in the docs for Abort becomes moot if setup is being run silently.
Cancel
To cancel the setup, the user can click the [Cancel] button or the [X] close button of the Setup Wizard.
In this case, Inno will invoke the callback function CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean) (if defined) and terminates setup, possible with an escape hatch dialog:
Called when the user clicks the Cancel button or clicks the window's
Close button. The Cancel parameter specifies whether normal cancel
processing should occur; it defaults to True. The Confirm parameter
specifies whether an "Exit Setup?" message box should be displayed;
User [Code] can invoke the Cancel Button mechinism via calling WizardForm.Close(), but this only works if Setup is displaying the Wizard Form, and doesn't work in silent mode.
Cancel Details
WizardForm.Close[ref4], or the click on the actual button, will eventually call TMainForm.FormCloseQuery (in Main.pas), which will call CancelButtonClick callbacks[ref5] and dependeing on Confirm value, either call TerminateApp(); directly or first call the helper function ExitSetupMsgBox()that will display the message box to the user.
Footnotes:
[ref1] : For non-delphi folks: If you search over the sources in you text editor, make sure to include at least .iss .pas and .dpr
[ref2] : ssInstall exception handling is located at issrc\Projects\Main.pas: TMainForm.Install via SetStep(ssInstall, False); and the except block at the end of TMainForm.Install where TerminateApp is called.
[ref3] : See Setup.dpr calling MainForm.InitializeWizard from Main.paswhich calls WizardForm.ClickThroughPages iff not InstallMode = imNormal, i.e. in the silent case.
[ref4] : WizardForm.Close() internally calls MainForm.Close() (see: TWizardForm.FormClose)
[ref5] : There are actually two kinds of cancel button click callbacks defineable in Inno [Code]: The global CancelButtonClickprocedure, and each Wizard page also has a OnCancelButtonClick: TWizardPageCancelEvent that can be set.

Take a look at InitializeSetup and Abort in the InnoSetup help. As Cody said, it is possible. If you're having problems, post what you've done and the problem you're having.

Somewhere in your code section you perform a check. Right?
As result of that check you want to exit the installation.
To perform the exit put the line:
PostMessage (WizardForm.Handle, $0010, 0, 0); { quit setup, $0010=WM_CLOSE }
Hopefully this helps

Related

Correct logging within thread without dialog with Eurekalog

I have a Delphi 10 project using the latest version of EurekaLog. I'm currently using EurekaLog to help me debug problems in my production clients.
I noticed that EurekaLog wasn't registering errors that happened within threads. After I started reading up on it, I found that I need to change from TThread to TThreadEx, and add the following code at the start of my Execute overriden method.
SetEurekaLogStateInThread(ThreadID, true);
Despite this, when an error happens, it does not generate an event in the EL file.
If I add ExceptionManager.StandardEurekaError('TThrdSincArquivos.Execute => ' + ex.Message); on the try..except, it does log. But the stack trace is displayed as if the error occurred on the line where I call StandardEurekaLog(), not on the line where the error actually occurred. This defeats the purpose of the whole thing.
Another problem is that it displays a dialog box, which I don't want, since the error occurred inside a background thread. I just want it logged. I should get a dialog only with errors on the main thread.
How can I achieve theses results within the thread?
Actually log the error with the correct stack.
When on the main thread, display the dialog, but within a thread, just log with no dialog.
EDIT
Below is my EurekaLog Muti-threading configuration
Here is my thread declaration:
unit ThrdSincArquivos;
interface
uses
System.Classes, System.SysUtils, System.Generics.Collections, REST.Client, REST.Types,
System.JSON, Data.DB, Datasnap.DBClient, FireDAC.Comp.Client, FireDAC.Stan.Param, System.SyncObjs, EBase, EExceptionManager, EClasses;
type
TThrdSincArquivos = class(TThreadEx)
private
My thread's Create
constructor TThrdSincArquivos.Create(pPrimeiraExec: boolean; tipoSincParam: TTipoSinc);
begin
inherited Create(true);
NameThreadForDebugging('TThrdSincArquivos');
primeiraExec := pPrimeiraExec;
tipoSinc := tipoSincParam;
executadoThreadSinc := false;
FreeOnTerminate := true
end;
The start of my Execute
procedure TThrdSincArquivos.Execute;
var
contador: Integer;
begin
inherited;
try
and the end of the Execute
except
on ex: Exception do
begin
oLog.GravarLog(ex, 'TThrdSincArquivos.Execute => FIM');
end;
end;
end;
It refuses to log any exception to the Elf file. I tried to add a raise after my own log routine, but it still didn't help. It should log, but it isn't, unless I explicitly call the StandardEurekaError, but I get the stack wrong, and I get the dialog.
When you are using TThread class - it saves thread exception to .FatalException property, which you are supposed to handle in some way. Either from thread event, or from other (caller) thread. EurekaLog does not break this behaviour. E.g. your previosly written code will not change its behaviour when you enable EurekaLog. That way your properly written code would work correctly both with and without EurekaLog.
How your code is currently handling thread exceptions? Are you doing something like ShowMessage or custom logging? This obviosly would not work with EurekaLog, it does not know that you are processing exceptions with ShowMessage or your own custom logging code. You probably want something like Application.ShowException or re-raise in caller thread.
If you can not use default RTL/VCL processing (which is hooked by EurekaLog) for some reason - then you need to tell EurekaLog that you want to handle this particular exception. For example, from docs: you can use (for example) HandleException(E); from EBase unit:
Thread.WaitFor;
if Assigned(Thread.FatalException) then
begin
// Your old code is here
// Do your own thing: show message, log, etc.
// Tell EurekaLog to do its thing:
HandleException(Thread.FatalException);
end;
You would probably want to set exception filter or use events to disable dialogs for thread exceptions, because presumably you have already processed exception yourself (e.g. already showed message).
There is A LOT more ways to handle exception in threads, and EurekaLog's docs illustrate each thread case (like BeginThread, TThread, thread pools, etc.) with several possible options. It is just not reasonable to pack all this information into a single answer.
If, for some reason, you do not have code that processes .FatalException property of TThread - then you can use TThreadEx class and its .AutoHandleException property to handle exceptions automatically when thread exits, as described here:
type
TMyThread = class(TThreadEx)
protected
procedure Execute; override;
end;
procedure TMyThread.Execute;
begin
// ... your code ...
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Thread: TMyThread;
begin
Thread := TMyThread.Create(True, 'My thread');
Thread.AutoHandleException := True; // <- added
Thread.FreeOnTerminate := True;
Thread.Start;
Thread := nil; // never access thread var with FreeOnTerminate after Start
end;
However, be aware that you code will not work properly (e.g. will ignore exceptions) if you decide to disable EurekaLog in the future. Because if you remove EurekaLog from your project - then your project will have no code to handle thread exceptions!
P.S.
I need to change from TThread to TThreadEx, and add the following
code at the start of my Execute overriden method.
SetEurekaLogStateInThread(ThreadID, true);
That is slightly incorrect: you can do either one or another, but not both. And there are other ways to tell EurekaLog that it should hook exceptions in this thread.
Basically, exception life has two stages: raise and handle. EurekaLog hooks both stages when they are implemented in default RTL/VCL code. You need to explicitly indicate which threads you want to hook, because you probably want to ignore system / 3rd party threads, which you have no control over. And it so happens that default processing for TThread does not exist in RTL/VCL. That is why there is nothing to hook.

Inno Setup: Modify icon and title of specific error message

I have a program that should only be installed on 64-bit architectures. So I added:
ArchitecturesAllowed = x64
In my Inno Setup file to prevent this.
The problem is that the message displayed is a bit too "violent". It displays "error" in title and a big red cross.
I've found that I can change content of this message (with OnlyOnTheseArchitectures message), but no way to modify icon and title without impacting other error message.
And I do not see what step can match...
Is there a way to make it like an information box?
There's no generic way to modify any standard Inno Setup message box.
You generally have to re-implement the function on your own. What is not always possible.
For your specific case, you are lucky, as you can easily implement your own custom check for 64-bit system using the IsWin64 function from the InitializeSetup event function.
And display your own custom message box using the MsgBox function.
function InitializeSetup(): Boolean;
begin
Result := True;
if not IsWin64 then
begin
MsgBox('This cannot be installed on 32-bit system.', mbInformation, MB_OK);
Result := False;
end;
end;

Inno Setup Change AppName based on component(s) selected

I need the installer to show different AppName based on (un)selected components. I tried this:
[Setup]
AppName={code:GetAppName}
AppVersion=1.0
AppVerName=Dagon Video Tools
AppId=Dagon Video Tools
DefaultDirName={sd}\Games\Dagon Video Tools
[Code]
function GetAppName(Value: string): string;
var
CurPageID: Integer;
Begin
Result := 'Dagon Video Tools'
if (CurPageID=wpSelectComponents) and IsComponentSelected('Slasher') and not IsComponentSelected('Frankenstein') then
begin
Result := 'Dagon Slasher';
end;
if (CurPageID=wpSelectComponents) and IsComponentSelected('Frankenstein') and not IsComponentSelected('Slasher') then
begin
Result := 'Dagon Frankenstein';
end;
if (CurPageID=wpSelectComponents) and IsComponentSelected('Slasher') and IsComponentSelected('Frankenstein') then
begin
Result := 'Dagon Video Tools';
end;
End;
But, as you can guess, this doesn't work. Is this script incomplete or should it be done in a different way altogether?
The AppName directive value is resolved (= your GetAppName is called) immediately after the InitializeSetup (if any) finishes. That is a long before the user is able to change the components.
So you cannot make AppName depend on the selected components.
Some uses of the AppName could be overridden with a custom value though, but not all. See below.
Though, as I know that your question is actually about a setup type, you can do this:
Create custom "type" page (like a menu) as the very first one.
Once the user selects the "type", restart the installer with a custom switch (e.g. /APPTYPE=slasher) and exit.
Once the installer is (re-)run with the /APPTYPE, you know from the beginning, what component/type you are installing and hence you can set the AppName normally.
Of course, you skip the custom "type" page.
This is actually a way simpler to implement. The only drawback is that the setup window is "recreated" after the user selects the "type".
This is the original response in case you do not want to use the above solution.
First, your implementation of the GetAppName is wrong. You are using an uninitialized variable CurPageID. And anyway, as mentioned already, the GetAppName is called even before the wizard window is created, so "current page" is irrelevant here.
The correct implementation would be like:
function GetAppName(Value: string): string;
begin
if IsComponentSelected('Slasher') and not IsComponentSelected('Frankenstein') then
begin
Result := 'Dagon Slasher';
end
else
if IsComponentSelected('Frankenstein') and not IsComponentSelected('Slasher') then
begin
Result := 'Dagon Frankenstein';
end
else
begin
Result := 'Dagon Video Tools';
end;
end;
But this still won't make it working in the AppName directive. We will use it in other contexts though later.
Also note that for your specific installer, you should better use the WizardSetupType(false) function instead of the IsComponentSelected.
FinishedLabel
Just override the Inno Setup default text in CurPageChanged(wpFinished):
procedure CurPageChanged(CurPageID: Integer);
var
S: string;
begin
if CurPageID = wpFinished then
begin
S := SetupMessage(msgFinishedHeadingLabel);
StringChange(S, '[name]', GetAppName(''));
WizardForm.FinishedHeadingLabel.Caption := S;
WizardForm.AdjustLabelHeight(WizardForm.FinishedHeadingLabel);
{ Ideally we should shift the FinishedLabel up or down here, }
{ if the height of the header changed. }
{ Note that other messages (msgFinishedLabelNoIcons or msgFinishedRestartLabel) }
{ are used in special situations, so this is not a complete solution. }
S := SetupMessage(msgFinishedLabel);
StringChange(S, '[name]', GetAppName(''));
WizardForm.FinishedLabel.Caption := S;
WizardForm.AdjustLabelHeight(WizardForm.FinishedLabel);
end;
end;
Add/Remove Programs
That's easy. There's the UninstallDisplayName directive for this, which is resolved only during the actual installation, when we already know the selected components. So we can use your (fixed) GetAppName here:
[Setup]
UninstallDisplayName={code:GetAppName}
Are you sure you want to completely remove AppName and all of its components?
You cannot change that. You better use some generic name in the AppName so that this message works for any component.
Or make the message not mention the application name at all:
[Messages]
ConfirmUninstall=Are you sure you want to completely remove this game?
Alternatively remove the message completely:
Replace or customize modal uninstallation windows in Inno Setup
Please wait while AppName is removed from your computer
The same solution as for the WizardForm.FinishedLabel. Just use the UninstallProgressForm.PageDescriptionLabel from the InitializeUninstallProgressForm.
AppName was successfully removed from your computer
Similar as with the "Are you sure you want to completely remove AppName and all of its components?"
Either make the AppName generic. Or disable the message with "silent" mode and implement your own message in the CurUninstallStepChanged(usPostUninstall).
Again, see Replace or customize modal uninstallation windows in Inno Setup.
For a similar discussion, see also Changing AppName and AppDir depending on language in Inno Setup.

Simple multithreading Delphi

I am still quite new to threading. I want to create a procedure which tests for a valid internet connection while the main thread creates the necessary forms. The code snippet stops at the end of the constructor with a 'Cannot call Start on a running or suspended thread' error. And for some reason the main form is closed after this error
constructor TPingThread.Create(IDThread: Integer);
begin
Self.FID:=IDThread;
Self.FreeOnTerminate:=true;
end;
destructor TPingThread.Destroy;
begin
EndThread(FID);
inherited;
end;
procedure TPingThread.Execute;
var
iTimeOuts, K: Byte;
sWebpage: String;
begin
inherited;
iTimeOuts:=0;
FIdPing:=TIdHTTP.Create(nil);
for k:=1 to 3 do
begin
Try
FIdPing.ConnectTimeout:=2000;
sWebpage:=FIdPing.Get('http://www.google.co.za')
Except
On Exception do inc(iTimeOuts);
End;
end;
if iTimeOuts=3 then MessageDlg('A working internetconnection is needed to reset your password',mtWarning,[mbOK],0);
if iTimeOuts=0 then FInternetConnection:=false
else FInternetConnection:=true;
FreeAndNil(FIdPing);
end;
There are some problems with your code:
You need to call the inherited constructor:
constructor TPingThread.Create(IDThread: Integer);
begin
inherited Create(false); // Or true to create a suspended thread
Self.FID:=IDThread;
...
Remove the inherited call in the Execute method, since this is an abstract declaration of TThread. Not an error per se, but should be avoided for clarity.
Use a try/finally after creating FIdPing in the Execute method.
As #mjn says, there is no need to call EndThread(), since the TThread handles this for you.
Calling the VCL MessageDlg() from a thread is not thread safe. You need to synchronize the call or use the Application.MessageBox, a Delphi wrapper to windows MessageBox. Best solution would be to skip the dialog and pass an error message to the main thread, which would need to know this error anyway.

AsyncCall with Delphi 2007

What I basically want is to start AsyncCall and proceed with my code loading. I have Interface section that consumes lots of time (600+ms) and I want to load this code in independent thread.
I've tried to use AsyncCall to make something like this:
procedure Load;
begin
...
end;
initialization
AsyncCall(#Load, []); // or LocalAsyncCall(#Load)
However, this Load procedure actually starts in Main thread and not in the new created thread. How can I force the Load procedure to be loaded in any thread other than MainThread?
I can create TThread and Execute this but I want to force AsyncCall or LocalAsyncCall or anything from AsyncCall library to make to work.
Thanks for your help.
Have you tried something like this?:
procedure Load;
begin
if GetCurrentThreadId <> MainThreadID then
Beep;
end;
var a: IAsyncCall;
initialization
a := AsyncCall(#Load, []);
a.ForceDifferentThread;
ForceDifferentThread() tells AsyncCalls that the assigned function must
not be executed in the current thread.
The problem is that your code is not retaining the IAsyncCall interface that is returned by the AsyncCall function.
AsyncCall(#Load, []);
//AsyncCall returns an IAsyncCall interface,
//but this code does not take a reference to it
Because of this, the interface that is returned has its reference count decremented to zero as soon as the initialization section completes. This therefore frees the object that implements the interface which does this:
destructor TAsyncCall.Destroy;
begin
if FCall <> nil then
begin
try
--> FCall.Sync; // throw raised exceptions here
finally
FCall.Free;
end;
end;
inherited Destroy;
end;
The key line is the call to Sync which forces the asynchronous call to be executed to completion. All this happens in the main thread which explains the behaviour that you report.
The solution is that you simply need to keep the IAsyncCall interface alive by storing it in a variable.
var
a: IAsyncCall;
initialization
a := AsyncCall(#Load, []);
In the real code you need to ensure that Load had completed before running any code that is reliant on Load. When your program reached a point where it required Load to have been called it has to call Sync on the IAsyncCall interface.
So you might write it something like this.
unit MyUnit;
interface
procedure EnsureLoaded;
implementation
uses
AsyncCalls;
....
procedure Load;
begin
....
end;
var
LoadAsyncCall: IAsyncCall;
procedure EnsureLoaded;
begin
LoadAsyncCall := nil;//this will effect a call to Sync
end;
initialization
LoadAsyncCall := AsyncCall(#Load, []);
end.
The call EnsureLoaded from other units that required Load to have run. Or, alternatively, call EnsureLoaded from any methods exported by MyUnit that depended on Load having run. The latter option has much better encapsulation.

Resources