mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 18:30:47 +08:00
1375 lines
48 KiB
Text
1375 lines
48 KiB
Text
; NetX Windows installer (Inno Setup 6+)
|
||
; Compile after: packaging\build_release.ps1
|
||
; ISCC.exe packaging\installer\netx.iss
|
||
; Encoding: UTF-8 with BOM (required for Chinese CustomMessages)
|
||
|
||
#define MyAppName "NetX"
|
||
#ifndef MyAppVersion
|
||
#define MyAppVersion "0.4.19"
|
||
#endif
|
||
#define MyAppPublisher "NetX"
|
||
#define MyAppURL "https://github.com/hansjone/netx"
|
||
#define MyAppExeName "packaging\start_netx_app.ps1"
|
||
|
||
; Stage directory produced by build_release.ps1 (relative to this .iss)
|
||
#define StageDir "..\release\netx-win64"
|
||
#define IconFile "..\assets\netx.ico"
|
||
#define AssetsDir "..\assets"
|
||
|
||
[Setup]
|
||
AppId={{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}
|
||
AppName={#MyAppName}
|
||
AppVersion={#MyAppVersion}
|
||
AppVerName={#MyAppName} {#MyAppVersion}
|
||
VersionInfoVersion={#MyAppVersion}
|
||
VersionInfoProductName={#MyAppName}
|
||
UninstallDisplayName={#MyAppName} {#MyAppVersion}
|
||
AppPublisher={#MyAppPublisher}
|
||
AppPublisherURL={#MyAppURL}
|
||
DefaultDirName={autopf}\NetX
|
||
DefaultGroupName=NetX
|
||
DisableProgramGroupPage=yes
|
||
LicenseFile={#StageDir}\LICENSE
|
||
OutputDir=..\release
|
||
#ifndef OutputBaseFilename
|
||
#define OutputBaseFilename "NetX-Setup-" + MyAppVersion
|
||
#endif
|
||
OutputBaseFilename={#OutputBaseFilename}
|
||
SetupIconFile={#IconFile}
|
||
UninstallDisplayIcon={app}\packaging\assets\netx.ico
|
||
Compression=lzma2
|
||
SolidCompression=yes
|
||
; Follows the Windows light/dark setting; windows11 = flat Fluent-style controls.
|
||
WizardStyle=modern dynamic windows11 hidebevels
|
||
WizardImageFile={#AssetsDir}\wizard-100.png,{#AssetsDir}\wizard-125.png,{#AssetsDir}\wizard-150.png,{#AssetsDir}\wizard-200.png
|
||
WizardSmallImageFile={#AssetsDir}\wizard-small-100.png,{#AssetsDir}\wizard-small-125.png,{#AssetsDir}\wizard-small-150.png,{#AssetsDir}\wizard-small-200.png
|
||
WizardImageBackColor=#0a1222
|
||
ArchitecturesAllowed=x64compatible
|
||
ArchitecturesInstallIn64BitMode=x64compatible
|
||
PrivilegesRequired=admin
|
||
; Data lives under {commonappdata}\NetX - not overwritten by upgrades.
|
||
; Do NOT use CloseApplications=yes — it can freeze/beep on the Tasks/Ready
|
||
; pages while scanning locked NetX tray/service processes with no visible dialog.
|
||
CloseApplications=no
|
||
; Pick from the Windows UI language; no extra dialog.
|
||
ShowLanguageDialog=auto
|
||
|
||
[Languages]
|
||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||
Name: "chinesesimplified"; MessagesFile: "Languages\ChineseSimplified.isl"
|
||
|
||
[CustomMessages]
|
||
english.CreateDesktopIcon=Create a desktop shortcut
|
||
english.SetupOptions=After install:
|
||
english.StartTrayNow=Start NetX now
|
||
english.RunModeGroup=How NetX runs:
|
||
english.RunModeTray=Desktop tray (recommended) - control NetX from the notification area
|
||
english.EnableAutostart=Start automatically when I sign in to Windows
|
||
english.RunModeService=Windows service - runs at boot without anyone signed in (servers)
|
||
english.MaintenanceGroup=Maintenance:
|
||
english.EnableAutoUpdate=Install updates automatically every day (needs network)
|
||
english.ServiceInstallFailed=NetX was installed, but the Windows service could not be registered (exit %1).%n%nYou can still start NetX from the Start Menu. Log: %2
|
||
english.IconStart=Start NetX
|
||
english.IconStop=Stop NetX
|
||
english.IconUpdate=Check for updates
|
||
english.UninstallDeleteData=Also delete NetX data (%ProgramData%\NetX)?%n%nYes = remove database, settings, and secrets%nNo = keep data for a later reinstall
|
||
english.DbPageCaption=Database
|
||
english.DbPageDescription=Choose built-in or external PostgreSQL. Optional credential key: paste from an existing install, or leave empty to auto-generate.
|
||
english.DbBundled=Built-in PostgreSQL (portable, offline)
|
||
english.DbBundledPort=Port
|
||
english.DbBundledPortInvalid=The built-in PostgreSQL port must be a number between 1024 and 65535.
|
||
english.DbBundledPortInUse=Port %1 is already in use on this computer.%n%nChoose another port for the built-in PostgreSQL.
|
||
english.DbExternal=External PostgreSQL (existing server)
|
||
english.DbHost=Host
|
||
english.DbPort=Port
|
||
english.DbUser=User
|
||
english.DbPassword=Password
|
||
english.DbName=Database
|
||
english.CredKey=Credential key
|
||
english.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY (optional — empty = auto-generate)
|
||
english.DbFieldsRequired=Please fill in host, port, user, password, and database name.
|
||
english.DbTesting=Testing PostgreSQL connection, please wait…
|
||
english.DbTestFailed=Cannot connect to PostgreSQL with these settings.%n%n%1%n%nFix the settings and try again.
|
||
english.DbTestExtractFailed=Could not extract PostgreSQL client tools for connection test.
|
||
english.DbSilentExternalMissing=Silent install with external DB requires /DbHost /DbUser /DbPassword /DbName (optional /DbPort).
|
||
english.DbApplyFailed=Database configuration failed after file install (exit %1).%n%nLog: %2%n%nFix the problem, then use Start Menu → Reconfigure database — or reinstall.
|
||
english.DbApplyExecFailed=Could not start database configuration script.
|
||
english.DbBundledMissing=Built-in PostgreSQL files are missing from the install folder:%n%1%n%nThe Setup package is incomplete. Re-download NetX-Setup and install again.
|
||
english.DbEnvMissing=Database configuration did not write %ProgramData%\NetX\.env.
|
||
english.ReconfigureDb=Reconfigure database
|
||
english.UpgradeKeepDb=Existing NetX data found - database settings will be kept (no wizard).
|
||
english.ModePageCaption=Install mode
|
||
english.ModePageDescription=An existing NetX installation was found. Choose update or reinstall.
|
||
english.ModeUpdate=Update existing installation (keep database settings)
|
||
english.ModeReinstall=Reinstall and reconfigure database (same as first install)
|
||
english.ModeUpdateHint=Replaces program files only. ProgramData\.env and database data are kept.
|
||
english.ModeReinstallHint=Shows the Database page again. Does not delete ProgramData unless you uninstall and choose to.
|
||
chinesesimplified.CreateDesktopIcon=创建桌面快捷方式
|
||
chinesesimplified.SetupOptions=安装完成后:
|
||
chinesesimplified.StartTrayNow=立即启动 NetX
|
||
chinesesimplified.RunModeGroup=运行方式:
|
||
chinesesimplified.RunModeTray=桌面托盘(推荐)— 在任务栏通知区域控制 NetX
|
||
chinesesimplified.EnableAutostart=登录 Windows 时自动启动
|
||
chinesesimplified.RunModeService=Windows 服务 — 开机即运行,无需登录(适合服务器)
|
||
chinesesimplified.MaintenanceGroup=维护:
|
||
chinesesimplified.EnableAutoUpdate=每天自动安装更新(需要联网)
|
||
chinesesimplified.ServiceInstallFailed=NetX 已安装,但 Windows 服务注册失败(退出码 %1)。%n%n仍可从开始菜单启动 NetX。日志:%2
|
||
chinesesimplified.IconStart=启动 NetX
|
||
chinesesimplified.IconStop=停止 NetX
|
||
chinesesimplified.IconUpdate=检查更新
|
||
chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装
|
||
chinesesimplified.DbPageCaption=数据库
|
||
chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。
|
||
chinesesimplified.DbBundled=内置 PostgreSQL(便携,可离线)
|
||
chinesesimplified.DbBundledPort=端口
|
||
chinesesimplified.DbBundledPortInvalid=内置 PostgreSQL 端口必须是 1024–65535 之间的数字。
|
||
chinesesimplified.DbBundledPortInUse=本机端口 %1 已被占用。%n%n请为内置 PostgreSQL 换一个端口。
|
||
chinesesimplified.DbExternal=外置 PostgreSQL(已有服务器)
|
||
chinesesimplified.DbHost=主机
|
||
chinesesimplified.DbPort=端口
|
||
chinesesimplified.DbUser=用户
|
||
chinesesimplified.DbPassword=密码
|
||
chinesesimplified.DbName=数据库名
|
||
chinesesimplified.CredKey=凭据密钥
|
||
chinesesimplified.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY(可选,留空=自动生成)
|
||
chinesesimplified.DbFieldsRequired=请填写主机、端口、用户、密码和数据库名。
|
||
chinesesimplified.DbTesting=正在测试 PostgreSQL 连接,请稍候…
|
||
chinesesimplified.DbTestFailed=无法用当前设置连接 PostgreSQL。%n%n%1%n%n请改正后重试。
|
||
chinesesimplified.DbTestExtractFailed=无法解压 PostgreSQL 客户端以测试连接。
|
||
chinesesimplified.DbSilentExternalMissing=静默安装外置库需要参数 /DbHost /DbUser /DbPassword /DbName(可选 /DbPort)。
|
||
chinesesimplified.DbApplyFailed=文件安装后数据库自动配置失败(退出码 %1)。%n%n日志: %2%n%n请处理后使用开始菜单 → 重新配置数据库,或重新安装。
|
||
chinesesimplified.DbApplyExecFailed=无法启动数据库配置脚本。
|
||
chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件:%n%1%n%n安装包不完整,请重新下载 NetX-Setup 后再装。
|
||
chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。
|
||
chinesesimplified.ReconfigureDb=重新配置数据库
|
||
chinesesimplified.UpgradeKeepDb=检测到已有 NetX 数据 - 将保留数据库配置(跳过向导)。
|
||
chinesesimplified.ModePageCaption=安装模式
|
||
chinesesimplified.ModePageDescription=检测到已有 NetX 安装。请选择更新或重装。
|
||
chinesesimplified.ModeUpdate=更新现有安装(保留数据库配置)
|
||
chinesesimplified.ModeReinstall=重装并重新配置数据库(与首次安装相同)
|
||
chinesesimplified.ModeUpdateHint=仅替换程序文件,保留 ProgramData\.env 与数据库数据。
|
||
chinesesimplified.ModeReinstallHint=再次显示数据库页。不会删除 ProgramData(除非卸载时选择删除)。
|
||
|
||
[Tasks]
|
||
; Run mode is exclusive: tray + service together would race for the same port.
|
||
; On upgrades CurPageChanged pre-selects whatever is actually installed.
|
||
Name: "runtray"; Description: "{cm:RunModeTray}"; GroupDescription: "{cm:RunModeGroup}"; Flags: exclusive
|
||
Name: "runtray\autostart"; Description: "{cm:EnableAutostart}"
|
||
Name: "runservice"; Description: "{cm:RunModeService}"; GroupDescription: "{cm:RunModeGroup}"; Flags: exclusive unchecked
|
||
Name: "autoupdate"; Description: "{cm:EnableAutoUpdate}"; GroupDescription: "{cm:MaintenanceGroup}"; Flags: unchecked
|
||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||
|
||
[Files]
|
||
; Entire release stage -> {app}. Exclude .portable so installed builds use ProgramData.
|
||
Source: "{#StageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: ".portable"
|
||
; Connection-test tools (wizard Next / silent PrepareToInstall only)
|
||
Source: "test_pg_conn.ps1"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\psql.exe"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libpq.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libssl-3-x64.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libcrypto-3-x64.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libintl-9.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libiconv-2.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libwinpthread-1.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\zlib1.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\liblz4.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libzstd.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libxml2.dll"; Flags: dontcopy
|
||
Source: "{#StageDir}\postgres\pgsql\bin\libxslt.dll"; Flags: dontcopy
|
||
; VC++ runtime deployed app-local by build_release.ps1 (clean PCs lack the redistributable).
|
||
Source: "{#StageDir}\postgres\pgsql\bin\vcruntime140.dll"; Flags: dontcopy
|
||
; These helpers are extracted for PrepareToInstall, before the new program tree is copied.
|
||
Source: "{#StageDir}\packaging\_common.ps1"; Flags: dontcopy
|
||
Source: "{#StageDir}\packaging\service_lifecycle.ps1"; Flags: dontcopy
|
||
|
||
[Dirs]
|
||
; users-modify so tray / non-admin reconfigure can update .env and runtime files.
|
||
Name: "{commonappdata}\NetX"; Permissions: users-modify
|
||
Name: "{commonappdata}\NetX\data"; Permissions: users-modify
|
||
Name: "{commonappdata}\NetX\data\auth"; Permissions: users-modify
|
||
Name: "{commonappdata}\NetX\data\runtime"; Permissions: users-modify
|
||
Name: "{commonappdata}\NetX\pgdata"; Permissions: users-modify
|
||
Name: "{commonappdata}\NetX\backups"; Permissions: users-modify
|
||
|
||
[InstallDelete]
|
||
; Python minor releases can change stdlib layout (for example pathlib.py -> pathlib\\).
|
||
; Remove the previous runtime and venv before copying the new ones so an upgrade
|
||
; cannot leave orphaned modules that shadow files from the shipped Python version.
|
||
Type: filesandordirs; Name: "{app}\python\runtime"
|
||
Type: filesandordirs; Name: "{app}\.venv"
|
||
; Remove legacy desktop shortcuts from older Setup builds (keep a single NetX icon).
|
||
Type: files; Name: "{commondesktop}\NetX Start.lnk"
|
||
Type: files; Name: "{commondesktop}\NetX First-time setup.lnk"
|
||
Type: files; Name: "{commondesktop}\首次配置(内置或外置数据库).lnk"
|
||
; Start Menu entries from <= 0.4.16 (English-only names; toggles moved into the tray / Tasks page).
|
||
Type: files; Name: "{group}\NetX Tray.lnk"
|
||
Type: files; Name: "{group}\Start NetX.lnk"
|
||
Type: files; Name: "{group}\Stop NetX.lnk"
|
||
Type: files; Name: "{group}\Check for updates.lnk"
|
||
Type: files; Name: "{group}\Open NetX UI.url"
|
||
Type: files; Name: "{group}\Install Windows Service (admin).lnk"
|
||
Type: files; Name: "{group}\Enable silent auto-update (daily).lnk"
|
||
Type: files; Name: "{group}\Enable start at logon.lnk"
|
||
|
||
[Icons]
|
||
; Prefer .cmd targets so Server 2012 / classic Start Menu shows normal program entries.
|
||
; Autostart / auto-update / service toggles live in the tray Settings menu and the Tasks page.
|
||
Name: "{group}\NetX"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
|
||
Name: "{group}\{cm:IconStart}"; Filename: "{app}\NetX-Start.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
|
||
Name: "{group}\{cm:IconStop}"; Filename: "{app}\NetX-Stop.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
|
||
Name: "{group}\{cm:IconUpdate}"; Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\launch_shortcut.ps1"" -Action update -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
|
||
Name: "{group}\{cm:ReconfigureDb}"; Filename: "{app}\NetX-FirstRun.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"
|
||
; One desktop shortcut only (tray = start + tray UI).
|
||
Name: "{commondesktop}\NetX"; Filename: "{app}\NetX-Tray.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\packaging\assets\netx.ico"; Tasks: desktopicon
|
||
|
||
[Run]
|
||
; Autostart / service / auto-update are applied from the Tasks page in CurStepChanged.
|
||
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\netx_tray.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"" -StartOnLaunch"; WorkingDir: "{app}"; Description: "{cm:StartTrayNow}"; Flags: postinstall runhidden nowait skipifsilent; Check: NetxShouldStartTray
|
||
|
||
; Stop runtime BEFORE files are deleted. Do not also Exec prepare from
|
||
; InitializeUninstall (that previously raced/killed unins000 or hung the UI).
|
||
[UninstallRun]
|
||
Filename: "powershell.exe"; Parameters: "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{app}\packaging\uninstall_prepare.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; RunOnceId: "StopNetX"; Flags: runhidden waituntilterminated
|
||
|
||
[UninstallDelete]
|
||
; Program files always removed. Data dir only when user chose Yes in InitializeUninstall.
|
||
Type: filesandordirs; Name: "{app}"
|
||
|
||
[Code]
|
||
var
|
||
GDeleteNetxData: Boolean;
|
||
GTempDeleteDataScript: String;
|
||
DbPage: TWizardPage;
|
||
RbBundled: TNewRadioButton;
|
||
RbExternal: TNewRadioButton;
|
||
LblBundledPort: TNewStaticText;
|
||
EdBundledPort: TNewEdit;
|
||
GBundledPort: String;
|
||
LblHost: TNewStaticText;
|
||
LblPort: TNewStaticText;
|
||
LblUser: TNewStaticText;
|
||
LblPassword: TNewStaticText;
|
||
LblDbName: TNewStaticText;
|
||
EdHost: TNewEdit;
|
||
EdPort: TNewEdit;
|
||
EdUser: TNewEdit;
|
||
EdPassword: TNewEdit;
|
||
EdDbName: TNewEdit;
|
||
LblCredKey: TNewStaticText;
|
||
LblCredHint: TNewStaticText;
|
||
EdCredKey: TNewEdit;
|
||
GDbMode: String;
|
||
GDbUrl: String;
|
||
GDbHost: String;
|
||
GDbPort: String;
|
||
GDbUser: String;
|
||
GDbPassword: String;
|
||
GDbName: String;
|
||
GCredKey: String;
|
||
GSkipDbPage: Boolean;
|
||
GIsUpgrade: Boolean;
|
||
GInstallMode: String;
|
||
ModePage: TWizardPage;
|
||
RbModeUpdate: TNewRadioButton;
|
||
RbModeReinstall: TNewRadioButton;
|
||
LblModeHint: TNewStaticText;
|
||
GDbConfigured: Boolean;
|
||
GPgToolsReady: Boolean;
|
||
GDbConnOk: Boolean;
|
||
GNextBtnCaption: String;
|
||
GTasksSynced: Boolean;
|
||
|
||
function EnvHasDbMode(): Boolean;
|
||
var
|
||
Lines: TArrayOfString;
|
||
i: Integer;
|
||
Line: String;
|
||
begin
|
||
Result := False;
|
||
if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then
|
||
Exit;
|
||
for i := 0 to GetArrayLength(Lines) - 1 do
|
||
begin
|
||
Line := Trim(Lines[i]);
|
||
if Pos('NETX_DB_MODE=', Line) = 1 then
|
||
begin
|
||
if Length(Line) > Length('NETX_DB_MODE=') then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function ReadEnvValue(Key: String): String;
|
||
var
|
||
Lines: TArrayOfString;
|
||
i: Integer;
|
||
Line: String;
|
||
begin
|
||
Result := '';
|
||
if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then
|
||
Exit;
|
||
for i := 0 to GetArrayLength(Lines) - 1 do
|
||
begin
|
||
Line := Trim(Lines[i]);
|
||
if Pos(Key + '=', Line) = 1 then
|
||
Result := Trim(Copy(Line, Length(Key) + 2, Length(Line)));
|
||
end;
|
||
end;
|
||
|
||
function ValidBundledPort(Port: String; var ErrMsg: String): Boolean;
|
||
var
|
||
N: Integer;
|
||
begin
|
||
N := StrToIntDef(Trim(Port), -1);
|
||
Result := (N >= 1024) and (N <= 65535);
|
||
if not Result then
|
||
ErrMsg := ExpandConstant('{cm:DbBundledPortInvalid}');
|
||
end;
|
||
|
||
{ True when nothing listens on Port (loopback or any address). }
|
||
function TcpPortFree(Port: String): Boolean;
|
||
var
|
||
ResultCode: Integer;
|
||
begin
|
||
Result := True;
|
||
if Exec('powershell.exe',
|
||
'-NoProfile -ExecutionPolicy Bypass -Command "$p=' + Port +
|
||
'; foreach ($a in @([Net.IPAddress]::Loopback, [Net.IPAddress]::Any)) { try { $l = New-Object Net.Sockets.TcpListener($a, $p); $l.Start(); $l.Stop() } catch { exit 1 } }; exit 0"',
|
||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||
Result := (ResultCode = 0);
|
||
end;
|
||
|
||
function EnvModeIsExternal(): Boolean;
|
||
var
|
||
Lines: TArrayOfString;
|
||
i: Integer;
|
||
Line: String;
|
||
begin
|
||
Result := False;
|
||
if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then
|
||
Exit;
|
||
for i := 0 to GetArrayLength(Lines) - 1 do
|
||
begin
|
||
Line := Trim(Lines[i]);
|
||
if CompareText(Line, 'NETX_DB_MODE=external') = 0 then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function ParamIsTruthy(const ParamName: String): Boolean;
|
||
var
|
||
V: String;
|
||
begin
|
||
V := LowerCase(Trim(ExpandConstant('{param:' + ParamName + '|}')));
|
||
Result := (V = '1') or (V = 'true') or (V = 'yes') or (V = 'on');
|
||
end;
|
||
|
||
function DetectExistingConfiguredInstall(): Boolean;
|
||
var
|
||
Lines: TArrayOfString;
|
||
i: Integer;
|
||
Line: String;
|
||
begin
|
||
{ NETX_DATABASE_URL-only installs predate NETX_DB_MODE and use an external DB. }
|
||
Result := EnvHasDbMode();
|
||
if Result then
|
||
Exit;
|
||
if not LoadStringsFromFile(ExpandConstant('{commonappdata}\NetX\.env'), Lines) then
|
||
Exit;
|
||
for i := 0 to GetArrayLength(Lines) - 1 do
|
||
begin
|
||
Line := Trim(Lines[i]);
|
||
if (Pos('NETX_DATABASE_URL=', Line) = 1) and
|
||
(Length(Line) > Length('NETX_DATABASE_URL=')) then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure SyncSkipDbFromInstallMode();
|
||
begin
|
||
GSkipDbPage := (GInstallMode = 'update');
|
||
end;
|
||
|
||
procedure ApplyInstallModeFromChoice();
|
||
begin
|
||
if (RbModeReinstall <> nil) and RbModeReinstall.Checked then
|
||
GInstallMode := 'reinstall'
|
||
else if GIsUpgrade then
|
||
GInstallMode := 'update'
|
||
else
|
||
GInstallMode := 'fresh';
|
||
SyncSkipDbFromInstallMode();
|
||
end;
|
||
|
||
procedure ModeChoiceClick(Sender: TObject);
|
||
begin
|
||
ApplyInstallModeFromChoice();
|
||
if LblModeHint = nil then
|
||
Exit;
|
||
if GInstallMode = 'reinstall' then
|
||
LblModeHint.Caption := ExpandConstant('{cm:ModeReinstallHint}')
|
||
else
|
||
LblModeHint.Caption := ExpandConstant('{cm:ModeUpdateHint}');
|
||
end;
|
||
|
||
function ResolveInstallModeFromParams(): Boolean;
|
||
{ Returns True if a CLI param forced the mode. }
|
||
var
|
||
Mode: String;
|
||
begin
|
||
Result := False;
|
||
Mode := LowerCase(Trim(ExpandConstant('{param:InstallMode|}')));
|
||
if (Mode = 'update') or (Mode = 'upgrade') then
|
||
begin
|
||
GInstallMode := 'update';
|
||
Result := True;
|
||
end
|
||
else if (Mode = 'reinstall') or (Mode = 'fresh') then
|
||
begin
|
||
GInstallMode := 'reinstall';
|
||
Result := True;
|
||
end
|
||
else if ParamIsTruthy('SkipDbPage') then
|
||
begin
|
||
GInstallMode := 'update';
|
||
Result := True;
|
||
end
|
||
else if ParamIsTruthy('ForceDbPage') then
|
||
begin
|
||
GInstallMode := 'reinstall';
|
||
Result := True;
|
||
end;
|
||
if Result then
|
||
SyncSkipDbFromInstallMode();
|
||
end;
|
||
|
||
procedure StopNetxBeforeFileReplace();
|
||
var
|
||
ResultCode: Integer;
|
||
LifecycleScript: String;
|
||
Params: String;
|
||
begin
|
||
ExtractTemporaryFile('_common.ps1');
|
||
ExtractTemporaryFile('service_lifecycle.ps1');
|
||
LifecycleScript := ExpandConstant('{tmp}\service_lifecycle.ps1');
|
||
Params :=
|
||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
|
||
' -Action StopForUpdate' +
|
||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
|
||
if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||
RaiseException('Could not stop NetX before replacing program files.');
|
||
if ResultCode <> 0 then
|
||
RaiseException('Could not stop NetX services/tasks before replacing program files. See ProgramData\\NetX\\data\\runtime\\service_lifecycle.log.');
|
||
end;
|
||
|
||
procedure ResumeNetxServiceAfterInstall();
|
||
var
|
||
ResultCode: Integer;
|
||
LifecycleScript: String;
|
||
Params: String;
|
||
begin
|
||
LifecycleScript := ExpandConstant('{app}\packaging\service_lifecycle.ps1');
|
||
if not FileExists(LifecycleScript) then
|
||
Exit;
|
||
Params :=
|
||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
|
||
' -Action ResumeAfterUpdate' +
|
||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
|
||
if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||
RaiseException('NetX was updated, but its previously running service could not be restarted.');
|
||
if ResultCode <> 0 then
|
||
RaiseException('NetX was updated, but its previously running service could not be restarted. See ProgramData\\NetX\\data\\runtime\\service_lifecycle.log.');
|
||
end;
|
||
|
||
function NetxShouldStartTray(): Boolean;
|
||
begin
|
||
Result := GDbConfigured or
|
||
FileExists(ExpandConstant('{commonappdata}\NetX\.env'));
|
||
end;
|
||
|
||
procedure UpdateDbFieldState();
|
||
var
|
||
Ext: Boolean;
|
||
begin
|
||
Ext := RbExternal.Checked;
|
||
LblHost.Enabled := Ext;
|
||
LblPort.Enabled := Ext;
|
||
LblUser.Enabled := Ext;
|
||
LblPassword.Enabled := Ext;
|
||
LblDbName.Enabled := Ext;
|
||
EdHost.Enabled := Ext;
|
||
EdPort.Enabled := Ext;
|
||
EdUser.Enabled := Ext;
|
||
EdPassword.Enabled := Ext;
|
||
EdDbName.Enabled := Ext;
|
||
LblBundledPort.Enabled := not Ext;
|
||
EdBundledPort.Enabled := not Ext;
|
||
end;
|
||
|
||
procedure DbModeClick(Sender: TObject);
|
||
begin
|
||
UpdateDbFieldState();
|
||
end;
|
||
|
||
function ExtractPgClientTools(): Boolean;
|
||
begin
|
||
if GPgToolsReady and FileExists(ExpandConstant('{tmp}\psql.exe')) and
|
||
FileExists(ExpandConstant('{tmp}\test_pg_conn.ps1')) then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
Result := False;
|
||
try
|
||
ExtractTemporaryFile('test_pg_conn.ps1');
|
||
ExtractTemporaryFile('psql.exe');
|
||
ExtractTemporaryFile('libpq.dll');
|
||
ExtractTemporaryFile('libssl-3-x64.dll');
|
||
ExtractTemporaryFile('libcrypto-3-x64.dll');
|
||
ExtractTemporaryFile('libintl-9.dll');
|
||
ExtractTemporaryFile('libiconv-2.dll');
|
||
ExtractTemporaryFile('libwinpthread-1.dll');
|
||
ExtractTemporaryFile('zlib1.dll');
|
||
ExtractTemporaryFile('liblz4.dll');
|
||
ExtractTemporaryFile('libzstd.dll');
|
||
ExtractTemporaryFile('libxml2.dll');
|
||
ExtractTemporaryFile('libxslt.dll');
|
||
ExtractTemporaryFile('vcruntime140.dll');
|
||
Result := FileExists(ExpandConstant('{tmp}\psql.exe')) and
|
||
FileExists(ExpandConstant('{tmp}\test_pg_conn.ps1'));
|
||
GPgToolsReady := Result;
|
||
except
|
||
Result := False;
|
||
GPgToolsReady := False;
|
||
end;
|
||
end;
|
||
|
||
procedure SetWizardBusy(const Busy: Boolean; const StatusText: String);
|
||
begin
|
||
if Busy then
|
||
begin
|
||
if GNextBtnCaption = '' then
|
||
GNextBtnCaption := WizardForm.NextButton.Caption;
|
||
WizardForm.NextButton.Caption := StatusText;
|
||
WizardForm.NextButton.Enabled := False;
|
||
WizardForm.BackButton.Enabled := False;
|
||
WizardForm.CancelButton.Enabled := False;
|
||
end
|
||
else
|
||
begin
|
||
if GNextBtnCaption <> '' then
|
||
WizardForm.NextButton.Caption := GNextBtnCaption;
|
||
WizardForm.NextButton.Enabled := True;
|
||
WizardForm.BackButton.Enabled := True;
|
||
WizardForm.CancelButton.Enabled := True;
|
||
end;
|
||
WizardForm.Refresh;
|
||
end;
|
||
|
||
function TestExternalDb(const Host, Port, User, Password, DbName: String; var ErrMsg: String): Boolean;
|
||
var
|
||
ResultCode: Integer;
|
||
UrlFile: String;
|
||
ErrFile: String;
|
||
PwFile: String;
|
||
Params: String;
|
||
Lines: TArrayOfString;
|
||
i: Integer;
|
||
begin
|
||
Result := False;
|
||
ErrMsg := '';
|
||
if not ExtractPgClientTools() then
|
||
begin
|
||
ErrMsg := ExpandConstant('{cm:DbTestExtractFailed}');
|
||
Exit;
|
||
end;
|
||
|
||
UrlFile := ExpandConstant('{tmp}\netx_db_url.txt');
|
||
ErrFile := ExpandConstant('{tmp}\netx_db_test_err.txt');
|
||
PwFile := ExpandConstant('{tmp}\netx_db_pw.txt');
|
||
DeleteFile(UrlFile);
|
||
DeleteFile(ErrFile);
|
||
SaveStringToFile(PwFile, Password, False);
|
||
|
||
Params :=
|
||
'-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{tmp}\test_pg_conn.ps1') + '"' +
|
||
' -PgHost "' + Host + '"' +
|
||
' -Port ' + Port +
|
||
' -User "' + User + '"' +
|
||
' -PasswordFile "' + PwFile + '"' +
|
||
' -Database "' + DbName + '"' +
|
||
' -PsqlPath "' + ExpandConstant('{tmp}\psql.exe') + '"' +
|
||
' -OutUrlFile "' + UrlFile + '"' +
|
||
' -OutErrFile "' + ErrFile + '"';
|
||
|
||
if not Exec('powershell.exe', Params, ExpandConstant('{tmp}'), SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||
begin
|
||
ErrMsg := 'powershell_exec_failed';
|
||
DeleteFile(PwFile);
|
||
Exit;
|
||
end;
|
||
DeleteFile(PwFile);
|
||
if ResultCode <> 0 then
|
||
begin
|
||
ErrMsg := 'psql/test exit ' + IntToStr(ResultCode);
|
||
if LoadStringsFromFile(ErrFile, Lines) then
|
||
begin
|
||
ErrMsg := '';
|
||
for i := 0 to GetArrayLength(Lines) - 1 do
|
||
begin
|
||
if ErrMsg <> '' then
|
||
ErrMsg := ErrMsg + #13#10;
|
||
ErrMsg := ErrMsg + Lines[i];
|
||
end;
|
||
if ErrMsg = '' then
|
||
ErrMsg := 'psql/test exit ' + IntToStr(ResultCode);
|
||
end;
|
||
Exit;
|
||
end;
|
||
if not LoadStringsFromFile(UrlFile, Lines) or (GetArrayLength(Lines) < 1) or (Trim(Lines[0]) = '') then
|
||
begin
|
||
ErrMsg := 'url_file_missing';
|
||
Exit;
|
||
end;
|
||
GDbUrl := Trim(Lines[0]);
|
||
Result := True;
|
||
end;
|
||
|
||
function SaveExternalFieldsFromControls(var ErrMsg: String): Boolean;
|
||
begin
|
||
Result := False;
|
||
GDbHost := Trim(EdHost.Text);
|
||
GDbPort := Trim(EdPort.Text);
|
||
GDbUser := Trim(EdUser.Text);
|
||
GDbPassword := EdPassword.Text;
|
||
GDbName := Trim(EdDbName.Text);
|
||
GDbConnOk := False;
|
||
GDbUrl := '';
|
||
if (GDbHost = '') or (GDbPort = '') or (GDbUser = '') or
|
||
(GDbPassword = '') or (GDbName = '') then
|
||
begin
|
||
ErrMsg := ExpandConstant('{cm:DbFieldsRequired}');
|
||
Exit;
|
||
end;
|
||
GDbMode := 'external';
|
||
Result := True;
|
||
end;
|
||
|
||
function ValidateExternalDbSaved(var ErrMsg: String): Boolean;
|
||
begin
|
||
Result := False;
|
||
if GDbConnOk and (GDbUrl <> '') then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
if not TestExternalDb(GDbHost, GDbPort, GDbUser, GDbPassword, GDbName, ErrMsg) then
|
||
begin
|
||
if ErrMsg = '' then
|
||
ErrMsg := 'connection_failed';
|
||
ErrMsg := FmtMessage(ExpandConstant('{cm:DbTestFailed}'), [ErrMsg]);
|
||
GDbConnOk := False;
|
||
Exit;
|
||
end;
|
||
GDbConnOk := True;
|
||
Result := True;
|
||
end;
|
||
|
||
function ApplySilentDbParams(var ErrMsg: String): Boolean;
|
||
var
|
||
Mode, Host, Port, User, Password, DbName: String;
|
||
begin
|
||
Result := False;
|
||
GCredKey := Trim(ExpandConstant('{param:CredentialSecretKey|}'));
|
||
Mode := LowerCase(ExpandConstant('{param:DbMode|bundled}'));
|
||
if Mode = '' then
|
||
Mode := 'bundled';
|
||
if Mode = 'bundled' then
|
||
begin
|
||
GBundledPort := Trim(ExpandConstant('{param:BundledPgPort|}'));
|
||
if GBundledPort = '' then
|
||
GBundledPort := ReadEnvValue('NETX_BUNDLED_PG_PORT');
|
||
if GBundledPort = '' then
|
||
GBundledPort := '15432';
|
||
if not ValidBundledPort(GBundledPort, ErrMsg) then
|
||
Exit;
|
||
GDbMode := 'bundled';
|
||
GDbUrl := '';
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
if Mode <> 'external' then
|
||
begin
|
||
ErrMsg := 'invalid DbMode (use bundled or external)';
|
||
Exit;
|
||
end;
|
||
Host := ExpandConstant('{param:DbHost|}');
|
||
Port := ExpandConstant('{param:DbPort|5432}');
|
||
User := ExpandConstant('{param:DbUser|}');
|
||
Password := ExpandConstant('{param:DbPassword|}');
|
||
DbName := ExpandConstant('{param:DbName|}');
|
||
if (Host = '') or (User = '') or (Password = '') or (DbName = '') then
|
||
begin
|
||
ErrMsg := ExpandConstant('{cm:DbSilentExternalMissing}');
|
||
Exit;
|
||
end;
|
||
if not TestExternalDb(Host, Port, User, Password, DbName, ErrMsg) then
|
||
begin
|
||
if ErrMsg = '' then
|
||
ErrMsg := 'connection_failed';
|
||
ErrMsg := FmtMessage(ExpandConstant('{cm:DbTestFailed}'), [ErrMsg]);
|
||
Exit;
|
||
end;
|
||
GDbMode := 'external';
|
||
Result := True;
|
||
end;
|
||
|
||
procedure CaptureCredKeyFromControls();
|
||
begin
|
||
if EdCredKey <> nil then
|
||
GCredKey := Trim(EdCredKey.Text)
|
||
else
|
||
GCredKey := '';
|
||
end;
|
||
|
||
procedure InitializeWizard();
|
||
var
|
||
TopPos: Integer;
|
||
begin
|
||
GDbMode := 'bundled';
|
||
GDbUrl := '';
|
||
GDbHost := '';
|
||
GDbPort := '5432';
|
||
GDbUser := '';
|
||
GDbPassword := '';
|
||
GDbName := '';
|
||
GCredKey := '';
|
||
GBundledPort := ReadEnvValue('NETX_BUNDLED_PG_PORT');
|
||
if GBundledPort = '' then
|
||
GBundledPort := '15432';
|
||
GDbConfigured := False;
|
||
GPgToolsReady := False;
|
||
GDbConnOk := False;
|
||
{ Existing install: interactive Mode page (Update vs Reinstall).
|
||
Silent: /InstallMode=update|reinstall (or /SkipDbPage=1 /ForceDbPage=1). }
|
||
GIsUpgrade := DetectExistingConfiguredInstall();
|
||
ModePage := nil;
|
||
RbModeUpdate := nil;
|
||
RbModeReinstall := nil;
|
||
LblModeHint := nil;
|
||
if not ResolveInstallModeFromParams() then
|
||
begin
|
||
if GIsUpgrade then
|
||
GInstallMode := 'update'
|
||
else
|
||
GInstallMode := 'fresh';
|
||
SyncSkipDbFromInstallMode();
|
||
end;
|
||
|
||
if GIsUpgrade and (not WizardSilent) then
|
||
begin
|
||
ModePage := CreateCustomPage(wpSelectDir,
|
||
ExpandConstant('{cm:ModePageCaption}'),
|
||
ExpandConstant('{cm:ModePageDescription}'));
|
||
|
||
RbModeUpdate := TNewRadioButton.Create(ModePage);
|
||
RbModeUpdate.Parent := ModePage.Surface;
|
||
RbModeUpdate.Caption := ExpandConstant('{cm:ModeUpdate}');
|
||
RbModeUpdate.Checked := (GInstallMode <> 'reinstall');
|
||
RbModeUpdate.Top := ScaleY(8);
|
||
RbModeUpdate.Left := ScaleX(0);
|
||
RbModeUpdate.Width := ModePage.SurfaceWidth;
|
||
RbModeUpdate.OnClick := @ModeChoiceClick;
|
||
|
||
RbModeReinstall := TNewRadioButton.Create(ModePage);
|
||
RbModeReinstall.Parent := ModePage.Surface;
|
||
RbModeReinstall.Caption := ExpandConstant('{cm:ModeReinstall}');
|
||
RbModeReinstall.Checked := (GInstallMode = 'reinstall');
|
||
RbModeReinstall.Top := RbModeUpdate.Top + ScaleY(28);
|
||
RbModeReinstall.Left := ScaleX(0);
|
||
RbModeReinstall.Width := ModePage.SurfaceWidth;
|
||
RbModeReinstall.OnClick := @ModeChoiceClick;
|
||
|
||
LblModeHint := TNewStaticText.Create(ModePage);
|
||
LblModeHint.Parent := ModePage.Surface;
|
||
LblModeHint.Top := RbModeReinstall.Top + ScaleY(36);
|
||
LblModeHint.Left := ScaleX(0);
|
||
LblModeHint.Width := ModePage.SurfaceWidth;
|
||
LblModeHint.Height := ScaleY(60);
|
||
LblModeHint.AutoSize := False;
|
||
LblModeHint.WordWrap := True;
|
||
ModeChoiceClick(nil);
|
||
|
||
DbPage := CreateCustomPage(ModePage.ID,
|
||
ExpandConstant('{cm:DbPageCaption}'),
|
||
ExpandConstant('{cm:DbPageDescription}'));
|
||
end
|
||
else
|
||
DbPage := CreateCustomPage(wpSelectDir,
|
||
ExpandConstant('{cm:DbPageCaption}'),
|
||
ExpandConstant('{cm:DbPageDescription}'));
|
||
|
||
RbBundled := TNewRadioButton.Create(DbPage);
|
||
RbBundled.Parent := DbPage.Surface;
|
||
RbBundled.Caption := ExpandConstant('{cm:DbBundled}');
|
||
RbBundled.Checked := True;
|
||
RbBundled.Top := ScaleY(8);
|
||
RbBundled.Left := ScaleX(0);
|
||
RbBundled.Width := DbPage.SurfaceWidth - ScaleX(150);
|
||
RbBundled.OnClick := @DbModeClick;
|
||
|
||
{ Bundled PostgreSQL port (127.0.0.1 only), on the same row as its radio button. }
|
||
LblBundledPort := TNewStaticText.Create(DbPage);
|
||
LblBundledPort.Parent := DbPage.Surface;
|
||
LblBundledPort.Caption := ExpandConstant('{cm:DbBundledPort}');
|
||
LblBundledPort.Top := RbBundled.Top + ScaleY(2);
|
||
LblBundledPort.Left := DbPage.SurfaceWidth - ScaleX(140);
|
||
|
||
EdBundledPort := TNewEdit.Create(DbPage);
|
||
EdBundledPort.Parent := DbPage.Surface;
|
||
EdBundledPort.Top := RbBundled.Top - ScaleY(2);
|
||
EdBundledPort.Left := DbPage.SurfaceWidth - ScaleX(80);
|
||
EdBundledPort.Width := ScaleX(80);
|
||
EdBundledPort.Text := GBundledPort;
|
||
|
||
RbExternal := TNewRadioButton.Create(DbPage);
|
||
RbExternal.Parent := DbPage.Surface;
|
||
RbExternal.Caption := ExpandConstant('{cm:DbExternal}');
|
||
RbExternal.Top := RbBundled.Top + ScaleY(28);
|
||
RbExternal.Left := ScaleX(0);
|
||
RbExternal.Width := DbPage.SurfaceWidth;
|
||
RbExternal.OnClick := @DbModeClick;
|
||
|
||
TopPos := RbExternal.Top + ScaleY(36);
|
||
|
||
LblHost := TNewStaticText.Create(DbPage);
|
||
LblHost.Parent := DbPage.Surface;
|
||
LblHost.Caption := ExpandConstant('{cm:DbHost}');
|
||
LblHost.Top := TopPos;
|
||
LblHost.Left := ScaleX(16);
|
||
|
||
EdHost := TNewEdit.Create(DbPage);
|
||
EdHost.Parent := DbPage.Surface;
|
||
EdHost.Top := TopPos;
|
||
EdHost.Left := ScaleX(120);
|
||
EdHost.Width := ScaleX(220);
|
||
EdHost.Text := '127.0.0.1';
|
||
|
||
TopPos := TopPos + ScaleY(28);
|
||
LblPort := TNewStaticText.Create(DbPage);
|
||
LblPort.Parent := DbPage.Surface;
|
||
LblPort.Caption := ExpandConstant('{cm:DbPort}');
|
||
LblPort.Top := TopPos;
|
||
LblPort.Left := ScaleX(16);
|
||
|
||
EdPort := TNewEdit.Create(DbPage);
|
||
EdPort.Parent := DbPage.Surface;
|
||
EdPort.Top := TopPos;
|
||
EdPort.Left := ScaleX(120);
|
||
EdPort.Width := ScaleX(80);
|
||
EdPort.Text := '5432';
|
||
|
||
TopPos := TopPos + ScaleY(28);
|
||
LblUser := TNewStaticText.Create(DbPage);
|
||
LblUser.Parent := DbPage.Surface;
|
||
LblUser.Caption := ExpandConstant('{cm:DbUser}');
|
||
LblUser.Top := TopPos;
|
||
LblUser.Left := ScaleX(16);
|
||
|
||
EdUser := TNewEdit.Create(DbPage);
|
||
EdUser.Parent := DbPage.Surface;
|
||
EdUser.Top := TopPos;
|
||
EdUser.Left := ScaleX(120);
|
||
EdUser.Width := ScaleX(220);
|
||
EdUser.Text := 'netx';
|
||
|
||
TopPos := TopPos + ScaleY(28);
|
||
LblPassword := TNewStaticText.Create(DbPage);
|
||
LblPassword.Parent := DbPage.Surface;
|
||
LblPassword.Caption := ExpandConstant('{cm:DbPassword}');
|
||
LblPassword.Top := TopPos;
|
||
LblPassword.Left := ScaleX(16);
|
||
|
||
EdPassword := TNewEdit.Create(DbPage);
|
||
EdPassword.Parent := DbPage.Surface;
|
||
EdPassword.Top := TopPos;
|
||
EdPassword.Left := ScaleX(120);
|
||
EdPassword.Width := ScaleX(220);
|
||
EdPassword.PasswordChar := '*';
|
||
|
||
TopPos := TopPos + ScaleY(28);
|
||
LblDbName := TNewStaticText.Create(DbPage);
|
||
LblDbName.Parent := DbPage.Surface;
|
||
LblDbName.Caption := ExpandConstant('{cm:DbName}');
|
||
LblDbName.Top := TopPos;
|
||
LblDbName.Left := ScaleX(16);
|
||
|
||
EdDbName := TNewEdit.Create(DbPage);
|
||
EdDbName.Parent := DbPage.Surface;
|
||
EdDbName.Top := TopPos;
|
||
EdDbName.Left := ScaleX(120);
|
||
EdDbName.Width := ScaleX(220);
|
||
EdDbName.Text := 'netx';
|
||
|
||
TopPos := TopPos + ScaleY(36);
|
||
LblCredKey := TNewStaticText.Create(DbPage);
|
||
LblCredKey.Parent := DbPage.Surface;
|
||
LblCredKey.Caption := ExpandConstant('{cm:CredKey}');
|
||
LblCredKey.Top := TopPos;
|
||
LblCredKey.Left := ScaleX(0);
|
||
|
||
EdCredKey := TNewEdit.Create(DbPage);
|
||
EdCredKey.Parent := DbPage.Surface;
|
||
EdCredKey.Top := TopPos;
|
||
EdCredKey.Left := ScaleX(120);
|
||
EdCredKey.Width := DbPage.SurfaceWidth - ScaleX(120);
|
||
EdCredKey.Text := '';
|
||
EdCredKey.PasswordChar := '*';
|
||
|
||
TopPos := TopPos + ScaleY(24);
|
||
LblCredHint := TNewStaticText.Create(DbPage);
|
||
LblCredHint.Parent := DbPage.Surface;
|
||
LblCredHint.Caption := ExpandConstant('{cm:CredKeyHint}');
|
||
LblCredHint.Top := TopPos;
|
||
LblCredHint.Left := ScaleX(120);
|
||
LblCredHint.Width := DbPage.SurfaceWidth - ScaleX(120);
|
||
LblCredHint.AutoSize := False;
|
||
|
||
UpdateDbFieldState();
|
||
|
||
{ Prefill from existing ProgramData\.env when present. }
|
||
if EnvModeIsExternal() then
|
||
begin
|
||
RbExternal.Checked := True;
|
||
UpdateDbFieldState();
|
||
end
|
||
else if EnvHasDbMode() then
|
||
begin
|
||
RbBundled.Checked := True;
|
||
UpdateDbFieldState();
|
||
end;
|
||
|
||
if WizardSilent then
|
||
begin
|
||
{ Params applied in PrepareToInstall }
|
||
end;
|
||
end;
|
||
|
||
function ShouldSkipPage(PageID: Integer): Boolean;
|
||
begin
|
||
Result := False;
|
||
if (ModePage <> nil) and (PageID = ModePage.ID) then
|
||
begin
|
||
{ Only interactive existing installs show the mode page. }
|
||
Result := (not GIsUpgrade) or WizardSilent;
|
||
Exit;
|
||
end;
|
||
if (DbPage <> nil) and (PageID = DbPage.ID) then
|
||
Result := GSkipDbPage;
|
||
end;
|
||
|
||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||
var
|
||
ErrMsg: String;
|
||
begin
|
||
Result := True;
|
||
if (ModePage <> nil) and (CurPageID = ModePage.ID) then
|
||
begin
|
||
ApplyInstallModeFromChoice();
|
||
Exit;
|
||
end;
|
||
{ Only the DB page runs validation. Other pages must always proceed
|
||
(Tasks/Ready used to appear "dead" when CloseApplications hung). }
|
||
if (DbPage = nil) or (CurPageID <> DbPage.ID) or GSkipDbPage then
|
||
Exit;
|
||
|
||
CaptureCredKeyFromControls();
|
||
if RbBundled.Checked then
|
||
begin
|
||
GBundledPort := Trim(EdBundledPort.Text);
|
||
if not ValidBundledPort(GBundledPort, ErrMsg) then
|
||
begin
|
||
MsgBox(ErrMsg, mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
{ The current install's own PG may hold its port; only probe a changed port. }
|
||
if (GBundledPort <> ReadEnvValue('NETX_BUNDLED_PG_PORT')) and (not TcpPortFree(GBundledPort)) then
|
||
begin
|
||
MsgBox(FmtMessage(ExpandConstant('{cm:DbBundledPortInUse}'), [GBundledPort]), mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
GDbMode := 'bundled';
|
||
GDbUrl := '';
|
||
GDbConnOk := True;
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
|
||
if not SaveExternalFieldsFromControls(ErrMsg) then
|
||
begin
|
||
MsgBox(ErrMsg, mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
|
||
{ Connection test here so bad settings never reach Tasks/Ready.
|
||
Show busy state — ExtractTemporaryFile + psql can take a few seconds. }
|
||
SetWizardBusy(True, ExpandConstant('{cm:DbTesting}'));
|
||
try
|
||
Result := ValidateExternalDbSaved(ErrMsg);
|
||
finally
|
||
SetWizardBusy(False, '');
|
||
end;
|
||
if not Result then
|
||
MsgBox(ErrMsg, mbError, MB_OK);
|
||
end;
|
||
|
||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||
var
|
||
ErrMsg: String;
|
||
begin
|
||
Result := '';
|
||
NeedsRestart := False;
|
||
|
||
if (ModePage <> nil) and (not WizardSilent) then
|
||
ApplyInstallModeFromChoice();
|
||
|
||
{ Stop tray / API / bundled PG so Program Files can be replaced safely. }
|
||
if GSkipDbPage or GIsUpgrade or DirExists(ExpandConstant('{app}\packaging')) then
|
||
StopNetxBeforeFileReplace();
|
||
|
||
if GSkipDbPage then
|
||
Exit;
|
||
|
||
if WizardSilent then
|
||
begin
|
||
if not ApplySilentDbParams(ErrMsg) then
|
||
Result := ErrMsg;
|
||
Exit;
|
||
end;
|
||
|
||
if GDbMode = '' then
|
||
GDbMode := 'bundled';
|
||
|
||
{ Re-check only if user never got a successful test (should be rare). }
|
||
if (GDbMode = 'external') and (not GDbConnOk) then
|
||
begin
|
||
if not ValidateExternalDbSaved(ErrMsg) then
|
||
Result := ErrMsg;
|
||
end;
|
||
end;
|
||
|
||
function ApplyDatabaseConfig(): Boolean;
|
||
var
|
||
ResultCode: Integer;
|
||
Params: String;
|
||
UrlFile: String;
|
||
KeyFile: String;
|
||
LogFile: String;
|
||
InitDb: String;
|
||
EnvFile: String;
|
||
Wrapper: String;
|
||
WrapBody: String;
|
||
begin
|
||
Result := False;
|
||
GDbConfigured := False;
|
||
if GDbMode = '' then
|
||
GDbMode := 'bundled';
|
||
|
||
InitDb := ExpandConstant('{app}\postgres\pgsql\bin\initdb.exe');
|
||
if (GDbMode = 'bundled') and (not FileExists(InitDb)) then
|
||
begin
|
||
MsgBox(FmtMessage(ExpandConstant('{cm:DbBundledMissing}'), [InitDb]), mbError, MB_OK);
|
||
Exit;
|
||
end;
|
||
|
||
ForceDirectories(ExpandConstant('{commonappdata}\NetX\data\runtime'));
|
||
LogFile := ExpandConstant('{commonappdata}\NetX\data\runtime\setup_first_run.log');
|
||
Wrapper := ExpandConstant('{tmp}\netx_apply_db.ps1');
|
||
|
||
WrapBody :=
|
||
'$ErrorActionPreference = ''Stop''' + #13#10 +
|
||
'$log = ''' + LogFile + '''' + #13#10 +
|
||
'Start-Transcript -Path $log -Force | Out-Null' + #13#10 +
|
||
'try {' + #13#10 +
|
||
' & ''' + ExpandConstant('{app}\packaging\setup_first_run.ps1') + '''' +
|
||
' -ProgramRoot ''' + ExpandConstant('{app}') + '''' +
|
||
' -DataRoot ''' + ExpandConstant('{commonappdata}\NetX') + '''' +
|
||
' -NonInteractive -DbMode ' + GDbMode;
|
||
|
||
if (GDbMode = 'bundled') and (GBundledPort <> '') then
|
||
WrapBody := WrapBody + ' -BundledPort ' + GBundledPort;
|
||
|
||
if GDbMode = 'external' then
|
||
begin
|
||
UrlFile := ExpandConstant('{tmp}\netx_db_url.txt');
|
||
if GDbUrl <> '' then
|
||
SaveStringToFile(UrlFile, GDbUrl, False);
|
||
WrapBody := WrapBody +
|
||
' -ExternalDatabaseUrlFile ''' + UrlFile + ''' -SkipExternalProbe';
|
||
end;
|
||
|
||
if GCredKey <> '' then
|
||
begin
|
||
KeyFile := ExpandConstant('{tmp}\netx_cred_key.txt');
|
||
SaveStringToFile(KeyFile, GCredKey, False);
|
||
WrapBody := WrapBody + ' -CredentialSecretKeyFile ''' + KeyFile + '''';
|
||
end;
|
||
|
||
WrapBody := WrapBody + #13#10 +
|
||
' if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE }' + #13#10 +
|
||
'} catch {' + #13#10 +
|
||
' Write-Error $_' + #13#10 +
|
||
' exit 1' + #13#10 +
|
||
'} finally {' + #13#10 +
|
||
' Stop-Transcript | Out-Null' + #13#10 +
|
||
'}' + #13#10;
|
||
|
||
SaveStringToFile(Wrapper, WrapBody, False);
|
||
Params := '-NoProfile -ExecutionPolicy Bypass -File "' + Wrapper + '"';
|
||
|
||
if not Exec(
|
||
'powershell.exe', Params, ExpandConstant('{app}'),
|
||
SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||
begin
|
||
MsgBox(ExpandConstant('{cm:DbApplyExecFailed}'), mbError, MB_OK);
|
||
Exit;
|
||
end;
|
||
if ResultCode <> 0 then
|
||
begin
|
||
MsgBox(FmtMessage(ExpandConstant('{cm:DbApplyFailed}'), [IntToStr(ResultCode), LogFile]), mbError, MB_OK);
|
||
Exit;
|
||
end;
|
||
|
||
EnvFile := ExpandConstant('{commonappdata}\NetX\.env');
|
||
if not FileExists(EnvFile) then
|
||
begin
|
||
MsgBox(ExpandConstant('{cm:DbEnvMissing}'), mbError, MB_OK);
|
||
Exit;
|
||
end;
|
||
|
||
GDbConfigured := True;
|
||
Result := True;
|
||
end;
|
||
|
||
function InitializeUninstall(): Boolean;
|
||
var
|
||
DeleteSrc: String;
|
||
LifecycleScript: String;
|
||
Params: String;
|
||
ResultCode: Integer;
|
||
begin
|
||
Result := True;
|
||
GDeleteNetxData := False;
|
||
GTempDeleteDataScript := ExpandConstant('{tmp}\netx_uninstall_delete_data.ps1');
|
||
|
||
// Copy data-wipe helper to TEMP before program files are removed.
|
||
DeleteSrc := ExpandConstant('{app}\packaging\uninstall_delete_data.ps1');
|
||
if FileExists(DeleteSrc) then
|
||
CopyFile(DeleteSrc, GTempDeleteDataScript, False);
|
||
|
||
if UninstallSilent then
|
||
begin
|
||
// Settings often uses QuietUninstallString; keep data by default.
|
||
GDeleteNetxData := False;
|
||
end
|
||
else
|
||
begin
|
||
GDeleteNetxData :=
|
||
MsgBox(ExpandConstant('{cm:UninstallDeleteData}'), mbConfirmation, MB_YESNO) = IDYES;
|
||
end;
|
||
|
||
LifecycleScript := ExpandConstant('{app}\packaging\service_lifecycle.ps1');
|
||
if FileExists(LifecycleScript) then
|
||
begin
|
||
Params :=
|
||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + LifecycleScript + '"' +
|
||
' -Action RemoveForUninstall' +
|
||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
|
||
if (not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode)) or
|
||
(ResultCode <> 0) then
|
||
begin
|
||
MsgBox('Could not stop/remove the NetX Windows service or startup task. Uninstall has been cancelled to protect the installation. See ProgramData\\NetX\\data\\runtime\\service_lifecycle.log.', mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||
var
|
||
ResultCode: Integer;
|
||
DataRoot: String;
|
||
begin
|
||
if (CurUninstallStep = usPostUninstall) and GDeleteNetxData then
|
||
begin
|
||
DataRoot := ExpandConstant('{commonappdata}\NetX');
|
||
if FileExists(GTempDeleteDataScript) then
|
||
begin
|
||
Exec(
|
||
'powershell.exe',
|
||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' +
|
||
GTempDeleteDataScript + '" -DataRoot "' + DataRoot + '"',
|
||
ExpandConstant('{tmp}'),
|
||
SW_HIDE,
|
||
ewWaitUntilTerminated,
|
||
ResultCode
|
||
);
|
||
end;
|
||
if DirExists(DataRoot) then
|
||
DelTree(DataRoot, True, True, True);
|
||
end;
|
||
end;
|
||
|
||
procedure RepairNetxVenvAfterInstall();
|
||
var
|
||
ResultCode: Integer;
|
||
RepairScript: String;
|
||
Params: String;
|
||
begin
|
||
RepairScript := ExpandConstant('{app}\packaging\repair_venv.ps1');
|
||
if not FileExists(RepairScript) then
|
||
Exit;
|
||
Params :=
|
||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + RepairScript + '"' +
|
||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
|
||
Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||
end;
|
||
|
||
function NetxTaskExists(const Name: String): Boolean;
|
||
var
|
||
RC: Integer;
|
||
begin
|
||
Result := Exec(ExpandConstant('{sys}\schtasks.exe'), '/Query /TN "\NetX\' + Name + '"', '',
|
||
SW_HIDE, ewWaitUntilTerminated, RC) and (RC = 0);
|
||
end;
|
||
|
||
function NetxServiceExists(): Boolean;
|
||
var
|
||
RC: Integer;
|
||
begin
|
||
Result := Exec(ExpandConstant('{sys}\sc.exe'), 'query NetX', '', SW_HIDE, ewWaitUntilTerminated, RC) and (RC = 0);
|
||
end;
|
||
|
||
procedure CurPageChanged(CurPageID: Integer);
|
||
begin
|
||
{ Existing install: show the run mode / toggles that are really in place
|
||
(older versions set them from Finish-page checkboxes Inno never recorded). }
|
||
if (CurPageID = wpSelectTasks) and (not GTasksSynced) then
|
||
begin
|
||
GTasksSynced := True;
|
||
if FileExists(ExpandConstant('{app}\packaging\netx_tray.ps1')) then
|
||
begin
|
||
if NetxServiceExists() then
|
||
WizardSelectTasks('runservice')
|
||
else
|
||
WizardSelectTasks('runtray');
|
||
if NetxTaskExists('NetXTray') then
|
||
WizardSelectTasks('runtray\autostart')
|
||
else
|
||
WizardSelectTasks('!runtray\autostart');
|
||
if NetxTaskExists('NetXUpdate') then
|
||
WizardSelectTasks('autoupdate')
|
||
else
|
||
WizardSelectTasks('!autoupdate');
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function RunNetxScript(const Script, Extra: String): Integer;
|
||
var
|
||
Params: String;
|
||
begin
|
||
Params :=
|
||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\packaging\' + Script) + '"' +
|
||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"' + Extra;
|
||
if not Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, Result) then
|
||
Result := -1;
|
||
end;
|
||
|
||
function TasksGivenOnCommandLine(): Boolean;
|
||
var
|
||
i: Integer;
|
||
P: String;
|
||
begin
|
||
Result := False;
|
||
for i := 1 to ParamCount do
|
||
begin
|
||
P := Uppercase(ParamStr(i));
|
||
if (Pos('/TASKS=', P) = 1) or (Pos('/MERGETASKS=', P) = 1) then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure ApplyRunModeTasks();
|
||
var
|
||
Code: Integer;
|
||
begin
|
||
{ Silent auto-updates keep whatever is installed unless /TASKS is passed. }
|
||
if WizardSilent and (not TasksGivenOnCommandLine()) then
|
||
Exit;
|
||
if not FileExists(ExpandConstant('{commonappdata}\NetX\.env')) then
|
||
Exit;
|
||
|
||
if WizardIsTaskSelected('runservice') then
|
||
begin
|
||
Code := RunNetxScript('install_service.ps1', ' -Start');
|
||
if Code <> 0 then
|
||
SuppressibleMsgBox(FmtMessage(ExpandConstant('{cm:ServiceInstallFailed}'), [IntToStr(Code),
|
||
ExpandConstant('{commonappdata}\NetX\data\runtime')]), mbError, MB_OK, IDOK);
|
||
end
|
||
else if NetxServiceExists() then
|
||
RunNetxScript('install_service.ps1', ' -Uninstall');
|
||
|
||
if WizardIsTaskSelected('runtray\autostart') then
|
||
RunNetxScript('install_autostart.ps1', '')
|
||
else
|
||
RunNetxScript('install_autostart.ps1', ' -Remove');
|
||
|
||
if WizardIsTaskSelected('autoupdate') then
|
||
RunNetxScript('install_update_task.ps1', '')
|
||
else if NetxTaskExists('NetXUpdate') then
|
||
RunNetxScript('install_update_task.ps1', ' -Remove');
|
||
end;
|
||
|
||
procedure CurStepChanged(CurStep: TSetupStep);
|
||
var
|
||
UninstallKey: String;
|
||
begin
|
||
if CurStep = ssPostInstall then
|
||
begin
|
||
{ Always relink .venv -> local python/runtime (Setup is elevated). }
|
||
RepairNetxVenvAfterInstall();
|
||
|
||
{ Upgrade keeps existing .env / pgdata - do not re-run DB wizard. }
|
||
if not GSkipDbPage then
|
||
ApplyDatabaseConfig()
|
||
else
|
||
GDbConfigured := FileExists(ExpandConstant('{commonappdata}\NetX\.env'));
|
||
|
||
UninstallKey :=
|
||
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001}_is1';
|
||
RegDeleteValue(HKLM, UninstallKey, 'QuietUninstallString');
|
||
ApplyRunModeTasks();
|
||
ResumeNetxServiceAfterInstall();
|
||
end;
|
||
end;
|