Newbie question: I would like to run a powershell script (.ps1) at the end of the inno-setup install. Can anyone give me a tip on where to put this? I want the user prompted to be asked if he wants to run this script.
Oh yes, what this script does is run netsh.exe to open up a port, the script is clever and it grabs Env:username and Env:userdomain from the current context. Would the context be the admin who is running the setup? or would it be the original user that ran the setup.exe?
Another way is to run the script using the ShellExec from the code.
[Files]
Source: "yourPowershell.ps1"; DestDir: "{app}"; Flags: overwritereadonly replacesameversion promptifolder;
[Tasks]
Name: "runpowershell"; Description: "Do you want to run Powershell script?"
[Code]
procedure CurStepChanged(CurStep: TSetupStep);
var
ErrorCode: Integer;
ReturnCode: Boolean;
begin
if CurStep = ssPostInstall then begin
if(IsTaskSelected('runpowershell')) then begin
ExtractTemporaryFile('yourPowershell.ps1');
ReturnCode := ShellExec('open', '"PowerShell"', ExpandConstant(' -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File "{tmp}\YourPowershell.ps1"'), '', SW_SHOWNORMAL, ewWaitUntilTerminated, ErrorCode);
if (ReturnCode = False) then
MsgBox('Message about problem. Error code: ' + IntToStr(ErrorCode) + ' ' + SysErrorMessage(ErrorCode), mbInformation, MB_OK);
end;
end;
[Run]
.....; Description: Run Script; Flags: postinstall
(See the help for more details.) By default this will display a checkbox and run under the original user's context (although it depends a bit on how the installer is run).
You might want to reconsider this approach, though; if you are performing a machine-wide install then you should probably open the port machine-wide too. You can do this with pure Inno code calling WinAPIs -- no powershell required. (Which is a good thing, because it might not be installed.)
Alternatively if you want to keep it a per-user setting you should consider making your application prompt the user for a decision on first run. After all, why give an option to only one of the many possible users of your app?
Related
This question already has answers here:
How to force Inno Setup setup to fail when Run command fails?
(4 answers)
Using Process Exit code to show error message for a specific File in [Run]
(2 answers)
Closed 2 years ago.
I've been creating my first installers with Inno Setup and while they work, I want to look into some more profound error handling in case things go south during installation.
Here are the script snippets containing what I have so far. All of this works, questions follow below.
[Components]
Name: "base"; Description: "Install base files needed for this application"; Types: full compact custom; Flags: fixed
Name: "fonts"; Description: "Install Spartan fonts for a correct rendering of the application"; Types: full custom
Name: "updater"; Description: "Create a scheduled task to run the component updater on a daily basis"; Types: full custom
[Tasks]
Name: "update"; Description: "Run the updater after the installation to bring all application files up to date"
[Files]
; Application files
Source: "{#MyAppSourceNetwork1}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion; Components: base
Source: "{#MyAppSourceNetwork1}\{#MyAppcfgName}"; DestDir: "{app}"; Flags: ignoreversion; Components: base
Source: "{#MyAppSourceNetwork1}\Source\Componenten_COM\*"; DestDir: "{app}\Componenten_COM"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: base
Source: "{#MyAppSourceNetwork1}\Source\Componenten_RegAsm\*"; DestDir: "{app}\Componenten_RegAsm"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: base
Source: "{#MyAppSourceNetwork2}\*"; DestDir: "{#MyAppLocalFolderLib}\OCXinstaller"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: base
; Updater
Source: "{#MyAppSource}\Updater\*"; DestDir: "{#MyAppLocalFolderLib}\Updater"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: base
[Run]
; Create scheduled task for updater
Filename: "powershell"; \
Parameters: "-Executionpolicy Bypass -NoProfile -WindowStyle Hidden -Command ""Register-ScheduledTask -TaskName '{#MyAppTSUpdaterName}' -Description 'Smartclient component updater' -User System -Action (New-ScheduledTaskAction -Execute wscript -Argument '{#MyAppLocalFolderLib}\Updater\{#MyAppUpdateScript}') -Trigger (New-ScheduledTaskTrigger -Daily -At 07:00) -Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -ExecutionTimeLimit (New-TimeSpan -Hours 2) -MultipleInstances IgnoreNew)"""; \
Description: "Create scheduled task for updater"; \
StatusMsg: "Creating scheduled task ""{#MyAppTSUpdaterName}""..."; \
Flags: runhidden; \
Components: updater
; Run scheduled task for updater
Filename: "wscript"; \
Parameters: """{#MyAppLocalFolderLib}\Updater\{#MyAppUpdateScript}"""; \
Description: "Run Smartclient component updater"; \
StatusMsg: "Running Smartclient updater. Please wait, this will take a few minutes..."; \
Flags: runhidden; \
BeforeInstall: SetMarqueeProgress(True); \
AfterInstall: SetMarqueeProgress(False); \
Tasks: update
; Offer option to launch application after installation
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent unchecked
[Code]
// Show infinite progress bar while waiting
procedure SetMarqueeProgress(Marquee: Boolean);
begin
if Marquee then
begin
WizardForm.ProgressGauge.Style := npbstMarquee;
end
else
begin
WizardForm.ProgressGauge.Style := npbstNormal;
end;
end;
For the purpose of testing, I deliberatly wrote some errors in the commands (RRRegister-ScheduledTask / UUUpdater / NNNotepad) and in the log file below, you can clearly see that the commands do not return code 0.
During the installation I got message boxes for the wscript and notepad commands, making clear that something went wrong, with the return code and an OK button. However, the Powershell command doesn't give a message at all. It only returns an error when I write the command wrong (PPPowershell.exe) but not when there is an error in the Parameters flag. Any idea why? Because I'm sure Powershell returns some nice red lines indicating an error occured.
Additionally, I notice that Inno Setup writes in the log that the installation succeeded (7th line in my snippet) although it even still needs to start with the [Run] section. When I write the commands without typos, the setup does everything correctly, but it doesn't give me much confidence in how my installers might indicate success even when some actions didn't succeed. Why isn't the [Run] block considered to be an integral part if the installation?
2020-06-10 08:52:15.025 Saving uninstall information.
2020-06-10 08:52:15.025 Deleting uninstall key left over from previous administrative 32-bit install.
2020-06-10 08:52:15.027 Creating new uninstall key: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\{C3DAA6E8-7152-40C7-8487-F4A1BDC3EB92}_is1
2020-06-10 08:52:15.027 Writing uninstall key values.
2020-06-10 08:52:15.030 Detected previous non administrative install? No
2020-06-10 08:52:15.030 Detected previous administrative 64-bit install? No
2020-06-10 08:52:15.047 Installation process succeeded.
2020-06-10 08:52:15.052 -- Run entry --
2020-06-10 08:52:15.052 Run as: Current user
2020-06-10 08:52:15.053 Type: Exec
2020-06-10 08:52:15.053 Filename: powershell
2020-06-10 08:52:15.053 Parameters: -Executionpolicy Bypass -NoProfile -WindowStyle Hidden -Command "RRRegister-ScheduledTask -TaskName 'Smartclient Updater' -Description 'Smartclient component updater' -User System -Action (New-ScheduledTaskAction -Execute wscript -Argument 'C:\Smartclient_MH_LIB\Updater\Mediahuis_Smartclient_Components_Updater.vbs') -Trigger (New-ScheduledTaskTrigger -Daily -At 07:00) -Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -ExecutionTimeLimit (New-TimeSpan -Hours 2) -MultipleInstances IgnoreNew)"
2020-06-10 08:52:19.509 Process exit code: 1
2020-06-10 08:52:19.511 -- Run entry --
2020-06-10 08:52:19.511 Run as: Current user
2020-06-10 08:52:19.511 Type: Exec
2020-06-10 08:52:19.511 Filename: wscript
2020-06-10 08:52:19.511 Parameters: "C:\Smartclient_MH_LIB\UUUpdater\Mediahuis_Smartclient_Components_Updater.vbs"
2020-06-10 08:57:47.994 Process exit code: 1
2020-06-10 08:57:47.996 -- Run entry --
2020-06-10 08:57:47.996 Run as: Current user
2020-06-10 08:57:47.996 Type: Exec
2020-06-10 08:57:47.996 Filename: NNNotepad
2020-06-10 08:57:47.997 Exception message:
2020-06-10 08:57:47.997 Message box (OK):
Unable to execute file:
NNNotepad
CreateProcess failed; code 2.
The system cannot find the file specified.
2020-06-10 08:57:50.009 User chose OK.
So where should I take this from here to be able to handle the return codes of these commands?
How can I specify which return codes from a [Run] line should be considered fatal, and which not? How do I catch them? I understand I will probably need to write [Code] blocks, maybe using Exec? But how do I trigger them from a [Run] line? Using Check or something else?
Can someone give an example how to catch the return code from the Powershell or Wscript command and how to throw a message box which manipulates the behaviour of the installer (e.g. quit if you click Abort, continue if you click Ignore)?
How do you manipulate the exit code from the setup itself in case things go wrong? Or is this a big no-no?
I read things about Windows process exit codes that are different than return codes in Pascal scripting. This is confusing the hell out of me. Can someone explain?
I've been looking into many examples and similar topics, but couldn't really find a useful case which I could edit to suit my needs.
Thanks for reading this far and for any help / insight you can offer.
EDIT 2020/06/16
Sorry for the delay. Meanwhile the topic has been closed, but I wanted to let you know how I solved it eventually and that I'm grateful for the help you offered. With the suggestions provided by Martin Prikryl and some additional searching I was able to reach my goals: write a [Code] block which lets me perform additional tasks, catch the return code and override the Setup exitcode if I wish to. I will post my generic code example below for anyone who needs a similar solution.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; // Example - Run additional actions purely in the [Code] block during the ssPostInstall step (executed after the [Run] block) and use ExitCode to modify the default exit code of the setup
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[Tasks]
Name: "runscript"; Description: "Run a script"
[Code]
// Start with exmpty exit code, can be used to override setup exit code with GetCustomSetupExitCode
var
ExitCode: Integer;
// Show infinite progress bar while waiting
procedure SetMarqueeProgress(Marquee: Boolean);
begin
if Marquee then
begin
WizardForm.ProgressGauge.Style := npbstMarquee;
end
else
begin
WizardForm.ProgressGauge.Style := npbstNormal;
end;
end;
// Run post-installation commands
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
ProgramNAme: String;
ProgramArguments: String;
begin
if CurStep = ssPostInstall then
begin
// Change progressbar
SetMarqueeProgress(True);
// Run script if task is selected
if IsTaskSelected('runscript') then
begin
WizardForm.StatusLabel.Caption := 'Running script. This may take a few minutes...';
ProgramName := 'wscript';
ProgramArguments := ExpandConstant('{sd}\Smartclient_MH_LIB') + '\script\{#MyAppUpdateScript}';
Log('Running ' + ProgramName + ' ' + ProgramArguments);
if Exec(ProgramName, ProgramArguments, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
Log('ResultCode = ' + IntToStr(ResultCode) + '.');
if ResultCode = 0 then
begin
Log('Setup successfully ran the script.')
end
else
begin
Log('Setup failed to run the script.');
ExitCode := ExitCode + 1;
end;
end;
end
else
begin
Log('Skipping update because task "update" is not selected.');
end;
// Change progressbar
SetMarqueeProgress(False);
end;
end;
// Override setup exit code
function GetCustomSetupExitCode: Integer;
begin
if ExitCode <> 0 then
begin
Log('Returning custom exit code ' + IntToStr(ExitCode));
end;
Result := ExitCode;
end;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Based on the excellent Excel add-in installer (Daniel's XL Toolbox), I have built a setup file that among other things needs to register some ActiveX's
[Files]
; The include file makes adds all .XLA and .XLAM files contained in the
; SOURCEDIR to the project.
Source: "c:\source\path\MSCOMCTL.OCX"; \
DestDir: "\users\public\EzPasteFiles"; Flags: regserver
Source: "c:\source\path\DAS_AX_Knob.dll"; \
DestDir: "\users\public\EzPasteFiles"; Flags: regserver
Source: "c:\source\path\GIF89.DLL"; \
DestDir: "\users\public\EzPasteFiles"; Flags: regserver
I need the addin to install, then before starting to register the files a check is done about administrator rights and if the user has none, a message is displayed asking for entering the admin password so that registration can take place. I am aware that it can be done at the beginning of the setup, but then the addin will not be activated, if it is a standard user account. The addin needs registered components, a standard user can't install it properly.
I am looking for something like this to fire before the registration starts:
MyProgChecked := not(IsAdminLoggedOn or IsPowerUserLoggedOn);
if MyProgChecked = True then
begin
MsgBox(
'Kindly notice:' #13#13
'It seems as you are not looged as an administrator' #13#13
'Please abort and reinstall EzPaste AS an administrator' #13#13
'(To install As an Adminstrator, just save the exe setup anywhere then Right Click on it to get to this feature or ask your IT administrator for proper directives)',
mbConfirmation, MB_OK);
{ Popup message asking for Pwd }
ExitProcess(0);
end;
I am naturally open for any other approach
I'll be glad also to understand how a domain user (Windows server) without admin rights should proceed to install the addin.
You can execute regsvr32.exe "as administrator", this way:
[Files]
Source: "MyDll.dll"; DestDir: "{app}"; AfterInstall: RegMyDll
[Code]
procedure RegMyDll;
var
Path: string;
RegSvr: string;
Params: string;
Registered: Boolean;
ErrorCode: Integer;
begin
Path := ExpandConstant(CurrentFilename);
RegSvr := 'regsvr32.exe';
Params := Format('/s "%s"', [Path]);
Log(Format('Registering %s using "%s" %s', [Path, RegSvr, Params]));
Registered :=
ShellExec('runas', RegSvr, Params, '', SW_HIDE, ewWaitUntilTerminated, ErrorCode);
if Registered and (ErrorCode = 0) then
begin
Log(Format('Registered %s', [Path]));
end
else
begin
MsgBox(Format('Registering %s failed with code %d', [Path, ErrorCode]), mbError, MB_OK);
end;
end;
Alternative implementation is to create subinstaller for the registration only that will require Administrator privileges.
For a similar example, see Inno Setup - Access unprivileged account folders from installer that requires privileges.
Or use an opposite approach. Require administrator privileges using
[Setup]
PrivilegesRequired=admin
(which is default)
But deploy files to the original user folder.
See my answer to Inno Setup always installs into admin's AppData directory.
As part of the copy protection of my game, the installer needs to delete itself after the installation process. This code:
[Code]
procedure MyAfterInstall();
begin
DeleteFile('F:\TEST_SETUP\setup.exe');
end;
...does nothing because the setup runs.
Is there a solution to run an 'command line' or cmd that gets the full path of the installer (it could be everywhere on the disc of the client) and delete it after the install?
Add following method in your [CODE] section and you are all set...
[CODE]
procedure CurStepChanged(CurStep: TSetupStep);
var
strContent: String;
intErrorCode: Integer;
strSelf_Delete_BAT: String;
begin
if CurStep=ssDone then
begin
strContent := ':try_delete' + #13 + #10 +
'del "' + ExpandConstant('{srcexe}') + '"' + #13 + #10 +
'if exist "' + ExpandConstant('{srcexe}') + '" goto try_delete' + #13 + #10 +
'del %0';
strSelf_Delete_BAT := ExtractFilePath(ExpandConstant('{tmp}')) + 'SelfDelete.bat';
SaveStringToFile(strSelf_Delete_BAT, strContent, False);
Exec(strSelf_Delete_BAT, '', '', SW_HIDE, ewNoWait, intErrorCode);
end;
end;
If you need to delete the installer immediately after the installation, you have to implement some custom solution. As you have already found yourself, executable cannot delete itself as the executable is locked when it is running.
You can implement a small tool that you install by your installer. The tool will be run by the installer, when an installation finishes. The tool will keep running silently and trying to delete the installer, until is succeeds.
[Files]
; Install the tool
Source: "zapself.exe"; DestDir: "{app}"
[Run]
; Run the tool and pass the installer path
Filename: "{app}\zapself.exe"; Parameters: "{srcexe}"
Actually you do not need to build an .exe for this. A simple batch file created on-the-fly will do, for an example, see Unload a .NET DLL from an unmanaged process.
Need to say that I really do not understand why you are trying to do this.
I have an issue with the Inno Setup uninstaller. I have a exe file I want to executable to keep track of the installation and uninstallations. The exe is really simple and sends a message to a server.
[Files]
Source: "Tracker\LocalSandboxInstallTracker.exe"; DestDir: "{app}/Tracker";
Source: "Tracker\LocalSandboxInstallTracker.exe.config"; DestDir: "{app}/Tracker";
Source: "Tracker\Tracker.Client.dll"; DestDir: "{app}/Tracker";
[Run]
Filename: "{app}\Tracker\LocalSandboxInstallTracker.exe"; Parameters: " {#MyAppVersion} install"; Flags: runhidden; StatusMsg: "Sending tracking data..."
[Code]
procedure InitializeUninstallProgressForm();
var
ResultCode: Integer;
begin
Exec ('{app}\Tracker\LocalSandboxInstallTracker.exe',' {#MyAppVersion} uninstall','',SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
The call at the installation works well, but not at the uninstall. I placed a breakpoint to my Exec command and it really goes through there, but the exe does not seem to be called.
You must expand the {app} constant before passing it to the Exec script function. Use the ExpandConstant whenever you need to get the value of the constant. Modify your script this way:
Exec(ExpandConstant('{app}\Tracker\LocalSandboxInstallTracker.exe'),
'{#MyAppVersion} uninstall', '', SW_SHOW,
ewWaitUntilTerminated, ResultCode);
Also, you should check the function result and the output result code to react when the Exec function fails. The error code you'll get in the ResultCode you can check against the System Error Codes reference or use SysErrorMessage(ResultCode) to get the error description from script.
You have to call the ExpandConstant function if you want to use constants like {app} in your Exec call:
[Code]
procedure InitializeUninstallProgressForm();
var
ResultCode: Integer;
begin
Exec (ExpandConstant('{app}\Tracker\LocalSandboxInstallTracker.exe')
,' {#MyAppVersion} uninstall','',SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
Other way, you're failing to locate the exe.
To save bandwidth/space as well as prevent accidental meddling, the installation files for a database product (call it Ajax), have been zipped up (call that file "AJAX_Install_Files.ZIP). I would like to have Inno-Setup "install" (i.e., copy) the AJAX_Install_Files.ZIP file to the destination, and then Unzip the files into the same folder where the .ZIP file is located. A subsequent program would be fired off by Inno Setup to actually run the install of product "Ajax".
I've looked through the documentation, FAQ, and KB at the Inno Setup website, and this does not seem possible other than writing a Pascal script (code) - would that be correct, or are there are any alternative solutions?
You can use an external command line tool for unzipping your archive, see here for example. Put it in your [Files] section:
[Files]
Source: "UNZIP.EXE"; DestDir: "{tmp}"; Flags: deleteafterinstall
Then call it in your [Run] section, like this:
[Run]
Filename: "{tmp}\UNZIP.EXE"; Parameters: "{tmp}\ZipFile.ZIP -d C:\TargetDir"
(You'll probably want to take your target directory from a script variable, so there is some more work that needs to be done)
You can use the shell Folder.CopyHere method to extract a ZIP.
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
procedure UnZip(ZipPath, TargetPath: string);
var
Shell: Variant;
ZipFile: Variant;
TargetFolder: Variant;
begin
Shell := CreateOleObject('Shell.Application');
ZipFile := Shell.NameSpace(ZipPath);
if VarIsClear(ZipFile) then
RaiseException(
Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
TargetFolder := Shell.NameSpace(TargetPath);
if VarIsClear(TargetFolder) then
RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
TargetFolder.CopyHere(
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
Note that the flags SHCONTCH_NOPROGRESSBOX and SHCONTCH_RESPONDYESTOALL work on Windows Vista and newer.
For an example of extracting some files only, see:
How to get Inno Setup to unzip a single file?
I answered a very similar question and some of the details apply.
I would question why you need a ZIP file of the contents? I personally would place the uncompressed files into the setup. I would then have two [category] entries one for the application and one for the data. Default both the be checked.
This would allow the users to install a fresh set of the data if needed at a later date.
If you really want a ZIP file and want to keep it easy you could, ship both the zip files and the uncompressed files in the same setup.
Update:
By default files that get placed in your setup.exe are compressed.
You can also have the files extracted to a temporary location so you can run your
installation application, then have them deleted.
[Files]
Source: "Install1.SQL"; DestDir: "{tmp}"; Flags:deleteafterinstall;
Source: "Install2.SQL"; DestDir: "{tmp}"; Flags:deleteafterinstall;
You can just create silent self-extracting archive (SFX) archive, example described here how to create SFX archive for stuff you need, and write Pascal code to just run it like this (script for Inno Setup 6.0.2):
[Tasks]
Name: "intallSenselockDriver"; Description: "Install Senselock driver."; GroupDescription: "Install the necessary software:";
[Code]
function ExecTmpFile(FileName: String): Boolean;
var
ResultCode: Integer;
begin
if not Exec(ExpandConstant('{tmp}\' + FileName), '', '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode)
then
begin
MsgBox('Other installer failed to run!' + #13#10 + SysErrorMessage(ResultCode), mbError, MB_OK);
Result := False;
end
else
Result := True;
end;
procedure RunOtherInstallerSFX(ArchiveName: String; ExePath: String);
begin
ExtractTemporaryFile(ArchiveName);
ExecTmpFile(ArchiveName);
ExecTmpFile(ExePath);
end;
function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
if WizardIsTaskSelected('intallSenselockDriver') then
RunOtherInstallerSFX('1_senselock_windows_3.1.0.0.exe', '1_senselock_windows_3.1.0.0\InstWiz3.exe');
Result := '';
end;
It worked perfectly for me.
Using Double quotes worked for me.
Single quotes were not working.
[Files]
Source: "unzip.exe"; DestDir: "{userappdata}\{#MyAppName}\{#InputFolderName}"; Flags: ignoreversion
[Run]
Filename: "{userappdata}\{#MyAppName}\{#InputFolderName}\unzip.exe"; Parameters: " ""{userappdata}\{#MyAppName}\{#InputFolderName}\ZIPFILENAME.zip"" -d ""{userappdata}\{#MyAppName}\{#InputFolderName}"" "; Flags: runascurrentuser