Skip to content

Architecture

Rod Christiansen edited this page Sep 5, 2026 · 2 revisions

Architecture

ManageUsers is a single self-contained .NET 10 executable targeting net10.0-windows, published trimmed and compressed as one file per architecture. It has no service, no agent, no network calls, and no database. All state is on disk in %ProgramData%.

Components

Type File Responsibility
Program Program.cs Command-line parsing (System.CommandLine), the Global\ManageUsers mutex, exit codes
ManageUsersEngine Services/ManageUsersEngine.cs Orchestrates the run; owns the policy evaluation of each candidate
ConfigService Services/ConfigService.cs YAML load/save, exclusion merge, console-user detection
PolicyService Services/PolicyService.cs Rule matching, strategy parsing, end-of-term evaluation
UserEnumerationService Services/UserEnumerationService.cs NetUserEnum, SID resolution, ProfileList reading, stale/corrupt profile discovery, Administrators membership
UserDeletionService Services/UserDeletionService.cs Account and profile destruction, deferral, verification
RecycleBinService Services/RecycleBinService.cs Per-SID recycle-bin clearing and the orphan sweep
RepairService Services/RepairService.cs Profile-state warnings; login-screen hide list
LogService Services/LogService.cs Day-directory operational log, structured event stream, audit log, retention, rotation, legacy log migration

Models under Models/ are plain YamlDotNet-annotated DTOs plus AppConstants, which holds every path and the built-in exclusion set.

Run order

flowchart TD
    A[Mutex Global\ManageUsers] -->|already held| Z[exit 2]
    A --> B[LogService: open day directory,<br/>prune past 30 days, migrate legacy logs, rotate]
    B --> C[Load Config.yaml]
    C --> D[Load Sessions.yaml + Inventory.yaml]
    D --> E[Merge exclusions:<br/>built-in + config + sessions + console user]
    E --> F[Process DeferredDeletes from last run]
    F --> G[Enumerate local accounts<br/>skip disabled / excluded / protected admins]
    G --> H[Repair pass: log-only profile warnings]
    H --> I[Resolve policy from inventory<br/>first matching rule wins]
    I --> J[Pass 1: retention<br/>delete accounts past threshold]
    J --> K[Pass 2: orphans<br/>accounts with no profile — NO age check]
    K --> L[Pass 3: stale profiles<br/>folders with no account, threshold applies]
    L --> M[Pass 4: corrupt profiles<br/>NO age check]
    M --> N[Sweep orphaned recycle bins]
    N --> O[Write SpecialAccounts\UserList hide entries]
    O --> P[RUN_SUMMARY audit line, exit 0]
Loading

Ordering is deliberate in three places:

  • Deferred deletions run first, so an account that had a session yesterday is retried before the current run's evaluation adds more work.
  • Corrupt-profile remediation runs after the stale-profile pass, so profiles just removed are not re-evaluated as corrupt.
  • The recycle-bin sweep runs last, so bins emptied by this run's deletions are already handled and the sweep only sees genuine orphans.

How it talks to Windows

No WMI, no COM, no ADSI, no PowerShell. Enumeration is P/Invoke; destruction is a mix of a supported API and console tools.

Operation Mechanism
Enumerate local accounts netapi32!NetUserEnum level 1, FILTER_NORMAL_ACCOUNT
Administrators membership netapi32!NetLocalGroupGetMembers level 0, group name resolved from S-1-5-32-544 so it works on localized Windows; pages on ERROR_MORE_DATA
Name → SID advapi32!LookupAccountName; NTAccount.Translate in the deletion path
Profile inventory HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList, subkeys starting S-1-5-21- or S-1-12-1-
Last profile load LocalProfileLoadTimeHigh / …Low combined into a FILETIME
Profile in use HKEY_USERS\<SID> present = hive loaded
Delete profile userenv!DeleteProfileW, falling back to manual folder + registry removal
Delete account net user "<name>" /delete, success detected by the word successfully in the output
Kill processes taskkill /f /fi "USERNAME eq <MACHINE>\<user>"
End sessions quser to find the session id, then logoff <id>
Console user quser, first line containing Active
BitLocker status manage-bde -status C: (informational only)

