Skip to content

add Test Credentials to PSU MSI and align installer branding with Devolutions #5597

@DataTraveler1

Description

@DataTraveler1

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 ValidateServiceAccountCAValidateSvcAcctCA.CA.dll
CustomAction INSERT ValidateServiceAccountUI Type=1
CustomAction INSERT SetValidateServiceAccountData Type=51
CustomAction INSERT ValidateServiceAccount Type=1025
ControlEvent INSERT SettingsServiceDlg / NextDoAction / 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: ServiceAccountPasswordTestCredentialsStartServiceCheckbox.

MSI Database Changes

Table Operation Detail
CustomAction INSERT TestServiceCredentials Type=1
Control INSERT TestCredentials PushButton on SettingsServiceDlg
Control UPDATE ServiceAccountPassword.Control_NextTestCredentials
Control UPDATE StartServiceCheckbox.Y → 115
Control UPDATE OpenBrowserCheckbox.Y → 135
ControlEvent INSERT TestCredentialsDoAction / 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 to powershelluniversal.com
  • Manufacturer casing/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:

  1. Keep the already-accepted account-existence validation fix from issue #5592 as the baseline.
  2. Add the optional Test Credentials button so bad passwords can be diagnosed before CreateService fails.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions