Pro Update (PSIUpdater)

Pro Update is the self-updating desktop client that keeps all ProApps and local Windows services current. It reads version files from the deployment network share, detects updates across three channels (Alpha, Beta, Stable), and handles file copy, service restart, and self-update operations.


Overview

PropertyValue
ProjectPSIUpdater
SolutionPSIUpdater/PSI.Updater.sln
Framework.NET Framework 4.8 / WPF
DependenciesStandalone — zero references to PSI.Common or PSI.DataAccess
LoggingSerilog (rolling file)
NuGetMvvmLightLibs 5.4.1, Serilog 3.1.1, CommonServiceLocator 2.0.2, MaterialDesignThemes 4.9.0
CI/CDStandard release.yml workflow (PSIUpdater is in the app registry like any other app)

Pro Update is intentionally standalone with no dependency on PSI.Common. This ensures it can always update itself and other apps even if shared libraries are broken.


Update Channels

Pro Update supports three deployment channels with strict priority:

ChannelVersion fileBinary locationWho sees itPriority
Alphaalphaversion.txtApps/{AppName}/_alpha/{Version}/Alpha testers onlyHighest
Betabetaversion.txtApps/{AppName}/{Version}/Beta testers onlyMedium
Stableversion.txtApps/{AppName}/{Version}/All usersDefault

Channel resolution per app (highest version wins, label only if strictly higher than stable):

  1. If user has alpha access AND alpha version > stable version Alpha
  2. Else if user has beta access AND beta version > stable version Beta
  3. Else Stable (even if alpha/beta versions exist at the same version as stable)

Access is controlled per-app in deploy/app-registry.json (source of truth), synced to PSApplications.txt / PSServices.txt on the deployment share:

AppName;ExecutableName;Access;BetaAccess;AlphaAccess

Architecture

Service Decomposition

The application follows MVVM Light with extracted service interfaces for testability:

ServiceResponsibility
INetworkShareServiceRead PSApplications.txt, PSServices.txt, version files
IVersionServiceGet stable/beta/alpha versions, semantic comparison
IUpdateServiceCopy files, update apps, self-update
IProcessServiceStart/stop processes and Windows services
IFileVerificationServiceSHA256 hash comparison, directory comparison
IReleaseNotesServiceRead release-notes.json / beta / alpha variants
ITelemetryServiceRecord update success/failure to network share
IAppLoggerSerilog-based rolling file logger

Key Paths

PathPurpose
\\ad.ptihome.com\DFS\Data\APPS\Approved\DotNet\AppDeployments\Deployment base
C:\Users\Public\Progressive Surface\{AppName}\Local app install
C:\Program Files (x86)\Progressive Surface\Local service install
C:\Users\Public\Progressive Surface\Pro Update\logs\Log files

All paths are configurable via App.config appSettings (no longer hardcoded).


How Updates Work

Application Updates

1. Read PSApplications.txt from network share
2. For each authorized app:
   a. Resolve channel (Alpha > Beta > Stable)
   b. Read version from alphaversion.txt / betaversion.txt / version.txt
   c. Compare to local version via FileVersionInfo.ProductVersion
   d. If update needed:
      - Close running app (graceful close, then force kill after timeout)
      - Copy: network share -> temp folder -> local install folder
      - Verify file hashes match
      - Restart app if it was running

Service Updates

Services (PSILocalService, PSIBroadcast, PSIHealth) are always shown in the app list and use a trust-but-verify pattern:

1. Try WCF (via PSILocalService):
   a. Call UpdateService(serviceName, source, dest)
   b. Wait 3 seconds
   c. Re-check installed version — if it matches target, success
   d. If version unchanged, treat as failure and fall through
2. If WCF failed or unavailable:
   Fall back to ServiceUpdate.ps1 (runs elevated, handles everything)

ServiceUpdate.ps1 is the proven fallback — it stops PSIHealth, kills service processes, deletes install folders, copies fresh binaries, verifies, and restarts. Located at C:\Apps\ServiceUpdate.ps1 (local) or \\ad.ptihome.com\DFS\data\_Updates\ServiceUpdate.ps1 (network).