Shelling out to net, quser, logoff, taskkill and manage-bde means the tool inherits their behaviour, including localization. The quser parsing looks for the literal English Active, and success from net user is a substring check on the word successfully. On a non-English Windows installation both checks can misread. Each subprocess has a 30-second wait (5 seconds for quser).

Profile deletion strategy

The preferred path is DeleteProfileW, which removes the profile directory, the ProfileList and ProfileGuid entries, and associated per-SID state as one consistent operation. Manual deletion is the fallback and is ordered folder first, registry second on purpose: a ProfileList entry pointing at a gutted folder corrupts the next logon for that SID, while an orphaned folder is harmless and is retried next run.

DeleteProfileW can also report success while leaving files behind, so a residual-folder sweep follows it.

Recursive deletion is hand-rolled rather than Directory.Delete(recursive: true) because the built-in fails on the legacy compatibility junctions in a profile (Application Data, Local Settings, My Documents), whose ACLs deny enumerate-children. The custom walker:

  • clears read-only attributes on files and directories;
  • unlinks reparse points without descending into them;
  • swallows UnauthorizedAccessException and IOException per item, so one locked file cannot abort the tree;
  • retries a failed subdirectory with a non-recursive delete before moving on.

The cost of that resilience is that a partially-deleted profile is possible; VerifyDeletion catches it and the run records USER_DELETE_FAILED.

On-disk state

Path Written by Contents
C:\Program Files\sbin\manageusers.exe installer The binary
C:\ProgramData\Management\ManageUsers\Config.yaml you / config management Policy
C:\ProgramData\Management\ManageUsers\Sessions.yaml tool + you Local exclusions, deferred deletions
C:\ProgramData\Management\Inventory.yaml enrolment system Device facts (read-only to this tool)
C:\ProgramData\ManagedUsers\logs\<yyyy-MM-dd>\manageusers.log tool That day's operational log, rotated at 10 MB, 5 generations
C:\ProgramData\ManagedUsers\logs\<yyyy-MM-dd>\events.jsonl tool The same entries structured, one JSON object per line, not size-rotated
C:\ProgramData\ManagedUsers\logs\manageusers.audit.log tool Append-only decision/outcome log, size-rotated, exempt from the 30-day sweep
C:\ProgramData\Management\Logs\ManageUsers*.log earlier releases Migrated once to the logs root on first run of a current build

Day directories older than 30 days are deleted whole at the start of every run, along with any loose files left at the logs root. The audit log is the one thing that persists indefinitely — see Logging and Auditing.

The only mutable runtime state is Sessions.yaml (DeferredDeletes) and the logs. There is no cache, no watermark file, and no memory of previous runs beyond those — every run re-derives everything from the live system.

Concurrency and reentrancy

A single global mutex (Global\ManageUsers) makes concurrent runs impossible machine-wide; a second instance exits 2 immediately. Within a run everything is single-threaded. The logger serializes writes under one lock, and an audit line's write to the audit log, the day's operational log and events.jsonl all happen inside a single lock acquisition so they cannot interleave out of order.

Failure philosophy

The engine wraps the whole run in one try/catch. An unhandled exception aborts the remaining passes, logs the message and stack trace, and returns 1. Below that level, almost everything is best-effort and logged rather than fatal:

  • A failed Config.yaml parse falls back to defaults including empty exclusions.
  • A failed Administrators enumeration logs a warning and continues with whatever it gathered — protection may be incomplete.
  • A failed ProfileList read cancels the recycle-bin sweep rather than treating every bin as orphaned.
  • Individual file and registry failures are logged and stepped over.

Two of those defaults fail open rather than closed. That is the reason the Safe Deployment sequence exists, and the reason the audit log is worth shipping somewhere central.

See also

ManageUsers

Home

Before you deploy

Reference

Internals

Help


This tool deletes user accounts and profiles. Run --simulate first.

Clone this wiki locally