yuval
yuval

Reputation: 3088

Inno Setup: Can the installer update itself?

My installer creates a folder with my app and the installer itself. The installer is later on used as an updater for the application.

All of this works well but I would like to update the installer itself and not just my application.

I download a zip from my server and expect everything inside the zip to override everything in the app folder (including the installer itself). Every time I run the installer I get an error that a file is already in use.

Can the installer update itself?

Upvotes: 2

Views: 1475

Answers (3)

Pedro Gaspar
Pedro Gaspar

Reputation: 877

I've run into this same problem recently. We have a main installer that manages a bunch of other setup packages for our applications, and I wanted to add some mechanism for this main installer to update itself.

I've managed to find a solution creating a second Inno Setup package that serves just as an updater for the main installer, and that updater goes embedded in the main installer.

So, we have a XML file in our website that gives the latest version available for the main installer:

[ InstallerLastVersion.xml ]

<?xml version="1.0" encoding="utf-8"?>
<installer name="Our Central Installer" file="OurInstaller.exe" version="1.0.0.1" date="10/15/2021" />

The main code for this auto-update functionality in the main installer is that:

[ OurInstaller.iss ]

[Files]
; This file won't be installed ('dontcopy' flag), it is just embedded
; into the installer to be extracted and executed in case it's necessary
; to update the Installer.
Source: ".\OurInstallerUpdater.exe"; Flags: dontcopy
[Code]
const
  UrlRoot = 'http://ourwebsite.com/';

// Downloads a XML file from a website and loads it into XmlDoc parameter.
function LoadXml(XmlFile: String; var XmlDoc: Variant): Boolean;
begin
  XmlDoc := CreateOleObject('MSXML2.DOMDocument');
  XmlDoc.async := False;
  Result := XmlDoc.Load(UrlRoot + XmlFile);
end;

// Checks if there's a newer version of the Installer
// and fires the updater if necessary.
function InstallerWillBeUpdated(): Boolean;
var
  XmlDoc: Variant;
  LastVersion, Filename, Param: String;
  ResultCode: Integer;
begin
  if not LoadXml('InstallerLastVersion.xml', XmlDoc) then
  begin
    Result := False;
    Exit;
  end;
  // Gets the latest version number, retrieved from
  // the XML file download from the website.
  LastVersion := XmlDoc.documentElement.getAttribute('version');
  // If this installer version is the same as the one available
  // at the website, there's no need to update it.
  if '{#SetupSetting("AppVersion")}' = LastVersion then
  begin
    Result := False;
    Exit;
  end;

  if MsgBox('There is an update for this installer.' + #13#10 +
            'Do you allow this installer to be updated right now?',
            mbConfirmation, MB_YESNO) = IDNO then
  begin
    Result := False;
    Exit;
  end;

  // Extracts the updater, that was embedded into this installer,
  // to a temporary folder ({tmp}).
  ExtractTemporaryFile('OurInstallerUpdater.exe');
  // Gets the full path for the extracted updater in the temp folder.
  Filename := ExpandConstant('{tmp}\OurInstallerUpdater.exe');
  // The current folder where the installer is stored is going to be
  // passed as a parameter to the updater, so it can save the new version
  // of the installer in this same folder.
  Param := ExpandConstant('/Path={src}');
  // Executes the updater, with a command-line like this:
  //   OurInstallerUpdater.exe /Path=C:\InstallerPath
  Result := Exec(Filename, Param, '', SW_SHOW, ewNoWait, ResultCode);
end;

function InitializeSetup(): Boolean;
begin
  // Checks if the installer needs to be updated and fires the update.
  // If the update is fired the installer must be ended, so it can be
  // replaced with the new version. Returning this InitializeSetup()
  // function with False already makes the installer to be closed.
  if InstallerWillBeUpdated() then
  begin
    Result := False;
    Exit;
  end;
  Result := True;
end;

Now to the updater code (I'm using the "new" DownloadTemporaryFile() function added in Inno Setup 6.1):

[ OurInstallerUpdater.iss ]

[Code]
const
  UrlRoot = 'http://ourwebsite.com/';
  Installer = 'OurInstaller.exe';

function InitializeSetup(): Boolean;
var
  DestinationPath: String;
  ResultCode: Integer;
begin
  // Retrieves the parameter passed in the execution
  // of this installer, for example:
  //   OurInstallerUpdater.exe /Path=C:\InstallerPath
  // If no parameter was passed it uses 'C:\InstallerPath' as default.
  // (where {sd} is the constant that represents the System Drive)
  DestinationPath := ExpandConstant('{param:Path|{sd}\InstallerPath}') + '\' + Installer;
  try
    // Downloads the newer version of the installer to {tmp} folder.
    DownloadTemporaryFile(UrlRoot + Installer, Installer, '', nil);
    // Copies the downloaded file from the temp folder to the folder where
    // the current installer is stored, the one that fired this updater.
    FileCopy(ExpandConstant('{tmp}\') + Installer, DestinationPath, False);
    // Runs the updated installer.
    Exec(DestinationPath, '', '', SW_SHOW, ewNoWait, ResultCode);
  except
    MsgBox('The file ''' + Installer + ''' could not be downloaded.', mbInformation, MB_OK);
  end;

  // Returning False from this function implies that this
  // updater can now be finished, since its goal has already
  // been reached (to update the main installer).
  Result := False;
end;

In this setting you have to build OurInstallerUpdater.exe before OurInstaller.exe, since the first one is embedded into the second one.

Some sources:

Upvotes: 1

Martin Prikryl
Martin Prikryl

Reputation: 202118

You cannot replace running application.

You have these options:

  • Start the "updater" via batch file (referring to assumed shortcut to the updater in a Start menu or any other method of invocation), that makes a copy of the installer to a temporary location and runs the updater from there. When updating, update the original copy.
  • To avoid the batch file (and an unpleasant console window), you can use JScript. Or even make the installer (updater) do this itself (create a copy of itself, launch the copy, exit itself).
  • Use restartreplace flag in Files section entry to schedule installer/updater replace for the next Windows start.

Upvotes: 2

Laurie Stearn
Laurie Stearn

Reputation: 999

Keeping the installer in the {app} directory is probably acceptable for small applications, for larger ones consider an updater, or even another location, (in the form of a feature request) {Backup} to refer to a path on some flash or removable drive.

Run the setup from the {app} directory and after the version check, download the installer to the {tmp} folder.
Exec the installer thus before quitting, keeping mind of possible mutex conditions in the code section of your script:

if Exec(ExpandConstant('{tmp}\{OutputBaseFilename}), '', '', SW_SHOW,
 ewNoWait, ResultCode) then
// success/fail code follows

To copy the installer back to {app} the Install script will have this in Files:

[Files]
Source: "{srcexe}"; DestDir: "{app}"; Flags: external

Presumably the above line will not produce an error when the installer is actually run from {app}.

Then, to clean up, the next time the installer is run from the {src} (= {app}) directory, the downloaded one can be removed from the {tmp} directory with

DeleteFile({tmp}\{OutputBaseFilename})

Upvotes: 0

Related Questions