Design rule: Pro Update must always be able to update services regardless of the deployed service version. WCF is the fast path, ServiceUpdate.ps1 is the reliable fallback. No user-facing error messages about WCF status.

The three services

There are exactly three Windows services in the PSI.All stack that Pro Update manages:

Service (Windows name)ProjectPurpose
PSIHealthPSI.HealthMonitor.ServiceWatchdog. Keeps the other two running and auto-updates them on the Stable channel (see below).
PSILocalServicePSI.Service.LocalWCF endpoint on each PC. Hosts ServiceUpdateHelper and other local operations Pro Update calls into.
PSIBroadcastPSI.Broadcast.ServiceCross-app messaging bus.

If any of them falls out of date, Pro Update shows it as Update-Required in the main grid and the user can recover from the UI.

PSIHealth as a service auto-updater

PSIHealth includes an internal ServiceVersionChecker that performs silent stable-channel updates on a 30-minute cadence. This is the primary path that keeps services current in the field without any user action.

PropertyValue
Interval30 minutes (first check 1 minute after service start)
ChannelStable only — reads version.txt on the share, no awareness of beta/alpha
Managed servicesPSILocalService, PSIBroadcast (hard-coded in ManagedServices array)
Self-update?No — PSIHealth cannot update itself; Pro Update handles that via WCF or the script fallback
Update strategyNuclear reinstall: stop service → delete install folder → copy fresh from share → verify file count + sizes → start service (2 retry attempts)
Health loop coordinationSets a pause flag so the watchdog doesn’t restart the service mid-swap; writes an update-in-progress sentinel file
Corruption repairEven when versions match, verifies file count/sizes on the share vs. local. Mismatch triggers a reinstall.

State files live under %ProgramData%\PSI\ServiceUpdates\:

  • update-in-progress — sentinel written during an active update
  • last-update-{ServiceName}.txt — timestamp + version of the last successful update

Why Pro Update keeps its own update path anyway. PSIHealth covers the common case (stable, the two worker services, silently) but doesn’t cover:

  1. Beta/alpha service deployments — those only work through Pro Update.
  2. Updating PSIHealth itself.
  3. A broken or out-of-date PSIHealth — if the watchdog is the thing that’s stale, it can’t fix itself.

So the flow is: PSIHealth takes the background load off the user for the 90%+ case, and when something is out of date anyway, it shows up in Pro Update where the user can click Update and trigger WCF → ServiceUpdate.ps1 as needed.

Self-Update

Pro Update can update itself via a batch script workaround:

  1. Kill any other running instances of PSI.Updater (skip own PID — must not kill itself before writing the batch)
  2. Write %TEMP%\update.bat that: waits 3 seconds for the process to exit, xcopy from network share, relaunches (if DisplayFull mode)
  3. Launch the batch script
  4. Call Environment.Exit(0) to release file locks
  5. Batch completes the copy and relaunches Pro Update

Important: The self-update must never call Process.Kill() on its own PID. The batch file handles the timing — it waits for the process to exit naturally via Environment.Exit(0) before copying.


Command-Line Arguments

Pro Update accepts arguments from Application Manager or direct invocation:

ArgumentEffect
adminFull UI; gear-button visibility is still gated on AD\IT - RF membership
minimalMinimal UI, just progress bar
silentNo UI, fully automated
update=allAuto-update all authorized apps
update=AppNameUpdate specific app (also accepts update=App1;App2)
updateselfTrigger self-update batch
restartRestart apps after update
autocloseClose when complete
statusWrite current state (apps, services, versions, registry health) to status.json for CLI automation; combine with autoclose for one-shot probes

Arguments are case-insensitive. Multiple arguments separated by spaces.


UI

The main window shows a DataGrid with columns:

ColumnContent
ApplicationApp display name
ChannelAlpha (purple) / Beta (orange) / Stable (green) — the channel resolved for this user
InstalledCurrent local version
TargetVersion the user will get if they click Update (resolved from the user’s channel)
Release NotesExpandable notes icon; tooltip + expanded row details
ActionUpdate / Install / Reinstall button

An “Update All” button updates everything in sequence.

