Files
2026-04-12 13:45:59 -07:00

352 lines
18 KiB
Plaintext

#define B "Backrest"
#define Website "https://github.com/garethgeorge/backrest/"
; The following is needed to extract the version from the change log.
; If the application executable had the version info, then could use built-in GetVersion* functions.
#define fHandle FileOpen("CHANGELOG.md")
#expr FileRead(fHandle)
#expr FileRead(fHandle)
#define Line FileRead(fHandle)
#expr FileClose(fHandle)
#define VStart Pos("[", Line) + 1
#define VEnd Pos("]", Line)
#define VLen (VEnd - VStart)
#define BackrestVersion Copy(Line, VStart, VLen)
[Setup]
AppName={#B}
AppVersion={#BackrestVersion}
AppVerName={#B} {#BackrestVersion}
AppPublisher=garethgeorge
AppPublisherURL={#Website}
VersionInfoVersion={#BackrestVersion}
DefaultDirName={autopf}\{#B}
DefaultGroupName={#B}
DisableProgramGroupPage=yes
AlwaysShowDirOnReadyPage=yes
UninstallDisplayIcon={app}\icon.ico
#ifndef Arch
#define Arch "x86_64"
#endif
OutputBaseFilename={#B}Setup-{#Arch}
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
UsePreviousPrivileges=no
SetupMutex={#B}Setup
; Disable built-in RestartManager functionality, see comments under [Files].
CloseApplications=no
RestartApplications=no
#if SameText(Arch, "arm64")
#define ArchAllowed "arm64"
#else
#define ArchAllowed "x64os"
#endif
ArchitecturesAllowed={#ArchAllowed}
ArchitecturesInstallIn64BitMode={#ArchAllowed}
SetupLogging=yes
UninstallLogging=yes
[Tasks]
Name: "adminstartcurrent"; Description: "Run {#B} as the current user. Automatically start when the current user logs in. Inherits user access to network resources. Configuration is stored in the current user profile."; GroupDescription: "Execution context"; Check: IsAdminInstallMode; Flags: exclusive
Name: "adminstartsystem"; Description: "Run {#B} as the system user. Automatically start before any user logs in. Configuration is stored in the system user profile."; GroupDescription: "Execution context"; Check: IsAdminInstallMode; Flags: exclusive unchecked
Name: "autostart"; Description: "Automatically start {#B} at log on"; Check: IsUserInstallMode
Name: "desktopicon"; Description: "Create a desktop icon"; Flags: unchecked
Name: "addtopath"; Description: "Add {#B} directory to PATH ({code:GetEnvTarget})"; Flags: unchecked
#define PortDesc "Select a network port for the web interface"
Name: "port9898"; Description: "Default (9898)"; GroupDescription: "{#PortDesc}"; Flags: exclusive; Check: IsUserInstallMode
Name: "port9899"; Description: "9899"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
Name: "port9900"; Description: "9900"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
Name: "port9901"; Description: "9901"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
Name: "port9902"; Description: "9902"; GroupDescription: "{#PortDesc}"; Flags: exclusive unchecked; Check: IsUserInstallMode
[Files]
; Need to stop Backrest not only when uninstalling but also before upgrades or reinstalls
; This is only an issue when Backrest runs as SYSTEM or non-current user. Inno can natively close applications in other cases,
; but doing it the same way in all cases for consistency.
Source: "LICENSE"; DestDir: "{app}"; Flags: ignoreversion; BeforeInstall: StopBackrest
Source: "icon.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "backrest.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "restic.exe"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; For user install mode only.
Name: "{autostartup}\{#B} systray"; Filename: "{app}\backrest.exe"; Parameters: "--windows-tray {code:GetPortParam}"; IconFilename: "{app}\icon.ico"; Check: IsUserInstallMode; Tasks: autostart
Name: "{group}\{#B} systray"; Filename: "{app}\backrest.exe"; Parameters: "--windows-tray {code:GetPortParam}"; IconFilename: "{app}\icon.ico"; Check: IsUserInstallMode
; For both modes.
Name: "{group}\{#B}{code:GetIconSuffix}"; Filename: "http://localhost:{code:GetPort}/"; IconFilename: "{app}\icon.ico"
Name: "{group}\{#B} website"; Filename: "{#Website}"
Name: "{autodesktop}\{#B}{code:GetIconSuffix}"; Filename: "http://localhost:{code:GetPort}/"; IconFilename: "{app}\icon.ico"; Tasks: desktopicon
[Messages]
PrivilegesRequiredOverrideText2=%1 can be installed to run with standard or administrative privileges.%n%nIf you need to use Windows VSS feature with "--use-fs-snapshot" restic option, select the administrative option.%nProtecting Backrest web UI with a password is highly recommended, especially with the administrative install.
PrivilegesRequiredOverrideAllUsers=Install &system-wide with administrative privileges
[Run]
; Use Task Scheduler to run Backrest elevated. The 30s delay is needed to avoid an issue with tray icon being broken.
; The double-quotes escape double-quotes inside the parameter. The backslash escapes double-quotes inside the -Command block.
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""$t = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME ; $t.Delay = 'PT30S'; $s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries; $s.ExecutionTimeLimit = 'PT0S'; Register-ScheduledTask -Force -TaskName '{#B}' -RunLevel Highest -Trigger $t -Action $(New-ScheduledTaskAction -Execute \""{app}\backrest.exe\"" -Argument '--windows-tray --bind-address 127.0.0.1:9897' -WorkingDirectory '{app}') -Settings $s ; Start-ScheduledTask -TaskName '{#B}'"" "; Flags: runascurrentuser logoutput runhidden; Tasks: adminstartcurrent; Check: IsAdminInstallMode
; System user task. No need for systray here, and running it without returning control is the only way to stop it gracefully later.
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries; $s.ExecutionTimeLimit = 'PT0S'; Register-ScheduledTask -Force -TaskName '{#B}' -RunLevel Highest -User System -Trigger $(New-ScheduledTaskTrigger -AtStartup) -Action $(New-ScheduledTaskAction -Execute \""{app}\backrest.exe\"" -Argument '--bind-address 127.0.0.1:9897' -WorkingDirectory '{app}') -Settings $s ; Start-ScheduledTask -TaskName '{#B}'"" "; Flags: runascurrentuser logoutput runhidden; Tasks: adminstartsystem; Check: IsAdminInstallMode
; PATH
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""$newp = '{app}'; $a = [Environment]::GetEnvironmentVariable('PATH', '{code:GetEnvTarget}') -split ';' ; if ($a -notcontains $newp) {{ echo 'Adding to PATH'; $a += $newp; $path = $a -join ';' ; [Environment]::SetEnvironmentVariable('PATH', $path, '{code:GetEnvTarget}') }"" "; Flags: logoutput runhidden; Tasks: addtopath
; Remove from PATH for existing installation when unchecked.
; Reuse the same command in multiple places. Two quotes to escape in preprocessor, two more for the Parameters directive.
#define PathDelCmd "-ExecutionPolicy Bypass -Command """"$newp = '{app}'; $a = [Environment]::GetEnvironmentVariable('PATH', '{code:GetEnvTarget}') -split ';' ; if ($a -contains $newp) {{ echo 'Removing from PATH'; $path = ($a | Where-Object {{ $_ -ne $newp }) -join ';' ; [Environment]::SetEnvironmentVariable('PATH', $path, '{code:GetEnvTarget}') }"""" "
Filename: "powershell.exe"; Parameters: "{#PathDelCmd}"; Flags: logoutput runhidden; Tasks: not addtopath; Check: IsExistingInstallation
Filename: "{app}\backrest.exe"; Parameters: "--windows-tray {code:GetPortParam}"; Description: "Start {#B} (runs in the system tray)"; Flags: postinstall waituntilidle; Check: IsUserInstallMode
Filename: "http://localhost:{code:GetPort}/"; Description: "Open {#B} user interface"; Flags: postinstall shellexec
[UninstallRun]
Filename: "powershell.exe"; Parameters: "{#PathDelCmd}"; Flags: logoutput runhidden; RunOnceId: "RemoveFromPath"
[UninstallDelete]
Type: files; Name: "{app}\restic*.exe"
Type: files; Name: "{app}\install.lock"
; Built-in deletion runs before this section and fails to remove the directory due to the files above.
Type: dirifempty; Name: "{app}"
[Code]
var
UserInstallationExists, AdminInstallationExists: Boolean;
PreviousAdminUser, PreviousAdminTasks, PreviousVersionUser, PreviousVersionAdmin: String;
AppName, AppDirAdmin, AppDirUser, RegKey, Cmd: String;
procedure AssignGlobals();
begin
AppName := ExpandConstant('{#SetupSetting("AppName")}');
Cmd := ExpandConstant('{cmd}');
RegKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + AppName + '_is1';
if RegQueryStringValue(HKEY_CURRENT_USER, RegKey, 'Inno Setup: App Path', AppDirUser)
then UserInstallationExists := True else UserInstallationExists := False;
if RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'Inno Setup: App Path', AppDirAdmin)
then AdminInstallationExists := True else AdminInstallationExists := False;
RegQueryStringValue(HKEY_CURRENT_USER, RegKey, 'DisplayVersion', PreviousVersionUser)
RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'DisplayVersion', PreviousVersionAdmin)
RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'Inno Setup: Selected Tasks', PreviousAdminTasks);
RegQueryStringValue(HKEY_LOCAL_MACHINE, RegKey, 'Inno Setup: User', PreviousAdminUser);
end;
function IsUserInstallMode(): Boolean;
begin
if IsAdminInstallMode then Result := False else Result := True;
end;
function GetPort(Param: String): String;
var
S: String;
A: array of String;
i: Integer;
begin
if IsAdminInstallMode then Result := '9897'
else
begin
A := StringSplit(WizardSelectedTasks(False), [','], stAll);
for i := 0 to GetArrayLength(A) - 1 do
begin
S := A[i];
if Pos('port', S) > 0 then StringChangeEx(S, 'port', '', True);
end;
Result := S;
end;
end;
function GetPortParam(Param: String): String;
var
S: String;
begin
S := GetPort('');
// Don't add any shortcut parameters for default port selection.
if S = '9898' then Result := '' else Result := '--bind-address 127.0.0.1:' + S;
end;
function GetIconSuffix(Param: String): String;
begin
if IsAdminInstallMode and UserInstallationExists then Result := ' (system-wide)'
else if IsUserInstallMode and AdminInstallationExists then Result := ' (current user)'
else Result := '';
end;
function GetEnvTarget(Param: String): String;
begin
if IsAdminInstallMode then Result := 'Machine' else Result := 'User';
end;
function IsExistingInstallation(): Boolean;
begin
if UserInstallationExists or AdminInstallationExists then Result := True else Result := False;
end;
procedure StopBackrest();
var
ResultCode: Integer;
begin
// Attempt to terminate Backrest gracefully for the current user, wait for a second (since taskkill returns immediately),
// then check and kill forcefully if it's still running.
if IsUserInstallMode then
ExecAndLogOutput(Cmd, '/C "taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" & ping -n 2 127.0.0.1 >nul & tasklist /FI "USERNAME eq %USERNAME%" | findstr /I /V "setup" | findstr "backrest" && (echo Forcing & taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" /F)" ',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode, nil)
// For admin installs stop through the task scheduler. For the SYSTEM or non-current user this is the only way to stop gracefully.
// Ending the scheduled task makes a gracefull attempt, then force kills.
else if IsAdminInstallMode then
begin
ExecAndLogOutput(Cmd, '/C schtasks /End /TN ' + AppName + ' || (taskkill /FI "USERNAME eq %USERNAME%" /IM "backrest.exe" /F)',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode, nil);
// Remove the task when uninstalling.
if IsUninstaller then
ExecAndLogOutput(Cmd, '/C schtasks /Delete /TN ' + AppName + ' /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, nil);
end;
end;
procedure ShowUpgradeMsg(AppDir: String; InstalledVersion: String);
var
AppVersion, Msg: String;
InstalledVersionFull, NewVersionFull: Int64;
CompResult: Integer;
begin
AppVersion := ExpandConstant('{#SetupSetting("AppVersion")}');
// Convert both old and new versions to the correct type. It also automatically adds '.0' if '0.0.0' format is used.
if StrToVersion(InstalledVersion, InstalledVersionFull) and StrToVersion(AppVersion, NewVersionFull) then
begin
CompResult := ComparePackedVersion(InstalledVersionFull, NewVersionFull);
if CompResult < 0 then Msg := 'upgrade to'
else if CompResult = 0 then Msg := 'reinstall'
else if CompResult > 0 then Msg := 'downgrade to'
else Msg := 'upgrade/reinstall/downgrade';
end;
MsgBox('Detected existing installation of Backrest ' + InstalledVersion +
' in ' + Chr(13) + Chr(10) + AppDir + Chr(13) + Chr(10) + Chr(13) + Chr(10) +
'Setup will ' + Msg + ' version ' + AppVersion + '.', mbInformation, MB_OK);
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
ResultPortCheck: Integer;
begin
Result := True
if CurPageID = wpSelectTasks then
begin
// Prevent installing an admin instance when a user instance exists under the same user.
// It wouldn't work anyway due to the same configuration path.
if IsAdminInstallMode and WizardIsTaskSelected('adminstartcurrent') and UserInstallationExists then
begin
MsgBox('Detected an existing non-administrative installation under the same user ' + GetUserNameString +
'. Cannot proceed with this selection.', mbError, MB_OK)
Result := False;
end
else if IsUserInstallMode and not UserInstallationExists then
begin
if ExecAndLogOutput(Cmd, '/C netstat -na | findstr LISTENING | findstr /C:":' + GetPort('') + ' "', '', SW_HIDE,
ewWaitUntilTerminated, ResultPortCheck, nil) and (ResultPortCheck = 0) then
begin
MsgBox('Selected port is in use, choose another one and try again.', mbError, MB_OK)
Result := False;
end;
end;
end;
end;
procedure RunListClickCheck(Sender: TObject);
begin
if not WizardForm.RunList.Checked[0] then
begin
WizardForm.RunList.Checked[1] := False;
WizardForm.RunList.ItemEnabled[1] := False;
end
else WizardForm.RunList.ItemEnabled[1] := True;
end;
procedure CurPageChanged(CurPageID: Integer);
var
i: Integer;
begin
if CurPageID = wpSelectTasks then
// Prevent the user from switching installation type back and forth in admin mode.
// It would cause issues with terminating processes and also with Backrest data being in different user profiles.
if IsAdminInstallMode and AdminInstallationExists then
for i := 1 to 2 do WizardForm.TasksList.ItemEnabled[i] := False
end;
procedure InitializeWizard();
begin
WizardForm.ReadyMemo.ScrollBars := ssVertical;
WizardForm.ReadyMemo.WordWrap := True;
// Uncheck and disable the "open" checkbox when the "start" is unchecked on the Finish page.
if IsUserInstallMode then WizardForm.RunList.OnClickCheck := @RunListClickCheck;
end;
function InitializeSetup(): Boolean;
var
OldUninstaller: String;
I: Integer;
begin
Result := True;
AssignGlobals;
// Check for presence of the old Nullsoft installation.
if RegQueryStringValue(HKEY_CURRENT_USER, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\Backrest',
'UninstallString', OldUninstaller) then
begin
MsgBox('Detected an existing installation done by the old installer that needs to be uninstalled before proceeding. Your configuration will not be impacted.' + Chr(13) + Chr(10) + Chr(13) + Chr(10) +
'Re-run this setup after uninstallation is complete.', mbInformation, MB_OK);
ExecAsOriginalUser(OldUninstaller, '', '', SW_SHOWNORMAL, ewNoWait, I);
Abort;
end;
// Upgrade/reinstall scenarios.
if IsUserInstallMode and UserInstallationExists then ShowUpgradeMsg(AppDirUser, PreviousVersionUser)
else if IsUserInstallMode and AdminInstallationExists then
begin
if GetUserNameString = PreviousAdminUser then
begin
// Prevent installing a user instance when an admin instance already exists under the same user.
MsgBox('Detected an existing administrative installation under the same user ' + PreviousAdminUser +
'. Cannot proceed. Uninstall it and try again. Setup will exit now.', mbError, MB_OK);
Result := False;
end
else begin
// But allow and notify about an existing admin instance under a different user, if present.
MsgBox('Detected an existing administrative installation of Backrest ' + PreviousVersionAdmin + ' under user ' +
PreviousAdminUser + '.' + Chr(13) + Chr(10) + Chr(13) + Chr(10) +
'Setup will install a non-administrative instance for the current user.', mbInformation, MB_OK);
end;
end
// Notify the user about the existing installation of the same type, if present.
else if IsAdminInstallMode and AdminInstallationExists then
begin
ShowUpgradeMsg(AppDirAdmin, PreviousVersionAdmin);
// Warn if attempting to upgrade/reinstall under a different user.
if (Pos('adminstartcurrent', PreviousAdminTasks) > 0) and (GetUserNameString <> PreviousAdminUser) then
MsgBox('Warning! Previous installation of this type was done by user ' + PreviousAdminUser +
'. If you proceed, Backrest will be reinstalled to run under the current user. Configuration will be lost.', mbError, MB_OK);
end;
end;
function InitializeUninstall(): Boolean;
begin
Result := True;
AssignGlobals;
if AdminInstallationExists and UserInstallationExists then
MsgBox('Detected two existing installations of Backrest. Ensure you are running the correct uninstaller. Windows Settings - Apps panel shows only one entry at a time.' + Chr(13) + Chr(10) +
'Use Control Panel - Programs and Features instead to identify the correct uninstaller. Or run unins000.exe directly from the appropriate Backrest directory.', mbInformation, MB_OK);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
BackrestConfig: String;
begin
// Under the system user installation a special profile is used.
if IsAdminInstallMode and AdminInstallationExists and (Pos('adminstartsystem', PreviousAdminTasks) > 0) then
BackrestConfig := 'C:\Windows\system32\config\systemprofile\AppData\Roaming\backrest'
else
BackrestConfig := GetEnv('APPDATA') + '\backrest';
case CurUninstallStep of
usUninstall:
StopBackrest;
usDone:
begin
if MsgBox('Do you want to delete Backrest configuration in this location?' + Chr(13) + Chr(10) + BackrestConfig,
mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
if DelTree(BackrestConfig, True, True, True) then MsgBox('Done!', mbInformation, MB_OK)
else MsgBox('Failed to remove the path', mbInformation, MB_OK);
end;
end;
end;