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
| Property | Value |
|---|---|
| Project | PSIUpdater |
| Solution | PSIUpdater/PSI.Updater.sln |
| Framework | .NET Framework 4.8 / WPF |
| Dependencies | Standalone — zero references to PSI.Common or PSI.DataAccess |
| Logging | Serilog (rolling file) |
| NuGet | MvvmLightLibs 5.4.1, Serilog 3.1.1, CommonServiceLocator 2.0.2, MaterialDesignThemes 4.9.0 |
| CI/CD | Standard 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:
| Channel | Version file | Binary location | Who sees it | Priority |
|---|---|---|---|---|
| Alpha | alphaversion.txt | Apps/{AppName}/_alpha/{Version}/ | Alpha testers only | Highest |
| Beta | betaversion.txt | Apps/{AppName}/{Version}/ | Beta testers only | Medium |
| Stable | version.txt | Apps/{AppName}/{Version}/ | All users | Default |
Channel resolution per app (highest version wins, label only if strictly higher than stable):
- If user has alpha access AND alpha version > stable version → Alpha
- Else if user has beta access AND beta version > stable version → Beta
- 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:
| Service | Responsibility |
|---|---|
INetworkShareService | Read PSApplications.txt, PSServices.txt, version files |
IVersionService | Get stable/beta/alpha versions, semantic comparison |
IUpdateService | Copy files, update apps, self-update |
IProcessService | Start/stop processes and Windows services |
IFileVerificationService | SHA256 hash comparison, directory comparison |
IReleaseNotesService | Read release-notes.json / beta / alpha variants |
ITelemetryService | Record update success/failure to network share |
IAppLogger | Serilog-based rolling file logger |
Key Paths
| Path | Purpose |
|---|---|
\\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) | Project | Purpose |
|---|---|---|
PSIHealth | PSI.HealthMonitor.Service | Watchdog. Keeps the other two running and auto-updates them on the Stable channel (see below). |
PSILocalService | PSI.Service.Local | WCF endpoint on each PC. Hosts ServiceUpdateHelper and other local operations Pro Update calls into. |
PSIBroadcast | PSI.Broadcast.Service | Cross-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.
| Property | Value |
|---|---|
| Interval | 30 minutes (first check 1 minute after service start) |
| Channel | Stable only — reads version.txt on the share, no awareness of beta/alpha |
| Managed services | PSILocalService, PSIBroadcast (hard-coded in ManagedServices array) |
| Self-update? | No — PSIHealth cannot update itself; Pro Update handles that via WCF or the script fallback |
| Update strategy | Nuclear reinstall: stop service → delete install folder → copy fresh from share → verify file count + sizes → start service (2 retry attempts) |
| Health loop coordination | Sets a pause flag so the watchdog doesn’t restart the service mid-swap; writes an update-in-progress sentinel file |
| Corruption repair | Even 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 updatelast-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:
- Beta/alpha service deployments — those only work through Pro Update.
- Updating PSIHealth itself.
- 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:
- Kill any other running instances of PSI.Updater (skip own PID — must not kill itself before writing the batch)
- Write
%TEMP%\update.batthat: waits 3 seconds for the process to exit, xcopy from network share, relaunches (if DisplayFull mode) - Launch the batch script
- Call
Environment.Exit(0)to release file locks - 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 viaEnvironment.Exit(0)before copying.
Command-Line Arguments
Pro Update accepts arguments from Application Manager or direct invocation:
| Argument | Effect |
|---|---|
admin | Full UI; gear-button visibility is still gated on AD\IT - RF membership |
minimal | Minimal UI, just progress bar |
silent | No UI, fully automated |
update=all | Auto-update all authorized apps |
update=AppName | Update specific app (also accepts update=App1;App2) |
updateself | Trigger self-update batch |
restart | Restart apps after update |
autoclose | Close when complete |
status | Write 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:
| Column | Content |
|---|---|
| Application | App display name |
| Channel | Alpha (purple) / Beta (orange) / Stable (green) — the channel resolved for this user |
| Installed | Current local version |
| Target | Version the user will get if they click Update (resolved from the user’s channel) |
| Release Notes | Expandable notes icon; tooltip + expanded row details |
| Action | Update / 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 - RFis the only gate. The legacy fallback to adevelopersgroup insideapp-registry.jsonwas 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 toIT - 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 hardcodedAD\IT - RF, which silently never matched (the actual NetBIOS name isPTI_DOMAIN); admin access had been falling through to the now-removed JSON fallback the entire time.
What It Manages
| Section | Capability |
|---|---|
| Groups | Create/delete groups, add/remove members. Groups like developers or production can be referenced in access lists instead of individual users. |
| Beta Access | Add/remove users or groups from each app’s beta tester list |
| Alpha Access | Same as beta, for alpha testers |
| Visibility | Toggle apps between “Everyone” and specific users/groups. Controls who can see the app in Pro Update. |
How It Works
- Reads
app-registry.jsonfrom the network share on open - Admin edits groups, beta/alpha access, or visibility via the UI
- Save writes the updated JSON back to the share (with
.bakbackup) - Automatically runs
sync-registry.ps1logic to keepPSApplications.txtandPSServices.txtin sync - 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.,
AMD→Austin 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 andBeta 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.txtis 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 assignedlabel 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 liveDirectorySearcherfor 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 fixes —
ScrollViewer+MaxHeighton 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
| File | Purpose |
|---|---|
Views/AdminWindow.xaml | Admin panel UI layout |
Views/AdminWindow.xaml.cs | Click handlers for chips and search results |
ViewModels/AdminViewModel.cs | All admin logic: load, save, AD lookup, search |
Models/AdminModels.cs | EditableGroup, 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 type | Behavior |
|---|---|
alpha | Deploy 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. |
beta | Bump 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 / major | Same 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/majorcommitted, which meant beta version numbers burned without any git trace. That also meant thePro-Update/vXtag created at promotion time couldn’t point at the right commit — it ended up on whatevermain’s HEAD happened to be at promotion, which was often an unrelated commit.
Always bump, never reuse. Every retrigger of
release.ymlbumpsAssemblyInfo.cs— there’s no “reuse current version if source is ahead of production” branch.bump-version.ps1walks 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:
- Reads
betaversion.txton the share to find the beta version. - Writes that version to
version.txt, deletesbetaversion.txt. No rebuild — the exact same binaries that were serving beta users now serve everyone. - 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. - Leaves
betaAccessalone. 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.ymlworkflow was removed in March 2026. Pro Update now deploys through the same pipeline as every other app.
Historical artifact:
Pro-Update/v1.0.0.43on origin points at an unrelated Enterprise Manager commit due to the pre-April 2026 promotion bug. The source retroactively corresponding to.43is approximately commit71fdd55f9(the AssemblyInfo sync commit). Tags for.44and 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:
| Component | Role | Sees alpha? |
|---|---|---|
| Application Manager | System tray agent, polls version.txt/betaversion.txt every ~5 min, prompts user | No |
| Pro Update | Full update client, handles all 3 channels, manages service updates | Yes |
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 class | Coverage |
|---|---|
VersionServiceTests | Semantic version comparison, alpha/beta/stable resolution |
ApplicationInfoTests | UpdateRequired logic with edge cases |
NetworkShareServiceTests | File parsing, missing fields, error handling |
ProcessServiceTests | Timeout handling, service status |
FileVerificationServiceTests | SHA256 hash comparison, directory comparison |
RegistryParserTests | app-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)