Per-app deployed-version breakdowns (stable vs. beta vs. alpha) are not shown on the main grid — they live in the Admin Panel where the beta management workflow actually happens.


Admin Panel (“Beta Management”)

A gear button appears in the main window header (tooltip: Beta Management). It is visible only to members of the AD security group IT - RF — the single source of truth for who can manage beta/alpha access. Clicking it opens the Admin Panel — a standalone window for managing app/service access without editing JSON or running workflows.

Authorization model (v1.0.0.55+): Membership in AD\IT - RF is the only gate. The legacy fallback to a developers group inside app-registry.json was removed — admin access is now keyed entirely on AD so adds/removes flow through normal account management instead of editing the share. If you need someone to have Beta Management, ask IT to add them to IT - RF.

The role check uses the bare group name (IsInRole("IT - RF")) so it matches regardless of the user’s NetBIOS domain prefix. Earlier versions hardcoded AD\IT - RF, which silently never matched (the actual NetBIOS name is PTI_DOMAIN); admin access had been falling through to the now-removed JSON fallback the entire time.

What It Manages

SectionCapability
GroupsCreate/delete groups, add/remove members. Groups like developers or production can be referenced in access lists instead of individual users.
Beta AccessAdd/remove users or groups from each app’s beta tester list
Alpha AccessSame as beta, for alpha testers
VisibilityToggle apps between “Everyone” and specific users/groups. Controls who can see the app in Pro Update.

How It Works

  1. Reads app-registry.json from the network share on open
  2. Admin edits groups, beta/alpha access, or visibility via the UI
  3. Save writes the updated JSON back to the share (with .bak backup)
  4. Automatically runs sync-registry.ps1 logic to keep PSApplications.txt and PSServices.txt in sync
  5. Cancel discards changes and closes the window (prompts if unsaved changes exist)

AD Integration

  • On load, resolves all user IDs in access lists via Active Directory batch lookup
  • Unknown user initials are resolved to display names (e.g., AMDAustin Decker)
  • Disabled AD accounts are highlighted in red with a tooltip — helps admins clean up stale access lists
  • Search boxes for adding users query AD in real-time, showing both users and groups

UI Refinements (March–April 2026)

After the initial Phase 4 release, the admin panel gained a set of quality-of-life improvements:

  • Deployed version display on each app row (April 2026) — disambiguates the “has testers” dot. Each row shows Stable v<X> always and Beta v<Y> (orange, bold) only when a beta is actually deployed on the share and strictly newer than stable. Alpha similarly. Without this, a developer reported reading the orange dot as “there’s a beta deployed” when all it meant was “beta access list is configured.” Note: betaversion.txt == version.txt is the idle state (no live beta) — those rows correctly render as stable only, no beta label.
  • Orphaned-beta warning (April 2026) — when an app has a live beta on the share (betaversion.txt > version.txt) but zero users in its beta access list, the row appends an amber ⚠ no testers assigned label after the Beta version. Tooltip explains: “An active beta is deployed on the share but no users are in the beta access list. Assign testers or pull the beta.” Keeps betas from quietly sitting unused (and risking auto-promotion later with no validation). Alpha gets the same treatment.
  • “Beta/Alpha only” filter includes deployed state (April 2026) — the filter checkbox used to match only apps whose tester list was non-empty, which hid orphaned-beta apps. It now also matches apps with a deployed beta/alpha version regardless of tester state, so the orphan warning is visible when the filter is on.
  • Teams-chat reminder toast (April 2026) — adding a tester to an app that already has a deployed beta/alpha means the registry is up to date but the existing Teams chat is still membered with whoever was configured at beta-deploy time. On Save, a Snackbar toast pops up at the bottom of the admin window: ⚠ Remember to add the new tester(s) to the Teams chat — <AppName> beta (added <ids>). Multiple apps get combined into a single toast. The workflow does not auto-sync Teams chat membership; the admin does it in Teams when this toast appears.
  • Collapsible Groups section — Groups live inside an Expander (starts expanded) so admins managing many apps can collapse it out of the way.
  • Compact header bar — reduced padding/font size to reclaim vertical space.
  • Beta/Alpha dots on the app list — small orange/purple dots next to app names whose registry has a beta/alpha access list configured. Tooltip now reads “N beta tester(s) configured” so hovering reinforces that the dot is about the list, not about whether a beta is actually out. The version line below the name shows what’s actually deployed.
  • “Beta/Alpha only” filter — a checkbox that hides apps with no beta/alpha list configured, for quickly reviewing what’s opted in.
  • Bold names on unsaved changes — app and group names render bold when they have pending edits. Uses a fingerprint-based dirty tracker: adding then removing the same user cancels out and does not mark the entry dirty.
  • New group auto-selects after creation so you can start adding members immediately.
  • Smarter AD search — visibility search now includes live AD lookup for both users AND groups, disabled AD accounts are filtered out of search results, and the result limit was raised from 10/15 to 25.
  • Live AD search in Beta/Alpha tester picker (May 2026) — the Beta and Alpha tester search boxes used to only match KnownUsers (people the registry had seen before, i.e. anyone who’d launched Pro Update at least once). Brand-new employees were invisible — admins couldn’t pre-assign them to a beta because the picker didn’t return them. The Visibility tab had had live DirectorySearcher for a while; the same helper now backs Beta and Alpha. PSI on-prem AD is the source of truth for both AD and Entra (synced via AAD Connect), so the LDAP query reaches the same population Entra would. Disabled accounts and computer accounts (sam ending in $) still excluded; result list capped at 25.
  • Scrolling fixesScrollViewer + MaxHeight on every search dropdown, and a scrolling container on the Groups list so 5+ groups no longer get clipped.
  • Sort order — filtered app list is stable: apps first, then services, alphabetical within each.

