netx/packaging/installer/netx.iss
2026-10-14 10:28:09 +08:00

1375 lines
48 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

; 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;