#define MyAppName "Inventera" #define MyAppVersion "0.1.0-beta" #define MyAppPublisher "Inventera" #define MyAppExeName "Inventera.Server.exe" #define PostgreSqlMajor "18" #define PostgreSqlInstaller "installer\prerequisites\postgesql.exe" [Setup] AppId={{78BAF0DB-D841-49A7-954C-16DCA688B136} AppName={#MyAppName} AppVersion={#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} DefaultDirName={autopf}\{#MyAppName} DefaultGroupName={#MyAppName} PrivilegesRequired=admin OutputDir=dist OutputBaseFilename=Inventera SetupIconFile=picture\logo\main_logo.ico UninstallDisplayIcon={app}\{#MyAppExeName} Compression=lzma2/ultra64 SolidCompression=yes WizardStyle=modern ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible CloseApplications=yes RestartApplications=no DisableProgramGroupPage=yes UsePreviousAppDir=yes [Languages] Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" [Files] Source: "{#PostgreSqlInstaller}"; Flags: dontcopy noencryption nocompression Source: "installer\initialize_inventera_database.ps1"; Flags: dontcopy noencryption Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{group}\Inventera"; Filename: "http://127.0.0.1:{code:GetApplicationPort}"; IconFilename: "{app}\{#MyAppExeName}" Name: "{commondesktop}\Inventera"; Filename: "http://127.0.0.1:{code:GetApplicationPort}"; IconFilename: "{app}\{#MyAppExeName}" Name: "{commonstartup}\Inventera Server"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; IconFilename: "{app}\{#MyAppExeName}" [Run] Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""Inventera Web"""; Flags: runhidden waituntilterminated Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""Inventera Web"" dir=in action=allow protocol=TCP localport={code:GetApplicationPort}"; Flags: runhidden waituntilterminated Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Flags: nowait runhidden; StatusMsg: "Запуск сервера Inventera..." Filename: "http://127.0.0.1:{code:GetApplicationPort}"; Description: "Открыть Inventera"; Flags: shellexec nowait postinstall skipifsilent [UninstallRun] Filename: "{sys}\taskkill.exe"; Parameters: "/F /IM ""{#MyAppExeName}"""; Flags: runhidden waituntilterminated; RunOnceId: "StopInventera" Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""Inventera Web"""; Flags: runhidden waituntilterminated; RunOnceId: "RemoveInventeraFirewallRule" [Code] var ModePage: TInputOptionWizardPage; PostgreSqlInstallPage: TInputOptionWizardPage; AdminPage: TInputQueryWizardPage; PortsPage: TInputQueryWizardPage; DatabasePage: TInputQueryWizardPage; ExistingPsqlPage: TInputFileWizardPage; AutomaticAdminPassword: String; function IsAutomaticMode: Boolean; begin Result := ModePage.SelectedValueIndex = 0; end; function ShouldInstallPostgreSql: Boolean; begin Result := IsAutomaticMode or (PostgreSqlInstallPage.SelectedValueIndex = 0); end; function GetPostgreSqlPort(Param: String): String; begin Result := Trim(PortsPage.Values[0]); end; function GetApplicationPort(Param: String): String; begin Result := Trim(PortsPage.Values[1]); end; function GetDatabaseName: String; begin if IsAutomaticMode then Result := 'Inv_Web' else Result := Trim(DatabasePage.Values[0]); end; function GetDatabaseUser: String; begin if IsAutomaticMode then Result := 'invwebuser' else Result := Trim(DatabasePage.Values[1]); end; function GetDatabasePassword: String; begin if IsAutomaticMode then Result := 'invwebpassword' else Result := DatabasePage.Values[2]; end; function GetAdminUser: String; begin if IsAutomaticMode then Result := 'postgres' else Result := Trim(AdminPage.Values[0]); end; function GetAdminPassword: String; begin if IsAutomaticMode then Result := AutomaticAdminPassword else Result := AdminPage.Values[1]; end; function QuoteArgument(const Value: String): String; begin Result := '"' + Value + '"'; end; function GenerateAutomaticPassword: String; const Characters = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#_-'; var I: Integer; begin Result := 'Iv!'; for I := 1 to 29 do Result := Result + Characters[Random(Length(Characters)) + 1]; end; function FindExistingPsql: String; var Candidate: String; begin Result := ''; Candidate := ExpandConstant('{autopf}\PostgreSQL\18\bin\psql.exe'); if FileExists(Candidate) then begin Result := Candidate; Exit; end; Candidate := ExpandConstant('{autopf}\PostgreSQL\17\bin\psql.exe'); if FileExists(Candidate) then begin Result := Candidate; Exit; end; Candidate := ExpandConstant('{autopf}\PostgreSQL\16\bin\psql.exe'); if FileExists(Candidate) then begin Result := Candidate; Exit; end; Candidate := ExpandConstant('{autopf}\PostgreSQL\15\bin\psql.exe'); if FileExists(Candidate) then begin Result := Candidate; Exit; end; Candidate := ExpandConstant('{autopf}\PostgreSQL\14\bin\psql.exe'); if FileExists(Candidate) then begin Result := Candidate; Exit; end; Candidate := ExpandConstant('{autopf}\PostgreSQL\13\bin\psql.exe'); if FileExists(Candidate) then begin Result := Candidate; Exit; end; end; function GetPsqlPath: String; begin if ShouldInstallPostgreSql then Result := ExpandConstant('{autopf}\PostgreSQL\{#PostgreSqlMajor}\bin\psql.exe') else Result := Trim(ExistingPsqlPage.Values[0]); end; function IsValidPort(const Value: String; var PortNumber: Integer): Boolean; begin Result := TryStrToInt(Trim(Value), PortNumber) and (PortNumber >= 1) and (PortNumber <= 65535); end; function IsValidIdentifier(const Value: String): Boolean; var I: Integer; AllowedFirst: String; AllowedRest: String; begin Result := False; if Value = '' then Exit; AllowedFirst := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'; AllowedRest := AllowedFirst + '0123456789'; if Pos(Value[1], AllowedFirst) = 0 then Exit; for I := 2 to Length(Value) do if Pos(Value[I], AllowedRest) = 0 then Exit; Result := True; end; function IsSafePassword(const Value: String): Boolean; begin Result := (Value <> '') and (Pos('"', Value) = 0) and (Pos(#13, Value) = 0) and (Pos(#10, Value) = 0); end; function IsTcpPortAvailable(PortNumber: Integer): Boolean; var PowerShellPath: String; Parameters: String; ExitCode: Integer; begin PowerShellPath := ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe'); Parameters := '-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ' + '"$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, ' + IntToStr(PortNumber) + '); try { $listener.Start(); $listener.Stop(); exit 0 } catch { exit 1 }"'; Result := Exec( PowerShellPath, Parameters, '', SW_HIDE, ewWaitUntilTerminated, ExitCode ) and (ExitCode = 0); end; procedure InitializeWizard; begin AutomaticAdminPassword := GenerateAutomaticPassword; ModePage := CreateInputOptionPage( wpWelcome, 'Режим установки', 'Выберите способ настройки Inventera', 'Автоматический режим использует готовые параметры базы. Ручной режим позволяет изменить их.', True, False ); ModePage.Add('Автоматический'); ModePage.Add('Ручной'); ModePage.SelectedValueIndex := 0; PostgreSqlInstallPage := CreateInputOptionPage( ModePage.ID, 'Установка PostgreSQL', 'Нужно ли установить PostgreSQL?', 'Выберите «Нет», только если PostgreSQL уже установлен и доступен на этом компьютере.', True, False ); PostgreSqlInstallPage.Add('Да, установить PostgreSQL из комплекта Inventera'); PostgreSqlInstallPage.Add('Нет, использовать существующий PostgreSQL'); PostgreSqlInstallPage.SelectedValueIndex := 0; AdminPage := CreateInputQueryPage( PostgreSqlInstallPage.ID, 'Администратор PostgreSQL', 'Укажите учётную запись администратора', 'Эта учётная запись нужна только для создания базы и пользователя Inventera.' ); AdminPage.Add('Логин администратора PostgreSQL:', False); AdminPage.Add('Пароль администратора PostgreSQL:', True); AdminPage.Values[0] := 'postgres'; PortsPage := CreateInputQueryPage( AdminPage.ID, 'Сетевые порты', 'Укажите порты PostgreSQL и Inventera', 'PostgreSQL и веб-приложение используют разные порты. Установщик проверит, что нужные порты свободны.' ); PortsPage.Add('Порт PostgreSQL:', False); PortsPage.Add('Порт веб-приложения Inventera:', False); PortsPage.Values[0] := '5432'; PortsPage.Values[1] := '5050'; DatabasePage := CreateInputQueryPage( PortsPage.ID, 'База данных Inventera', 'Укажите имя базы и учётную запись приложения', 'Предлагаемые значения уже заполнены и могут быть изменены.' ); DatabasePage.Add('Имя базы данных:', False); DatabasePage.Add('Логин пользователя базы:', False); DatabasePage.Add('Пароль пользователя базы:', True); DatabasePage.Values[0] := 'Inv_Web'; DatabasePage.Values[1] := 'invwebuser'; DatabasePage.Values[2] := 'invwebpassword'; ExistingPsqlPage := CreateInputFilePage( DatabasePage.ID, 'Расположение PostgreSQL', 'Укажите psql.exe существующей установки', 'Файл psql.exe обычно находится в папке C:\Program Files\PostgreSQL\<версия>\bin.' ); ExistingPsqlPage.Add('Путь к psql.exe:', 'Исполняемые файлы|*.exe|Все файлы|*.*', '.exe'); ExistingPsqlPage.Values[0] := FindExistingPsql; end; function ShouldSkipPage(PageID: Integer): Boolean; begin Result := False; if IsAutomaticMode and ((PageID = PostgreSqlInstallPage.ID) or (PageID = AdminPage.ID) or (PageID = DatabasePage.ID) or (PageID = ExistingPsqlPage.ID)) then begin Result := True; Exit; end; if (not IsAutomaticMode) and (PageID = ExistingPsqlPage.ID) and ShouldInstallPostgreSql then begin Result := True; end; end; function NextButtonClick(CurPageID: Integer): Boolean; var PostgreSqlPort: Integer; ApplicationPort: Integer; begin Result := False; if CurPageID = AdminPage.ID then begin if not IsValidIdentifier(Trim(AdminPage.Values[0])) then begin MsgBox('Введите корректный логин администратора PostgreSQL.', mbError, MB_OK); Exit; end; if not IsSafePassword(AdminPage.Values[1]) then begin MsgBox('Введите пароль администратора. Двойные кавычки и переносы строк недопустимы.', mbError, MB_OK); Exit; end; end; if CurPageID = PortsPage.ID then begin if not IsValidPort(PortsPage.Values[0], PostgreSqlPort) then begin MsgBox('Порт PostgreSQL должен быть числом от 1 до 65535.', mbError, MB_OK); Exit; end; if not IsValidPort(PortsPage.Values[1], ApplicationPort) then begin MsgBox('Порт Inventera должен быть числом от 1 до 65535.', mbError, MB_OK); Exit; end; if PostgreSqlPort = ApplicationPort then begin MsgBox('PostgreSQL и Inventera не могут использовать один и тот же порт.', mbError, MB_OK); Exit; end; if ShouldInstallPostgreSql and (not IsTcpPortAvailable(PostgreSqlPort)) then begin MsgBox( 'Порт PostgreSQL ' + IntToStr(PostgreSqlPort) + ' уже занят. Укажите другой порт.', mbError, MB_OK ); Exit; end; if not IsTcpPortAvailable(ApplicationPort) then begin MsgBox( 'Порт Inventera ' + IntToStr(ApplicationPort) + ' уже занят. Укажите другой порт.', mbError, MB_OK ); Exit; end; end; if CurPageID = DatabasePage.ID then begin if not IsValidIdentifier(Trim(DatabasePage.Values[0])) then begin MsgBox('Имя базы может содержать только латинские буквы, цифры и знак подчёркивания.', mbError, MB_OK); Exit; end; if not IsValidIdentifier(Trim(DatabasePage.Values[1])) then begin MsgBox('Логин базы может содержать только латинские буквы, цифры и знак подчёркивания.', mbError, MB_OK); Exit; end; if not IsSafePassword(DatabasePage.Values[2]) then begin MsgBox('Введите пароль базы. Двойные кавычки и переносы строк недопустимы.', mbError, MB_OK); Exit; end; end; if CurPageID = ExistingPsqlPage.ID then begin if not FileExists(Trim(ExistingPsqlPage.Values[0])) then begin MsgBox('Укажите существующий файл psql.exe.', mbError, MB_OK); Exit; end; end; Result := True; end; function PostgreSqlInstallParameters: String; var Prefix: String; begin Prefix := ExpandConstant('{autopf}\PostgreSQL\{#PostgreSqlMajor}'); Result := '--mode unattended --unattendedmodeui minimal ' + '--enable-components server,commandlinetools ' + '--prefix ' + QuoteArgument(Prefix) + ' ' + '--datadir ' + QuoteArgument(Prefix + '\data') + ' ' + '--serverport ' + GetPostgreSqlPort('') + ' ' + '--servicename InventeraPostgreSQL{#PostgreSqlMajor} ' + '--superaccount ' + QuoteArgument(GetAdminUser) + ' ' + '--superpassword ' + QuoteArgument(GetAdminPassword) + ' ' + '--servicepassword ' + QuoteArgument(GetAdminPassword) + ' ' + '--create_shortcuts 0'; end; function DatabaseInitializationParameters: String; begin Result := '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File ' + QuoteArgument(ExpandConstant('{tmp}\initialize_inventera_database.ps1')) + ' ' + '-PsqlPath ' + QuoteArgument(GetPsqlPath) + ' ' + '-AdminUser ' + QuoteArgument(GetAdminUser) + ' ' + '-AdminPassword ' + QuoteArgument(GetAdminPassword) + ' ' + '-PostgreSqlPort ' + GetPostgreSqlPort('') + ' ' + '-DatabaseName ' + QuoteArgument(GetDatabaseName) + ' ' + '-DatabaseUser ' + QuoteArgument(GetDatabaseUser) + ' ' + '-DatabasePassword ' + QuoteArgument(GetDatabasePassword) + ' ' + '-ApplicationPort ' + GetApplicationPort('') + ' ' + '-ConfigPath ' + QuoteArgument(ExpandConstant('{commonappdata}\Inventera\config.json')); end; function PrepareToInstall(var NeedsRestart: Boolean): String; var ExitCode: Integer; IgnoreExitCode: Integer; Success: Boolean; PowerShellPath: String; PostgreSqlPort: Integer; ApplicationPort: Integer; begin Result := ''; NeedsRestart := False; Exec( ExpandConstant('{sys}\taskkill.exe'), '/F /IM "{#MyAppExeName}"', '', SW_HIDE, ewWaitUntilTerminated, IgnoreExitCode ); if not IsValidPort(GetPostgreSqlPort(''), PostgreSqlPort) or not IsValidPort(GetApplicationPort(''), ApplicationPort) then begin Result := 'Указаны некорректные сетевые порты.'; Exit; end; if ShouldInstallPostgreSql and (not IsTcpPortAvailable(PostgreSqlPort)) then begin Result := 'Порт PostgreSQL ' + IntToStr(PostgreSqlPort) + ' уже занят.'; Exit; end; if not IsTcpPortAvailable(ApplicationPort) then begin Result := 'Порт Inventera ' + IntToStr(ApplicationPort) + ' уже занят.'; Exit; end; if ShouldInstallPostgreSql then begin WizardForm.PreparingLabel.Caption := 'Установка PostgreSQL. Это может занять несколько минут, пожалуйста, подождите...'; ExtractTemporaryFile(ExtractFileName(ExpandConstant('{#PostgreSqlInstaller}'))); Success := Exec( ExpandConstant('{tmp}\postgesql.exe'), PostgreSqlInstallParameters, '', SW_SHOW, ewWaitUntilTerminated, ExitCode ); if (not Success) or (ExitCode <> 0) then begin Result := 'Не удалось установить PostgreSQL. Код ошибки: ' + IntToStr(ExitCode); Exit; end; end; WizardForm.PreparingLabel.Caption := 'Создание базы данных и сохранение настроек Inventera...'; ExtractTemporaryFile('initialize_inventera_database.ps1'); PowerShellPath := ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe'); Success := Exec( PowerShellPath, DatabaseInitializationParameters, '', SW_HIDE, ewWaitUntilTerminated, ExitCode ); if (not Success) or (ExitCode <> 0) then begin Result := 'Не удалось создать базу данных или сохранить настройки Inventera. Код ошибки: ' + IntToStr(ExitCode); Exit; end; end;