UI Framework

The Admin Panel uses MaterialDesignInXAML (v4.9.0) for a modern look with chip-style access lists, toggle switches, and search dropdowns. This is the only part of Pro Update that uses MaterialDesign — the main window retains the standard WPF styling.

Key Files

FilePurpose
Views/AdminWindow.xamlAdmin panel UI layout
Views/AdminWindow.xaml.csClick handlers for chips and search results
ViewModels/AdminViewModel.csAll admin logic: load, save, AD lookup, search
Models/AdminModels.csEditableGroup, EditableAccessEntry, AccessItem, AdSearchResult models

Status File (CLI automation)

Pro Update writes a status.json to C:\Users\Public\Progressive Surface\Pro Update\status.json after status and update operations. This is how monitoring scripts probe deployment state without needing the WPF UI.

Schema (illustrative):

{
  "timestamp": "2026-05-06T09:04:30-04:00",
  "action": "status",
  "result": "ok",
  "error": null,
  "user": "AMD",
  "proUpdateVersion": "1.0.0.55",
  "registryHealth": {
    "status": "ok",
    "error": null,
    "path": "\\\\ad.ptihome.com\\DFS\\Data\\APPS\\Approved\\DotNet\\AppDeployments\\app-registry.json"
  },
  "applications": [ /* per-app channel + installed/available versions */ ],
  "services":     [ /* per-service equivalent */ ]
}

When the registry JSON fails to parse on the share, registryHealth.status becomes "error" and error carries the parser message. The same condition shows a yellow banner in the UI so users can see why beta channels and the gear button look broken.

Telemetry

After each update, Pro Update writes a JSON telemetry file to the network share:

{deployBase}\Telemetry\{yyyy-MM-dd}\{machineName}.json
{
  "machine": "PC-NAME",
  "user": "jsmith",
  "app": "PSI.ProViewer",
  "fromVersion": "1.0.0.42",
  "toVersion": "1.0.0.43",
  "success": true,
  "timestamp": "2026-02-26T14:30:00Z"
}

This enables tracking deployment rollout across the organization without requiring an API server.


CI/CD Release

Pro Update uses the standard release workflow (release.yml) — the same as all other apps. It’s registered in deploy/app-registry.json with enabled: true and appears in the app dropdown when running the workflow.

The deployment model

