-
Notifications
You must be signed in to change notification settings - Fork 6
Description
Summary of the new feature / enhancement
PSU MSI Patch — Change Summary
This feature request builds on the already-posted and successful base fix from GitHub issue #5592:
That issue contains the original service-account validation fix. The changes documented below extend that baseline with the optional Test Credentials button and the Devolutions branding/metadata work.
Product: Devolutions PowerShell Universal 2026.1.3
Original package: Devolutions PowerShell Universal 2026.1.3 MSI
Change 1 — Error 1923 Fix (Account Validation)
Problem
The installer's SettingsServiceDlg dialog collects a Windows service account name and password but performs no validation before proceeding. If the account does not exist or the credentials are wrong, the installer advances to the CreateService call and fails with Error 1923 — a generic, misleading error with no actionable information and a full rollback.
Solution
A .NET 4.7.2 custom action DLL (ValidateSvcAcctCA.CA.dll) was built and injected into the MSI Binary table with two entry points:
This is the core fix that corresponds to GitHub issue #5592 and should be referred to that way when describing prior work to the upstream author.
| Entry point | Type | When it fires | What it does |
|---|---|---|---|
ValidateServiceAccountUI |
Type 1 (immediate) | Next button on SettingsServiceDlg, Ordering=0 |
Calls NTAccount.Translate(typeof(SecurityIdentifier)) — confirms the account exists; sets _SVCACCT_VALID="1" on success |
ValidateServiceAccount |
Type 1025 (deferred) | InstallExecuteSequence, seq 5797 |
Same existence check for silent /qn installs — returns ActionResult.Failure to abort before CreateService |
The three NewDialog ControlEvents that advance past SettingsServiceDlg were each updated to include the condition _SVCACCT_VALID = "1", so the Next button is gated — the user cannot advance until the account resolves.
MSI Database Changes
| Table | Operation | Detail |
|---|---|---|
Binary |
INSERT | ValidateServiceAccountCA → ValidateSvcAcctCA.CA.dll |
CustomAction |
INSERT | ValidateServiceAccountUI Type=1 |
CustomAction |
INSERT | SetValidateServiceAccountData Type=51 |
CustomAction |
INSERT | ValidateServiceAccount Type=1025 |
ControlEvent |
INSERT | SettingsServiceDlg / Next → DoAction / ValidateServiceAccountUI |
ControlEvent |
UPDATE ×3 | NewDialog conditions on Next gated on _SVCACCT_VALID = "1" |
InstallExecuteSequence |
INSERT | SetValidateServiceAccountData seq=5796; ValidateServiceAccount seq=5797 |
Change 2 — Test Credentials Button
Problem
Even with Change 1 in place, a wrong password passes the account-existence check and only fails at CreateService, still triggering Error 1923 and rollback.
Solution
Added an optional "Test Credentials" PushButton to SettingsServiceDlg. When clicked it fires a third entry point, TestServiceCredentials, which calls the Win32 LogonUser API with LOGON32_LOGON_NETWORK (type 3) and shows a specific MessageBoxW for each failure mode.
LOGON32_LOGON_NETWORK is used rather than LOGON32_LOGON_SERVICE (type 5) because the Log on as a service right may not yet be granted prior to installation — network logon validates the credential independently of that right.
The button is opt-in: it does not gate Next and does not change the behaviour of the existing account-existence check.
Error messages returned
| Win32 error | Message shown |
|---|---|
1326 ERROR_LOGON_FAILURE |
Account not found or password incorrect |
1327 ERROR_ACCOUNT_RESTRICTION |
Account restrictions prevent logon |
1330 ERROR_PASSWORD_EXPIRED |
Password has expired |
1331 ERROR_ACCOUNT_DISABLED |
Account is disabled |
1909 ERROR_ACCOUNT_LOCKED_OUT |
Account is locked out |
Built-in virtual accounts (LocalSystem, NT AUTHORITY\*, NT SERVICE\*) are detected and short-circuit with an informational message — no LogonUser call is made for these.
Control layout on SettingsServiceDlg
X=130 Y=92 W=120 H=17 PushButton "&Test Credentials"
StartServiceCheckbox moved Y=90→115; OpenBrowserCheckbox moved Y=110→135 to make room.
Tab order: ServiceAccountPassword → TestCredentials → StartServiceCheckbox.
MSI Database Changes
| Table | Operation | Detail |
|---|---|---|
CustomAction |
INSERT | TestServiceCredentials Type=1 |
Control |
INSERT | TestCredentials PushButton on SettingsServiceDlg |
Control |
UPDATE | ServiceAccountPassword.Control_Next → TestCredentials |
Control |
UPDATE | StartServiceCheckbox.Y → 115 |
Control |
UPDATE | OpenBrowserCheckbox.Y → 135 |
ControlEvent |
INSERT | TestCredentials → DoAction / TestServiceCredentials |
Test Results
Tested on Windows Server 2019
Change 3 — Devolutions Branding (Track A: Metadata + Typography)
Problem
The PSU MSI was originally built by IronmanSoftware (Adam Driscoll) using WiX 3.11. After Devolutions acquired PowerShell Universal (October 2025), Manufacturer was updated to Devolutions, Inc in the original WiX source, but the installer still carried:
- ARP links (
ARPHELPLINK,ARPURLINFOABOUT) pointing topowershelluniversal.com Manufacturercasing/punctuation inconsistent with other Devolutions products- The WiX default font Tahoma in all dialog TextStyle entries
- No alignment with the metadata conventions of other Devolutions products
Reference used: current Devolutions Remote Desktop Manager MSI metadata and installer conventions. The values below were aligned to match existing Devolutions products as closely as possible.
Property table changes
| Property | Old value | New value |
|---|---|---|
Manufacturer |
Devolutions, Inc |
Devolutions inc. |
ARPHELPLINK |
https://www.powershelluniversal.com |
https://forum.devolutions.net |
ARPURLINFOABOUT |
https://www.powershelluniversal.com |
https://devolutions.net |
ARPCOMMENTS |
(empty) | This installer database contains the logic and data required to install Devolutions PowerShell Universal. |
ARPURLUPDATEINFO |
present | removed (not present in Devolutions MSIs) |
ProductName |
PowerShell Universal |
Devolutions PowerShell Universal |
WIXUI_EXITDIALOGOPTIONALTEXT |
PowerShell Universal is now installed. |
Devolutions PowerShell Universal is now installed. |
TextStyle changes
| TextStyle | FaceName (before) | FaceName (after) | Size |
|---|---|---|---|
WixUI_Font_Normal |
Tahoma | Segoe UI | 8 |
WixUI_Font_Title |
Tahoma | Segoe UI | 9 |
WixUI_Font_Bigger |
Tahoma | Segoe UI | 12 |
Note: RDM uses Tahoma because Advanced Installer defaults to it. Segoe UI is kept here as a deliberate modernization while still moving the installer toward Devolutions-aligned presentation.
Change 4 — Devolutions Branding (Track B: Installer Visuals)
The standard WiX dialog visuals were updated so the installer looks like a Devolutions-owned product rather than a stock WiX package.
The important implementation point is not the specific working files used locally, but the outcome:
- the welcome / finish dialog artwork was replaced with Devolutions-aligned artwork
- the inner-dialog banner was replaced with a Devolutions-aligned banner that respects MSI text overlay constraints
- the final visual layout keeps MSI-rendered text readable instead of placing dark artwork under the title/description regions
The original WiX BMPs were 8-bit palette. The MSI engine accepts 24-bit BMP replacements without modification.
Change 5 — Devolutions Branding (Track C: Product Icon)
The ProductIcon row in the Icon table was replaced with a Devolutions-branded icon so Add/Remove Programs and MSI shell surfaces no longer show the legacy IronmanSoftware/PSU icon.
Change 6 — Devolutions Branding (Track D: Summary Information Stream)
The MSI Summary Information stream is separate from the Property table and is read by tools like Orca, sigcheck, and msiinfo. It previously fingerprinted the installer as a WiX-built package by exposing the WiX toolset version string in CreatingApplication (PID 18).
| PID | Field | Old value | New value |
|---|---|---|---|
| 3 | Subject | PowerShell Universal |
Devolutions PowerShell Universal |
| 4 | Author | Devolutions, Inc |
Devolutions inc. |
| 6 | Comments | Windows Installer Package |
This installer database contains the logic and data required to install Devolutions PowerShell Universal. |
| 18 | CreatingApplication | Windows Installer XML Toolset (3.11.2.4516) |
Devolutions PowerShell Universal |
PID 18 is the most visible fingerprint difference. RDM sets it to Remote Desktop Manager; PSU is set to Devolutions PowerShell Universal following the same convention.
Unchanged (by design)
| Item | Reason |
|---|---|
%ProgramData%\PowerShellUniversal in FatalError dialog |
Real disk path PSU writes logs to — changing it would misdirect users |
docs.powershelluniversal.com in License RTF |
Binary RTF blob; IronmanSoftware's EULA text to own |
ProductCode, UpgradeCode |
Must not change — upgrade/uninstall of existing installations depends on these GUIDs |
C# Implementation
The validation behavior described above is backed by a small C# custom action assembly with three entry points.
[CustomAction]
public static ActionResult ValidateServiceAccountUI(Session session)
{
session["_SVCACCT_VALID"] = "";
string account = session["SERVICEACCOUNT"];
if (string.IsNullOrWhiteSpace(account)
|| account.Equals("LocalSystem", StringComparison.OrdinalIgnoreCase)
|| account.StartsWith("NT AUTHORITY\\", StringComparison.OrdinalIgnoreCase)
|| account.StartsWith("NT SERVICE\\", StringComparison.OrdinalIgnoreCase))
{
session["_SVCACCT_VALID"] = "1";
return ActionResult.Success;
}
try
{
new NTAccount(account).Translate(typeof(SecurityIdentifier));
session["_SVCACCT_VALID"] = "1";
return ActionResult.Success;
}
catch (IdentityNotMappedException)
{
MessageBoxW(IntPtr.Zero,
"Service account '" + account + "' could not be found on this machine or domain.\n\n"
+ "Please verify the account name and try again.",
"PowerShell Universal Setup",
MB_OK | MB_ICONERROR);
return ActionResult.Success;
}
}
[CustomAction]
public static ActionResult ValidateServiceAccount(Session session)
{
string account = session.CustomActionData["SERVICEACCOUNT"];
if (string.IsNullOrWhiteSpace(account)
|| account.Equals("LocalSystem", StringComparison.OrdinalIgnoreCase)
|| account.StartsWith("NT AUTHORITY\\", StringComparison.OrdinalIgnoreCase)
|| account.StartsWith("NT SERVICE\\", StringComparison.OrdinalIgnoreCase))
{
session.Log("[ValidateServiceAccount] Built-in account, skipping.");
return ActionResult.Success;
}
try
{
new NTAccount(account).Translate(typeof(SecurityIdentifier));
session.Log("[ValidateServiceAccount] PASS: '" + account + "'");
return ActionResult.Success;
}
catch (IdentityNotMappedException)
{
session.Log("[ValidateServiceAccount] FAIL: '" + account + "' could not be resolved.");
var record = new Record(1);
record[0] = "Service account '[1]' could not be found. Correct the account name and re-run.";
record[1] = account;
session.Message(InstallMessage.Error, record);
return ActionResult.Failure;
}
}
[CustomAction]
public static ActionResult TestServiceCredentials(Session session)
{
string account = session["SERVICEACCOUNT"];
string password = session["SERVICEACCOUNTPASSWORD"];
if (string.IsNullOrWhiteSpace(account)
|| account.Equals("LocalSystem", StringComparison.OrdinalIgnoreCase)
|| account.StartsWith("NT AUTHORITY\\", StringComparison.OrdinalIgnoreCase)
|| account.StartsWith("NT SERVICE\\", StringComparison.OrdinalIgnoreCase))
{
MessageBoxW(IntPtr.Zero,
"'" + (string.IsNullOrWhiteSpace(account) ? "LocalSystem" : account) + "' is a built-in virtual account.\n\n"
+ "No password test is required — this account is always valid.",
"PowerShell Universal Setup",
MB_OK | MB_ICONINFORMATION);
return ActionResult.Success;
}
string domain;
string user;
int backslash = account.IndexOf('\\');
if (backslash > 0)
{
domain = account.Substring(0, backslash);
user = account.Substring(backslash + 1);
}
else
{
domain = null;
user = account;
}
IntPtr token = IntPtr.Zero;
try
{
bool ok = LogonUser(user, domain, password,
LOGON32_LOGON_NETWORK,
LOGON32_PROVIDER_DEFAULT,
out token);
if (ok)
{
MessageBoxW(IntPtr.Zero,
"Credentials for '" + account + "' verified successfully.\n\n"
+ "The account name and password are correct.",
"PowerShell Universal Setup",
MB_OK | MB_ICONINFORMATION);
}
else
{
int err = Marshal.GetLastWin32Error();
MessageBoxW(IntPtr.Zero,
BuildErrorMessage(account, err),
"PowerShell Universal Setup",
MB_OK | MB_ICONERROR);
}
}
finally
{
if (token != IntPtr.Zero)
CloseHandle(token);
}
return ActionResult.Success;
}This is the key implementation logic only. The surrounding DLL wrapper, MSI table wiring, and sequencing are described in the sections above.
Requested Outcome
The MSI should incorporate the following as a single cohesive improvement set:
- Keep the already-accepted account-existence validation fix from issue
#5592as the baseline. - Add the optional
Test Credentialsbutton so bad passwords can be diagnosed beforeCreateServicefails. - Align installer metadata and presentation with current Devolutions product conventions, including company naming, ARP links/comments, product naming, icon, and installer visuals.
The branding portion does not need to match any local prototype exactly. The important requirement is that the installer should ship with Devolutions-aligned metadata and non-stock visuals while preserving MSI text readability.
Proposed technical implementation details (optional)
No response