diff --git a/DnsServerWindowsSetup/DnsServerSetup.iss b/DnsServerWindowsSetup/DnsServerSetup.iss new file mode 100644 index 00000000..ef6879d5 --- /dev/null +++ b/DnsServerWindowsSetup/DnsServerSetup.iss @@ -0,0 +1,113 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Technitium DNS Server" +#define MyAppVersion "6.0" +#define MyAppPublisher "Technitium" +#define MyAppURL "https://technitium.com/dns/" +#define MyAppExeName "DnsServerSystemTrayApp.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{1052DB5E-35BD-4F67-89CD-1F45A1688E77} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +VersionInfoVersion=2.0.0.0 +VersionInfoCopyright="Copyright (C) 2021 Technitium" +DefaultDirName={commonpf32}\Technitium\DNS Server +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +PrivilegesRequired=admin +OutputDir=.\Release +OutputBaseFilename=DnsServerSetup +SetupIconFile=.\logo.ico +WizardSmallImageFile=.\logo.bmp +Compression=lzma +SolidCompression=yes +WizardStyle=modern +UninstallDisplayIcon={app}\{#MyAppExeName} + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; + +[Files] +Source: ".\publish\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: ".\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\DNS Server App"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\Dashboard"; Filename: "http://localhost:5380/" +Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\DNS Server App"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange("DNS Server App", '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +#include "helper.iss" +#include "legacy.iss" +#include "dotnet.iss" +#include "appinstall.iss" + +[Code] +{ + Skips the tasks page if it is an upgrade install +} +function ShouldSkipPage(PageID: Integer): Boolean; +begin + Result := ((PageID = wpSelectTasks) or (PageID = wpSelectDir)) and (IsLegacyInstallerInstalled or IsUpgrade); +end; + +function InitializeSetup: Boolean; +begin + CheckDotnetDependency; + Result := true; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssInstall then begin //Step happens just before installing files + WizardForm.StatusLabel.Caption := 'Stopping Tray App...'; + KillTrayApp(); //Stop the tray app if running + + if IsLegacyInstallerInstalled then + begin + WizardForm.StatusLabel.Caption := 'Stopping Service...'; + DoStopService(); //Stop the service if running + + WizardForm.StatusLabel.Caption := 'Removing Legacy Installer...'; + UninstallLegacyInstaller(); //Uninstall Legacy Installer if Installed already + end else + begin + WizardForm.StatusLabel.Caption := 'Uninstalling Service...'; + DoRemoveService(); //Stop and remove the service if installed + end; + end; + + if CurStep = ssPostInstall then begin //Step happens just after installing files + WizardForm.StatusLabel.Caption := 'Installing Service...'; + DoInstallService(); //Install service after all files installed, if not a portable install + end; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then //Step happens before processing uninstall log + begin + UninstallProgressForm.StatusLabel.Caption := 'Stopping Tray App...'; + KillTrayApp(); //Stop the tray app if running + + UninstallProgressForm.StatusLabel.Caption := 'Uninstalling Service...'; + DoRemoveService(); //Stop and remove the service + end; +end; \ No newline at end of file diff --git a/DnsServerWindowsSetup/appinstall.iss b/DnsServerWindowsSetup/appinstall.iss new file mode 100644 index 00000000..b03847ac --- /dev/null +++ b/DnsServerWindowsSetup/appinstall.iss @@ -0,0 +1,133 @@ + +#include "service.iss" + +#define SERVICE_NAME "DnsService" +#define SERVICE_FILE "DnsService.exe" +#define SERVICE_DISPLAY_NAME "Technitium DNS Server" +#define SERVICE_DESCRIPTION "Technitium DNS Server" +#define TRAYAPP_FILENAME "DnsServerSystemTrayApp.exe" + +[Code] +{ + Kills the tray app +} +procedure KillTrayApp; +begin + TaskKill('{#TRAYAPP_FILENAME}'); +end; + +{ + Stops the service +} +procedure DoStopService(); +var + stopCounter: Integer; +begin + stopCounter := 0; + if IsServiceInstalled('{#SERVICE_NAME}') then begin + Log('Service: Already installed'); + if IsServiceRunning('{#SERVICE_NAME}') then begin + Log('Service: Already running, stopping service...'); + StopService('{#SERVICE_NAME}'); + + while IsServiceRunning('{#SERVICE_NAME}') do + begin + if stopCounter > 2 then begin + Log('Service: Waited too long to stop, killing task...'); + TaskKill('{#SERVICE_FILE}'); + Log('Service: Task killed'); + break; + end else begin + Log('Service: Waiting for stop'); + Sleep(2000); + stopCounter := stopCounter + 1 + end; + end; + if stopCounter < 3 then Log('Service: Stopped'); + end; + end; +end; + +{ + Removes the service from the computer +} +procedure DoRemoveService(); +var + stopCounter: Integer; +begin + stopCounter := 0; + if IsServiceInstalled('{#SERVICE_NAME}') then begin + Log('Service: Already installed, begin remove...'); + if IsServiceRunning('{#SERVICE_NAME}') then begin + Log('Service: Already running, stopping...'); + StopService('{#SERVICE_NAME}'); + while IsServiceRunning('{#SERVICE_NAME}') do + begin + if stopCounter > 2 then begin + Log('Service: Waited too long to stop, killing task...'); + TaskKill('{#SERVICE_FILE}'); + Log('Service: Task killed'); + break; + end else begin + Log('Service: Waiting for stop'); + Sleep(5000); + stopCounter := stopCounter + 1 + end; + end; + end; + + stopCounter := 0; + Log('Service: Removing...'); + RemoveService('{#SERVICE_NAME}'); + while IsServiceInstalled('{#SERVICE_NAME}') do + begin + if stopCounter > 2 then begin + Log('Service: Waited too long to remove, continuing'); + break; + end else begin + Log('Service: Waiting for removal'); + Sleep(5000); + stopCounter := stopCounter + 1 + end; + end; + if stopCounter < 3 then Log('Service: Removed'); + end; +end; + +{ + Installs the service onto the computer +} +procedure DoInstallService(); +var + InstallSuccess: Boolean; + stopCounter: Integer; +begin + stopCounter := 0; + if IsServiceInstalled('{#SERVICE_NAME}') then begin + Log('Service: Already installed, skip install service'); + end else begin + Log('Service: Begin Install'); + InstallSuccess := InstallService(ExpandConstant('{app}\DnsService.exe'), '{#SERVICE_NAME}', '{#SERVICE_DISPLAY_NAME}', '{#SERVICE_DESCRIPTION}', SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START); + if not InstallSuccess then + begin + Log('Service: Install Fail ' + ServiceErrorToMessage(GetLastError())); + SuppressibleMsgBox(ExpandConstant('{cm:ServiceInstallFailure,' + ServiceErrorToMessage(GetLastError()) + '}'), mbCriticalError, MB_OK, IDOK); + end else begin + Log('Service: Install Success, Starting...'); + StartService('{#SERVICE_NAME}'); + + while IsServiceRunning('{#SERVICE_NAME}') <> true do + begin + if stopCounter > 3 then begin + Log('Service: Waited too long to start, continue'); + break; + end else begin + Log('Service: still starting') + Sleep(5000); + stopCounter := stopCounter + 1 + end; + end; + if stopCounter < 4 then Log('Service: Started'); + end; + end; +end; diff --git a/DnsServerWindowsSetup/dotnet.iss b/DnsServerWindowsSetup/dotnet.iss new file mode 100644 index 00000000..39478b2d --- /dev/null +++ b/DnsServerWindowsSetup/dotnet.iss @@ -0,0 +1,332 @@ +[Setup] +MinVersion=6.2 + +// remove next line if you only deploy 32-bit binaries and dependencies +ArchitecturesInstallIn64BitMode=x64 + +// dependency installation requires ready page and ready memo to be enabled (default behaviour) +DisableReadyPage=no +DisableReadyMemo=no + +// shared code for installing the dependencies +[Code] +// types and variables +type + TDependency = record + Filename: String; + Parameters: String; + Title: String; + URL: String; + Checksum: String; + ForceSuccess: Boolean; + InstallClean: Boolean; + RebootAfter: Boolean; + end; + + InstallResult = (InstallSuccessful, InstallRebootRequired, InstallError); + +var + MemoInstallInfo: String; + Dependencies: array of TDependency; + DelayedReboot, ForceX86: Boolean; + DownloadPage: TDownloadWizardPage; + +procedure AddDependency(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, InstallClean, RebootAfter: Boolean); +var + Dependency: TDependency; + I: Integer; +begin + MemoInstallInfo := MemoInstallInfo + #13#10 + '%1' + Title; + + Dependency.Filename := Filename; + Dependency.Parameters := Parameters; + Dependency.Title := Title; + + if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin + Dependency.URL := ''; + end else begin + Dependency.URL := URL; + end; + + Dependency.Checksum := Checksum; + Dependency.ForceSuccess := ForceSuccess; + Dependency.InstallClean := InstallClean; + Dependency.RebootAfter := RebootAfter; + + I := GetArrayLength(Dependencies); + SetArrayLength(Dependencies, I + 1); + Dependencies[I] := Dependency; +end; + +function IsPendingReboot: Boolean; +var + Value: String; +begin + Result := RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager', 'PendingFileRenameOperations', Value) or + (RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager', 'SetupExecute', Value) and (Value <> '')); +end; + +function InstallProducts: InstallResult; +var + ResultCode, I, ProductCount: Integer; +begin + Result := InstallSuccessful; + ProductCount := GetArrayLength(Dependencies); + MemoInstallInfo := SetupMessage(msgReadyMemoTasks); + + if ProductCount > 0 then begin + DownloadPage.Show; + + for I := 0 to ProductCount - 1 do begin + if Dependencies[I].InstallClean and (DelayedReboot or IsPendingReboot) then begin + Result := InstallRebootRequired; + break; + end; + + DownloadPage.SetText(Dependencies[I].Title, ''); + DownloadPage.SetProgress(I + 1, ProductCount); + + while True do begin + ResultCode := 0; + if ShellExec('', ExpandConstant('{tmp}{\}') + Dependencies[I].Filename, Dependencies[I].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin + if Dependencies[I].RebootAfter then begin + // delay reboot after install if we installed the last dependency anyways + if I = ProductCount - 1 then begin + DelayedReboot := True; + end else begin + Result := InstallRebootRequired; + MemoInstallInfo := Dependencies[I].Title; + end; + break; + end else if (ResultCode = 0) or Dependencies[I].ForceSuccess then begin + break; + end else if ResultCode = 3010 then begin + // Windows Installer ResultCode 3010: ERROR_SUCCESS_REBOOT_REQUIRED + DelayedReboot := True; + break; + end; + end; + + case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependencies[I].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of + IDABORT: begin + Result := InstallError; + MemoInstallInfo := MemoInstallInfo + #13#10 + ' ' + Dependencies[I].Title; + break; + end; + IDIGNORE: begin + MemoInstallInfo := MemoInstallInfo + #13#10 + ' ' + Dependencies[I].Title; + break; + end; + end; + end; + + if Result <> InstallSuccessful then begin + break; + end; + end; + + DownloadPage.Hide; + end; +end; + +// Inno Setup event functions +procedure InitializeWizard; +begin + DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; +begin + DelayedReboot := False; + + case InstallProducts of + InstallError: begin + Result := MemoInstallInfo; + end; + InstallRebootRequired: begin + Result := MemoInstallInfo; + NeedsRestart := True; + + // write into the registry that the installer needs to be executed again after restart + RegWriteStringValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InstallBootstrap', ExpandConstant('{srcexe}')); + end; + end; +end; + +function NeedRestart: Boolean; +begin + Result := DelayedReboot; +end; + +function UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; +begin + Result := ''; + if MemoUserInfoInfo <> '' then begin + Result := Result + MemoUserInfoInfo + Newline + NewLine; + end; + if MemoDirInfo <> '' then begin + Result := Result + MemoDirInfo + Newline + NewLine; + end; + if MemoTypeInfo <> '' then begin + Result := Result + MemoTypeInfo + Newline + NewLine; + end; + if MemoComponentsInfo <> '' then begin + Result := Result + MemoComponentsInfo + Newline + NewLine; + end; + if MemoGroupInfo <> '' then begin + Result := Result + MemoGroupInfo + Newline + NewLine; + end; + if MemoTasksInfo <> '' then begin + Result := Result + MemoTasksInfo; + end; + + if MemoInstallInfo <> '' then begin + if MemoTasksInfo = '' then begin + Result := Result + SetupMessage(msgReadyMemoTasks); + end; + Result := Result + FmtMessage(MemoInstallInfo, [Space]); + end; +end; + +function NextButtonClick(const CurPageID: Integer): Boolean; +var + I, ProductCount: Integer; + Retry: Boolean; +begin + Result := True; + + if (CurPageID = wpReady) and (MemoInstallInfo <> '') then begin + DownloadPage.Show; + + ProductCount := GetArrayLength(Dependencies); + for I := 0 to ProductCount - 1 do begin + if Dependencies[I].URL <> '' then begin + DownloadPage.Clear; + DownloadPage.Add(Dependencies[I].URL, Dependencies[I].Filename, Dependencies[I].Checksum); + + Retry := True; + while Retry do begin + Retry := False; + + try + DownloadPage.Download; + except + if GetExceptionMessage = SetupMessage(msgErrorDownloadAborted) then begin + Result := False; + I := ProductCount; + end else begin + case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of + IDABORT: begin + Result := False; + I := ProductCount; + end; + IDRETRY: begin + Retry := True; + end; + end; + end; + end; + end; + end; + end; + + DownloadPage.Hide; + end; +end; + +// architecture helper functions +function IsX64: Boolean; +begin + Result := not ForceX86 and Is64BitInstallMode; +end; + +function GetString(const x86, x64: String): String; +begin + if IsX64 then begin + Result := x64; + end else begin + Result := x86; + end; +end; + +function GetArchitectureSuffix: String; +begin + Result := GetString('', '_x64'); +end; + +function GetArchitectureTitle: String; +begin + Result := GetString(' (x86)', ' (x64)'); +end; + +function CompareVersion(const Version1, Version2: String): Integer; +var + Position, Number1, Number2: Integer; +begin + Result := 0; + while (Version1 <> '') or (Version2 <> '') do begin + Position := Pos('.', Version1); + if Position > 0 then begin + Number1 := StrToIntDef(Copy(Version1, 1, Position - 1), 0); + Delete(Version1, 1, Position); + end else if Version1 <> '' then begin + Number1 := StrToIntDef(Version1, 0); + Version1 := ''; + end else begin + Number1 := 0; + end; + + Position := Pos('.', Version2); + if Position > 0 then begin + Number2 := StrToIntDef(Copy(Version2, 1, Position - 1), 0); + Delete(Version2, 1, Position); + end else if Version2 <> '' then begin + Number2 := StrToIntDef(Version2, 0); + Version2 := ''; + end else begin + Number2 := 0; + end; + + if Number1 < Number2 then begin + Result := -1; + break; + end else if Number1 > Number2 then begin + Result := 1; + break; + end; + end; +end; + + + + + + + + +{ Check if dotnet5 is installed } +function IsDotNet5DesktopInstalled: Boolean; +var + ResultCode: Integer; +begin + Result := false; + Exec('cmd.exe', '/c dotnet --list-runtimes | find /n "Microsoft.WindowsDesktop.App 5."', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + if ResultCode = 0 then + begin + Result := true; + end; +end; + +{ if dotnet5 is not installed then add it for download } +procedure CheckDotnetDependency; +begin + if not IsDotNet5DesktopInstalled then + begin + AddDependency('dotnet50desktop' + GetArchitectureSuffix + '.exe', + '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '.NET Desktop Runtime 5.0.3' + GetArchitectureTitle, + GetString('https://go.microsoft.com/fwlink/?linkid=2155347', 'https://go.microsoft.com/fwlink/?linkid=2155258'), + '', False, False, False); + end; +end; + diff --git a/DnsServerWindowsSetup/helper.iss b/DnsServerWindowsSetup/helper.iss new file mode 100644 index 00000000..c7128450 --- /dev/null +++ b/DnsServerWindowsSetup/helper.iss @@ -0,0 +1,40 @@ +[Code] +{ + Helper functions +} + +{ + Checks to see if the installer is an 'upgrade' +} +function IsUpgrade: Boolean; +var + Value: string; + UninstallKey: string; +begin + UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + + ExpandConstant('{#SetupSetting("AppId")}') + '_is1'; + Result := (RegQueryStringValue(HKLM, UninstallKey, 'UninstallString', Value) or + RegQueryStringValue(HKCU, UninstallKey, 'UninstallString', Value)) and (Value <> ''); +end; + +{ + Kills a running program by its filename +} +procedure TaskKill(fileName: String); +var + ResultCode: Integer; +begin + Exec(ExpandConstant('taskkill.exe'), '/f /im ' + '"' + fileName + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + + +{ + Executes the MSI Uninstall by GUID functionality +} +function MsiExecUnins(appId: String): Integer; +var + ResultCode: Integer; +begin + ShellExec('', 'msiexec.exe', '/x ' + appId + ' /norestart /qb', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Result := ResultCode; +end; diff --git a/DnsServerWindowsSetup/legacy.iss b/DnsServerWindowsSetup/legacy.iss new file mode 100644 index 00000000..347a51a9 --- /dev/null +++ b/DnsServerWindowsSetup/legacy.iss @@ -0,0 +1,36 @@ +#define LEGACY_INSTALLER_APPID "{9B86AC7F-53B3-4E31-B245-D4602D16F5C8}" + +[Code] + +{ + Legacy Installer Functionality +} + +{ + Checks if the MSI Installer is installed +} +function IsLegacyInstallerInstalled: Boolean; +var + Value: string; + UninstallKey1, UninstallKey2: string; +begin + UninstallKey1 := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#LEGACY_INSTALLER_APPID}'; + UninstallKey2 := 'SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{#LEGACY_INSTALLER_APPID}'; + Result := ( + RegQueryStringValue(HKLM, UninstallKey1, 'UninstallString', Value) or + RegQueryStringValue(HKCU, UninstallKey1, 'UninstallString', Value) or + RegQueryStringValue(HKLM, UninstallKey2, 'UninstallString', Value) + ) and (Value <> ''); +end; + +{ + Uninstalls Legacy Installer +} +procedure UninstallLegacyInstaller; +var + ResultCode: Integer; +begin + Log('Uninstall MSI installer item'); + ResultCode := MsiExecUnins('{#LEGACY_INSTALLER_APPID}'); + Log('Result code ' + IntToStr(ResultCode)); +end; diff --git a/DnsServiceSetup/Windows/logo.bmp b/DnsServerWindowsSetup/logo.bmp similarity index 100% rename from DnsServiceSetup/Windows/logo.bmp rename to DnsServerWindowsSetup/logo.bmp diff --git a/DnsServiceSetup/Windows/logo.ico b/DnsServerWindowsSetup/logo.ico similarity index 100% rename from DnsServiceSetup/Windows/logo.ico rename to DnsServerWindowsSetup/logo.ico diff --git a/DnsServiceSetup/Windows/service.pas b/DnsServerWindowsSetup/service.iss similarity index 99% rename from DnsServiceSetup/Windows/service.pas rename to DnsServerWindowsSetup/service.iss index 754a2858..d0ab4850 100644 --- a/DnsServiceSetup/Windows/service.pas +++ b/DnsServerWindowsSetup/service.iss @@ -1,3 +1,5 @@ +[Code] + type SERVICE_STATUS = record dwServiceType : cardinal;