Every version folder on the share is a real build — there’s no separate “beta” and “stable” codebase. version.txt, betaversion.txt, and alphaversion.txt are just pointers to version folders. Promotion is a pointer flip, not a rebuild. This means every version number that gets used eventually could become stable via /promote, so every build needs its source anchored in git with a tag.

Release types

Release typeBehavior
alphaDeploy to _alpha/<Version>/, write alphaversion.txt. Does not commit the version bump and does not create a GitHub Release. _alpha/ is explicitly for pipeline testing; Application Manager never sees it.
betaBump version, deploy to Apps/<AppName>/<Version>/, write betaversion.txt, commit the AssemblyInfo bump back to main with [skip ci], tag <AppName>/v<Version> at that commit’s SHA, create a prerelease GitHub Release, create a beta-test tracking issue, notify the Teams chat. Beta users come from the registry (admin panel) — no need to enter them in the workflow.
minor / majorSame as beta plus: writes version.txt instead of betaversion.txt (so it becomes stable for all users), creates the GitHub Release as a regular release (not a prerelease). Deletes betaversion.txt if one existed.

Why every non-alpha release commits and tags: Because any beta can be promoted to stable later, and when it is, we need to be able to point at the exact source commit that produced that build. Before April 2026 only minor/major committed, which meant beta version numbers burned without any git trace. That also meant the Pro-Update/vX tag created at promotion time couldn’t point at the right commit — it ended up on whatever main’s HEAD happened to be at promotion, which was often an unrelated commit.

Always bump, never reuse. Every retrigger of release.yml bumps AssemblyInfo.cs — there’s no “reuse current version if source is ahead of production” branch. bump-version.ps1 walks past existing folders on the share to find a free slot, so the workflow always lands on a fresh version with its own commit and its own tag. Reusing a version number would desynchronize the tag from the binary on the share (overwrite with different code under the same label).

Promotion (/promote)

When a beta tracking issue gets a /promote comment, promote-beta.yml runs:

  1. Reads betaversion.txt on the share to find the beta version.
  2. Writes that version to version.txt, deletes betaversion.txt. No rebuild — the exact same binaries that were serving beta users now serve everyone.
  3. Looks up the existing GitHub Release by tag (created at the original beta deploy), PATCHes prerelease: false, and appends a “Promoted to stable: by @” footer to the body. Tag stays pinned to the original build commit.
  4. Leaves betaAccess alone. Tester rosters are typically stable across iterations, so the same list applies to the next beta of the same app — clearing forced re-entry for no real benefit. Edit the panel directly to remove a tester.

This means the tag on origin reliably points at the source that produced the deployed binaries, for every version ever shipped — beta or stable.

Note: The separate release-updater.yml workflow was removed in March 2026. Pro Update now deploys through the same pipeline as every other app.

Historical artifact: Pro-Update/v1.0.0.43 on origin points at an unrelated Enterprise Manager commit due to the pre-April 2026 promotion bug. The source retroactively corresponding to .43 is approximately commit 71fdd55f9 (the AssemblyInfo sync commit). Tags for .44 and later are correct.

See Deploy ProApps for the full CI/CD pipeline documentation.


Interaction with Application Manager

Pro Update and Application Manager are separate applications with complementary roles:

ComponentRoleSees alpha?
Application ManagerSystem tray agent, polls version.txt/betaversion.txt every ~5 min, prompts userNo
Pro UpdateFull update client, handles all 3 channels, manages service updatesYes

Application Manager can invoke Pro Update via Process.Start() with command-line arguments when an update is detected.


Testing

70 xUnit tests in PSIUpdater.Tests/ covering:

Test classCoverage
VersionServiceTestsSemantic version comparison, alpha/beta/stable resolution
ApplicationInfoTestsUpdateRequired logic with edge cases
NetworkShareServiceTestsFile parsing, missing fields, error handling
ProcessServiceTestsTimeout handling, service status
FileVerificationServiceTestsSHA256 hash comparison, directory comparison
RegistryParserTestsapp-registry.json structure, group expansion, malformed-JSON handling

Tests run automatically via test.yml on PRs touching PSIUpdater/**.


See Also


Last updated: May 2026 (v1.0.0.55 — AD-only Beta Management authorization, registry-health surfacing)