Fix endpoint user-context collection running in the wrong session
Mapped drives reported Unavailable on endpoints where the operator could see them fine. The data path was intact; the collection was running under the wrong token. Mapped drives are per logon token, so an elevated probe enumerates a different drive table than the interactive user. Collection: - Pin the user-context spool path so the probe and service agree on one location, and persist/overwrite it instead of deleting on read. - Refuse to collect while elevated (exit code 2) rather than emitting a confident wrong answer, and stamp elevated/username on the spool. - Reject spools stamped elevated or from an unexpected account on read, so an admin run cannot masquerade as valid. - Log netuse/smbmapping/userlistener counts so each run leaves evidence of what it actually saw. Install and launch context: - Move collection to a 'ShopDB user-context probe' scheduled task under S-1-5-32-545 at RunLevel Limited, the only mechanism that reliably runs in the operator's own session. Verify registration and record it in the install ledger. - Stop starting the tray as a child of the elevated installer; explorer.exe de-elevates without changing ACCOUNT, so an admin install produced an admin tray. The Run key only fires at logon, which left the tray dead on a machine already signed in, so add a transient 'ShopDB tray first launch' task that starts it once in the existing session and unregisters itself. - Hide the console window during --collect-usercontext. The agent is a console exe; the tray used to mask this with CreateNoWindow, but the scheduled task invokes it directly and flashed a window every cycle. Installer edits are in _install_script() in plugins/endpoints/api/siteagent.py, which generates the install.ps1 that actually ships. The standalone agent/installer/install-shopdbagent.ps1 was never shipped once the package moved to the UI builder and is deleted here to stop the divergence that caused an earlier fix to land in a file no endpoint ever ran. Versions: agent 1.10.1, tray 1.3.0. These share the pinned spool path and must be deployed together. Pre-existing test failures unrelated to this work (8, identical on a clean tree): docs publishable/versions, plugin contract symlink scan, import guard tamper check, security headers upload sandbox, and three endpoint timestamp tests.
This commit is contained in:
@@ -106,6 +106,8 @@ arrays. `adopt` is first-contact only.
|
||||
],
|
||||
"idleseconds": 45,
|
||||
"username": "operator",
|
||||
"elevated": false, // token elevation of the collecting process;
|
||||
// a spool stamped true is REJECTED (see 5.3)
|
||||
"collectedat": "2025-01-14T06:12:00Z"
|
||||
},
|
||||
"events": [
|
||||
@@ -194,8 +196,9 @@ Notes:
|
||||
rather than a string match against a Windows enum the server would have to
|
||||
keep in sync. `null` means the drive did not answer: unknown, NOT healthy.
|
||||
`healthstatus` keeps the human-readable source value alongside it.
|
||||
- `usercontext` is gathered by a probe the TRAY runs inside the signed-in
|
||||
operator's session, because mapped drives, user-owned listeners and idle time
|
||||
- `usercontext` is gathered by a probe run inside the signed-in operator's
|
||||
session by the `ShopDB user-context probe` scheduled task (and, opportunistically,
|
||||
by the tray), because mapped drives, user-owned listeners and idle time
|
||||
do not exist in the agent's own LocalSystem session 0. It is omitted when no
|
||||
one is signed in, which is a normal unattended state, not a fault. The
|
||||
security-context split that makes this work is normative; see section 5.
|
||||
@@ -302,9 +305,9 @@ Three processes run as three different principals. Confusing them is what makes
|
||||
|
||||
| Process | Runs as | May write to |
|
||||
|---|---|---|
|
||||
| Service (`shopdbagent.exe`) | LocalSystem, session 0 | `%ProgramData%\ShopDB\EndpointAgent\logs\agent-<date>.log` |
|
||||
| Tray + probe (`shopdb-endpoint-tray.exe`, `shopdbagent.exe --collect-usercontext`) | `ShopFloor` operator, interactive session | `C:\Users\ShopFloor\ShopDB\` and `C:\Users\ShopFloor\ShopDB\Logs\<datetime>-trayagent.log` |
|
||||
| Installer (`install-shopdbagent.ps1`) | Elevated admin, one-shot | `%ProgramData%\ShopDB\Logs\<datetime>-shopdbinstalls.log` |
|
||||
| Service (`shopdbagent.exe`) | LocalSystem, session 0 | `%ProgramData%\ShopDB\EndpointAgent\logs\<date>-agent.log` |
|
||||
| Tray + probe (`shopdb-endpoint-tray.exe`, `shopdbagent.exe --collect-usercontext`) | `ShopFloor` operator, interactive session, UNELEVATED (task principal `S-1-5-32-545` at `RunLevel Limited`) | `C:\Users\ShopFloor\ShopDB\` and `C:\Users\ShopFloor\ShopDB\Logs\<date>-trayagent.log` |
|
||||
| Installer (`install.ps1`, generated by `_install_script()` in `plugins/endpoints/api/siteagent.py`) | Elevated admin, one-shot | `%ProgramData%\ShopDB\Logs\<datetime>-shopdbinstall.log` and the append-only ledger `%ProgramData%\ShopDB\Logs\shopdbagent-installs.log` |
|
||||
|
||||
Rules:
|
||||
|
||||
@@ -314,23 +317,76 @@ Rules:
|
||||
degrades to a silent no-op when it cannot create its directory, so violating
|
||||
this does not raise an error - it discards the log line and the failure
|
||||
becomes invisible.
|
||||
2. **Only the tray collects user context**, because mapped drives, user-owned
|
||||
listeners and idle time are per-session and do not exist in session 0. The
|
||||
service MUST NOT try to descend into the session to collect them.
|
||||
3. **The tray WRITES the spool; the service READS it and then REMOVES it.**
|
||||
The service consumes the file after a successful parse, so a tray that has
|
||||
stopped running cannot keep re-reporting stale mapped drives as current. A
|
||||
file that fails to parse (a read that raced the write) is left in place.
|
||||
4. **The spool location is fixed at `C:\Users\ShopFloor\ShopDB\usercontext.json`.**
|
||||
Every shop-floor endpoint uses the same local `ShopFloor` account. Resolving
|
||||
it through `WTSQueryUserToken` is a fallback only: that call is privileged,
|
||||
endpoint protection blocks it, and when it fails the writer and reader
|
||||
2. **User context is collected in the operator's own logon session**, because
|
||||
mapped drives, user-owned listeners and idle time are per-session and do not
|
||||
exist in session 0. The service MUST NOT try to descend into the session to
|
||||
collect them. The authoritative collector is the `ShopDB user-context probe`
|
||||
scheduled task, registered by the installer against the `BUILTIN\Users` group
|
||||
(`S-1-5-32-545`) at `RunLevel Limited`, triggered at logon and every 30
|
||||
minutes. The tray also probes on its own cadence and writes the same spool;
|
||||
the two are belt-and-braces and the last writer wins.
|
||||
3. **The probe MUST NOT run elevated, and a spool stamped `elevated` is
|
||||
REJECTED.** Drive letters are a property of the logon token, not the machine:
|
||||
an elevated process gets a different token to the operator's interactive one
|
||||
and enumerates a different, usually empty, drive table. This was measured on a
|
||||
live endpoint, where the same two shares reported `Unavailable` from an
|
||||
elevated shell and `OK` from the operator's, in both `Win32_NetworkConnection`
|
||||
and `Get-SmbMapping`. `--collect-usercontext` therefore refuses to run when
|
||||
elevated (exit code 2) rather than producing a confident wrong answer, and the
|
||||
installer MUST NOT start the tray or probe as a child process - it is
|
||||
elevated, and `explorer.exe` de-elevates without changing ACCOUNT, so an
|
||||
install run by an admin produced a probe running as that admin. It MUST
|
||||
instead reach the operator's session through a scheduled task principal.
|
||||
`RunLevel Limited` on the task is load-bearing for the same reason; a task
|
||||
promoted to `Highest` reproduces the bug while still looking registered.
|
||||
4. **The probe WRITES the spool; the service only READS it.** The spool PERSISTS
|
||||
and is overwritten by each probe run. The service MUST NOT delete it: the
|
||||
collector's last-known-good cache is in-memory only, so consuming the file
|
||||
meant a service restart blanked mapped drives until the next probe cycle.
|
||||
Staleness is bounded by the in-file `collectedat` stamp (max age 90 minutes),
|
||||
not by deletion, so a probe that has stopped running still stops re-reporting
|
||||
stale drives - it just does so on a timer instead of on a read.
|
||||
5. **The spool location is fixed at `C:\Users\ShopFloor\ShopDB\usercontext.json`,
|
||||
and writer and reader MUST derive it from the same helper.** Every shop-floor
|
||||
endpoint uses the same local `ShopFloor` account. Resolving the path through
|
||||
`WTSQueryUserToken` or through `%USERPROFILE%` is forbidden: the former is
|
||||
privileged and endpoint protection blocks it, the latter is wrong under a
|
||||
brokered or re-parented tray launch, and in both cases the writer and reader
|
||||
disagree about the location with no error on either side.
|
||||
5. **Losing user context is never fatal.** If the `ShopFloor` profile is absent
|
||||
6. **The probe MUST verify the spool exists after writing it.** A zero exit code
|
||||
from `powershell.exe` proves the interpreter started, not that a file was
|
||||
produced; success is reported only when the file is present on disk.
|
||||
7. **A spool not stamped `ShopFloor` is REJECTED.**
|
||||
collecting `username` and the service discards any spool whose stamp is
|
||||
missing or different. Drive mappings from the wrong session are worse than
|
||||
no drive mappings, so this fails loudly rather than degrading quietly.
|
||||
8. **Losing user context is never fatal.** If the `ShopFloor` profile is absent
|
||||
or unwritable, `usercontext` is omitted and telemetry, commands, inventory
|
||||
and the command channel continue unaffected.
|
||||
6. **The installer provisions** `C:\Users\ShopFloor\ShopDB\` and its `Logs`
|
||||
subdirectory with `ShopFloor: Modify` before the tray first runs.
|
||||
9. **`--collect-usercontext` MUST NOT touch privileged resources.** It runs as
|
||||
the operator and only writes into that operator's own profile, so it is
|
||||
handled before the service's bootstrap logger, fault monitor and config load.
|
||||
It MUST NOT open the SYSTEM-owned log directory, register an EventLog source,
|
||||
or read `CollectorKey` from HKLM - those are precisely the calls an EPM broker
|
||||
intercepts, and the registry read would put the collector secret in a process
|
||||
that has no need of it. It therefore also works on a host whose CollectorKey
|
||||
has not been provisioned yet.
|
||||
10. **The installer provisions** `C:\Users\ShopFloor\ShopDB\` and its `Logs`
|
||||
subdirectory with `ShopFloor: Modify` before the probe first runs.
|
||||
11. **Every install appends one line to the install ledger**, including
|
||||
reinstalls and repeat installs of the same version, recording timestamp,
|
||||
host, installing user, service version, tray version, install directory and
|
||||
the user-context task state (`ok:<principal>/<runlevel>`, `UNVERIFIED:...`,
|
||||
`FAILED:...` or `skipped`). The task is the single point of failure for
|
||||
session-scoped collection, so its registration outcome MUST be recorded
|
||||
where it survives the install transcript.
|
||||
12. **Task registration MUST be verified by reading the task back**, not inferred
|
||||
from `Register-ScheduledTask` returning without error. Policy or a
|
||||
management agent can rewrite or drop a task after registration, and a task
|
||||
that exists but runs with the wrong principal or run level looks healthy in
|
||||
the scheduler UI while producing wrong data.
|
||||
13. **All log filenames use `<stamp>-<component>.log`** so service, tray and
|
||||
installer logs sort chronologically and collect with one date-leading glob.
|
||||
|
||||
The three log destinations are deliberately separate: an install is one-shot and
|
||||
elevated, the service log is a rolling runtime log, and the tray log is the only
|
||||
|
||||
@@ -217,7 +217,14 @@ param(
|
||||
[string] $ServiceName = "ShopDbEndpointAgent",
|
||||
[string] $TrayExeName = "shopdb-endpoint-tray.exe",
|
||||
[string] $CertFileName = {certfile},
|
||||
[switch] $NoTray
|
||||
[switch] $NoTray,
|
||||
|
||||
# Scheduled task that runs the user-context probe INSIDE the operator's own
|
||||
# logon session. Mapped drives are per logon token, so this is the only
|
||||
# reliable way to collect them; see the registration block below.
|
||||
[string] $UserContextTaskName = "ShopDB user-context probe",
|
||||
[int] $UserContextTaskMinutes = 30,
|
||||
[switch] $NoUserContextTask
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -362,10 +369,150 @@ if (-not $NoTray) {{
|
||||
}}
|
||||
catch {{ Write-Warning "could not create Start Menu shortcut: $($_.Exception.Message)" }}
|
||||
|
||||
# Launch the tray once now so it appears without waiting for the next
|
||||
# logon. Single-instance guard makes a duplicate launch a no-op.
|
||||
try {{ Start-Process -FilePath $installedTray -WorkingDirectory $InstallDir }}
|
||||
catch {{ Write-Warning "could not start tray: $($_.Exception.Message)" }}
|
||||
# Deliberately NOT launched here. This installer runs elevated, so
|
||||
# Start-Process would hand the tray an ADMIN token, and drive letters
|
||||
# are per logon token: the probe would then enumerate the installing
|
||||
# admin's drive table, not the operator's. Measured on a live endpoint,
|
||||
# the same two shares reported Unavailable from the elevated shell and
|
||||
# OK from the operator's own session, in both Win32_NetworkConnection
|
||||
# and Get-SmbMapping. Launching via explorer.exe does not fix this
|
||||
# either - it de-elevates but does NOT change account.
|
||||
#
|
||||
# The Run key above is the correct autostart: it fires at a genuine
|
||||
# logon, in the operator's own session. Collection no longer depends on
|
||||
# the tray at all - the scheduled task below owns it.
|
||||
Write-Host " tray will start at next logon (not launched from this elevated install)."
|
||||
}}
|
||||
}}
|
||||
|
||||
# 2b. Register the user-context probe as a scheduled task owned by the Users
|
||||
# GROUP. This is what makes mapped drives, user listeners and idle time
|
||||
# collectable: it is the only mechanism that starts a process in the operator's
|
||||
# own session with the operator's own token.
|
||||
#
|
||||
# RunLevel Limited is load-bearing, not a hardening nicety - the unelevated
|
||||
# token is precisely what sees the operator's mapped drives. GroupId rather
|
||||
# than UserId so the task runs as WHOEVER signs in, each in their own session.
|
||||
$taskState = "skipped"
|
||||
if (-not $NoUserContextTask) {{
|
||||
Write-Host " registering user-context probe task ..."
|
||||
$taskAgentExe = Join-Path $InstallDir "shopdbagent.exe"
|
||||
$taskRegistered = $false
|
||||
$taskDetail = "no principal accepted"
|
||||
try {{
|
||||
$taskAction = New-ScheduledTaskAction -Execute $taskAgentExe -Argument "--collect-usercontext"
|
||||
# AtLogOn covers the normal case. The repeating trigger matters just as
|
||||
# much: AtLogOn does NOT fire for a session that is already signed in,
|
||||
# which is every install onto a machine someone is currently using.
|
||||
$taskTriggers = @(New-ScheduledTaskTrigger -AtLogOn)
|
||||
if ($UserContextTaskMinutes -gt 0) {{
|
||||
$taskTriggers += New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $UserContextTaskMinutes)
|
||||
}}
|
||||
$taskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
|
||||
# BUILTIN\\Users by SID first, then by name: the SID is locale
|
||||
# independent (on non-English Windows the group is not called "Users"),
|
||||
# but a hardened host can still reject it.
|
||||
foreach ($groupId in @("S-1-5-32-545", "BUILTIN\\Users")) {{
|
||||
try {{
|
||||
$taskPrincipal = New-ScheduledTaskPrincipal -GroupId $groupId -RunLevel Limited -ErrorAction Stop
|
||||
Register-ScheduledTask -TaskName $UserContextTaskName -Action $taskAction -Trigger $taskTriggers -Principal $taskPrincipal -Settings $taskSettings -Force -ErrorAction Stop | Out-Null
|
||||
$taskRegistered = $true
|
||||
$taskDetail = $groupId
|
||||
break
|
||||
}}
|
||||
catch {{ $taskDetail = $_.Exception.Message }}
|
||||
}}
|
||||
}}
|
||||
catch {{ $taskDetail = "could not build the task: $($_.Exception.Message)" }}
|
||||
|
||||
if ($taskRegistered) {{
|
||||
# Read the task back rather than trusting Register-ScheduledTask to have
|
||||
# returned cleanly: policy or a management agent can rewrite or drop a
|
||||
# task after registration, and a task that exists but runs elevated
|
||||
# looks healthy in the scheduler UI while producing wrong data.
|
||||
$verified = Get-ScheduledTask -TaskName $UserContextTaskName -ErrorAction SilentlyContinue
|
||||
if (-not $verified) {{
|
||||
$taskState = "UNVERIFIED:task not found after registration"
|
||||
Write-Warning " user-context task registered but cannot be read back."
|
||||
}}
|
||||
elseif ([string] $verified.Principal.RunLevel -eq "Highest") {{
|
||||
$taskState = "UNVERIFIED:elevated"
|
||||
Write-Warning " user-context task runs ELEVATED; mapped drives would be collected from the wrong token."
|
||||
}}
|
||||
elseif (-not $verified.Principal.GroupId) {{
|
||||
$taskState = "UNVERIFIED:no group principal"
|
||||
Write-Warning " user-context task has no group principal; it would run as one specific account."
|
||||
}}
|
||||
else {{
|
||||
$taskState = "ok:$($verified.Principal.GroupId)/$($verified.Principal.RunLevel)"
|
||||
Write-Host " user-context task registered and verified: $taskState"
|
||||
}}
|
||||
|
||||
# AtLogOn will not fire for an already-signed-in session, so kick it once
|
||||
# rather than leaving a fresh install blind until the repeat interval.
|
||||
try {{
|
||||
Start-ScheduledTask -TaskName $UserContextTaskName -ErrorAction Stop
|
||||
Write-Host " user-context task started once for the current session."
|
||||
}}
|
||||
catch {{ Write-Warning " could not start the task now: $($_.Exception.Message); it runs at next logon or interval." }}
|
||||
}}
|
||||
else {{
|
||||
# Not fatal: the service still checks in, only the session-scoped fields
|
||||
# are missing, and the ledger records why.
|
||||
$taskState = "FAILED:$taskDetail"
|
||||
Write-Warning " could not register the user-context task: $taskDetail"
|
||||
Write-Warning " mapped drives and idle time will not be collected."
|
||||
}}
|
||||
}}
|
||||
|
||||
# 2c. Start the tray in the operator's session.
|
||||
#
|
||||
# The Run key registered above only fires at LOGON, so installing onto a machine
|
||||
# someone is already signed in to leaves no tray until they sign out and back in.
|
||||
# The installer cannot start it directly - it is elevated, and the tray must run
|
||||
# unelevated in the user's own session - so we borrow the same mechanism that
|
||||
# solves this for the probe: a transient scheduled task with a Limited group
|
||||
# principal, run once and then deleted.
|
||||
#
|
||||
# Registered without a trigger; it exists only to be invoked immediately. The
|
||||
# tray is single-instance, so if one is somehow already running this is a no-op.
|
||||
if (-not $NoTray -and (Test-Path (Join-Path $InstallDir $TrayExeName))) {{
|
||||
$launchTaskName = "ShopDB tray first launch"
|
||||
try {{
|
||||
$launchExe = Join-Path $InstallDir $TrayExeName
|
||||
$launchAction = New-ScheduledTaskAction -Execute $launchExe -WorkingDirectory $InstallDir
|
||||
$launchSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
$launched = $false
|
||||
foreach ($groupId in @("S-1-5-32-545", "BUILTIN\\Users")) {{
|
||||
try {{
|
||||
$launchPrincipal = New-ScheduledTaskPrincipal -GroupId $groupId -RunLevel Limited -ErrorAction Stop
|
||||
Register-ScheduledTask -TaskName $launchTaskName -Action $launchAction -Principal $launchPrincipal -Settings $launchSettings -Force -ErrorAction Stop | Out-Null
|
||||
Start-ScheduledTask -TaskName $launchTaskName -ErrorAction Stop
|
||||
$launched = $true
|
||||
break
|
||||
}}
|
||||
catch {{ }}
|
||||
}}
|
||||
|
||||
# Give the task a moment to spawn the tray before removing it: deleting a
|
||||
# task that is still starting can kill the process it just launched.
|
||||
if ($launched) {{
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host " tray started in the operator's session."
|
||||
}}
|
||||
else {{
|
||||
Write-Warning " could not start the tray now; it will start at next logon via the Run key."
|
||||
}}
|
||||
|
||||
# Transient by design: this is a one-shot launcher, not an autostart.
|
||||
# The Run key owns autostart, and leaving this behind would mean two
|
||||
# things racing to start the same single-instance tray at every logon.
|
||||
try {{ Unregister-ScheduledTask -TaskName $launchTaskName -Confirm:$false -ErrorAction Stop }}
|
||||
catch {{ Write-Warning " could not remove the transient launch task '$launchTaskName'; remove it manually." }}
|
||||
}}
|
||||
catch {{
|
||||
Write-Warning " could not start the tray now: $($_.Exception.Message); it will start at next logon."
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -397,15 +544,22 @@ Write-Host " service is running." -ForegroundColor Green
|
||||
|
||||
$trayBase2 = [System.IO.Path]::GetFileNameWithoutExtension($TrayExeName)
|
||||
if (-not $NoTray -and (Test-Path (Join-Path $InstallDir $TrayExeName))) {{
|
||||
$trayOk = $false
|
||||
for ($attempt = 1; $attempt -le 5; $attempt++) {{
|
||||
if (Get-Process -Name $trayBase2 -ErrorAction SilentlyContinue) {{ $trayOk = $true; break }}
|
||||
Write-Host " tray not running yet (attempt $attempt/5); relaunching ..."
|
||||
try {{ Start-Process -FilePath (Join-Path $InstallDir $TrayExeName) -WorkingDirectory $InstallDir }} catch {{ }}
|
||||
# Report only; never relaunch from here. This block used to Start-Process the
|
||||
# tray when it was not found running, which would hand it this installer's
|
||||
# ADMIN token and reintroduce the wrong-drive-table bug. The launch task
|
||||
# above is the only place the tray is started during install, and it starts
|
||||
# it correctly - unelevated, in the operator's session.
|
||||
$trayFound = $false
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {{
|
||||
if (Get-Process -Name $trayBase2 -ErrorAction SilentlyContinue) {{ $trayFound = $true; break }}
|
||||
Start-Sleep -Seconds 2
|
||||
}}
|
||||
if ($trayOk) {{ Write-Host " tray is running." -ForegroundColor Green }}
|
||||
else {{ Write-Warning "tray did not confirm running; it will retry at next logon via the Run key." }}
|
||||
if ($trayFound) {{
|
||||
Write-Host " tray is running." -ForegroundColor Green
|
||||
}}
|
||||
else {{
|
||||
Write-Warning "tray did not confirm running; it will start at next logon via the Run key."
|
||||
}}
|
||||
}}
|
||||
|
||||
# 5. Preserve the install artifacts (minus the secret-bearing install.ps1) under
|
||||
@@ -432,6 +586,29 @@ if (Test-Path $installSelf) {{
|
||||
}}
|
||||
|
||||
Write-Host "[6/6] Done." -ForegroundColor Green
|
||||
|
||||
# Append one line per install, including reinstalls and repeat installs of the
|
||||
# same version. The user-context task state is recorded because it is the single
|
||||
# point of failure for session-scoped collection: if mapped drives go missing
|
||||
# later, this answers "was the task ever registered on this host, and as what"
|
||||
# without needing the endpoint back. The ledger is a record, not a gate - a
|
||||
# failure to append must never fail an otherwise-good install.
|
||||
try {{
|
||||
$ledgerDir = Join-Path $env:ProgramData "ShopDB\\Logs"
|
||||
New-Item -ItemType Directory -Force -Path $ledgerDir | Out-Null
|
||||
$ledgerPath = Join-Path $ledgerDir "shopdbagent-installs.log"
|
||||
$ledgerAgentVersion = "unknown"
|
||||
if (Test-Path $installedAgent) {{ $ledgerAgentVersion = (Get-Item $installedAgent).VersionInfo.FileVersion }}
|
||||
$ledgerTrayVersion = "not-installed"
|
||||
$ledgerTrayPath = Join-Path $InstallDir $TrayExeName
|
||||
if (Test-Path $ledgerTrayPath) {{ $ledgerTrayVersion = (Get-Item $ledgerTrayPath).VersionInfo.FileVersion }}
|
||||
$ledgerLine = "{{0}} host={{1}} installer={{2}} service={{3}} tray={{4}} dir={{5}} usercontexttask={{6}}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz"), $env:COMPUTERNAME, $env:USERNAME, $ledgerAgentVersion, $ledgerTrayVersion, $InstallDir, $taskState
|
||||
Add-Content -Path $ledgerPath -Value $ledgerLine -Encoding UTF8
|
||||
Write-Host "Install ledger: $ledgerPath"
|
||||
Write-Host " $ledgerLine" -ForegroundColor DarkGray
|
||||
}}
|
||||
catch {{ Write-Warning "could not append to the install ledger: $($_.Exception.Message)" }}
|
||||
|
||||
if (Test-Path $uninstaller) {{
|
||||
Write-Host "Uninstaller: $uninstaller" -ForegroundColor Cyan
|
||||
Write-Host " To remove the agent, run that script from an elevated PowerShell."
|
||||
|
||||
Reference in New Issue
Block a user