Ship uninstaller in agent bundle and harden tray launch
The site-agent bundle now includes uninstall-shopdbagent.ps1 so operators can cleanly remove an install without hunting for the repo script. The packaging route reads it from scripts/, which was not mounted into the api container, so add a read-only ./scripts:/app/scripts mount (recreate the container to apply). install.ps1 now registers the tray to autostart for every user via the HKLM Run value ShopDbEndpointTray, adds an All-Users 'ShopDB Agent' Start Menu shortcut for manual relaunch, and launches the tray once post-install. The tray enforces a single instance via a Global named mutex so repeated launches no longer stack duplicate icons. The uninstaller removes the Start Menu shortcut alongside the existing Run key and credential cleanup.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace ShopDbEndpointAgent.Tray;
|
||||
|
||||
// Interactive user-session tray app (SCAFFOLD). Separate process from the
|
||||
// Interactive user-session tray app (SCAFFOLD).
|
||||
// headless Windows Service: the service owns telemetry, this owns the operator
|
||||
// surface - a system-tray icon and the 5-click-max ServiceNow ticket flow
|
||||
// (DESIGN-DECISIONS-agent.md section 6). This iteration wires the tray and a
|
||||
@@ -8,9 +10,23 @@ namespace ShopDbEndpointAgent.Tray;
|
||||
// is deferred until that decision is made server-side.
|
||||
internal static class Program
|
||||
{
|
||||
// Machine-wide single-instance guard. The installer starts the tray from an
|
||||
// HKLM Run value for every user at logon, and an operator can also launch it
|
||||
// from the Start Menu shortcut, so a second copy is easy to spawn. A named
|
||||
// mutex in the Global namespace keeps exactly one tray per session: a later
|
||||
// launch that cannot own the mutex exits immediately instead of stacking a
|
||||
// duplicate icon. Global\ scopes across sessions on the same machine.
|
||||
private const string SingleInstanceMutexName = @"Global\ShopDbEndpointAgentTray";
|
||||
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
using var singleInstance = new Mutex(true, SingleInstanceMutexName, out bool owned);
|
||||
if (!owned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new TrayContext());
|
||||
}
|
||||
|
||||
@@ -99,6 +99,11 @@ services:
|
||||
retries: 3
|
||||
volumes:
|
||||
- ./plugins:/app/plugins:ro
|
||||
# Standalone operator scripts shipped inside generated bundles (the
|
||||
# endpoint-agent uninstaller). Read-only, same pattern as plugins: the
|
||||
# site-agent packaging route reads uninstall-shopdbagent.ps1 from here at
|
||||
# download time, so it must be visible in the container.
|
||||
- ./scripts:/app/scripts:ro
|
||||
# Self-signed TLS material for the dev/test https listener. Read-only;
|
||||
# generated on the host under ./certs and never baked into the image.
|
||||
- ./certs:/app/certs:ro
|
||||
|
||||
@@ -40,6 +40,14 @@ from .routes import endpoints_bp
|
||||
STAGED_AGENT_RELPATH = os.path.join('siteagent', 'shopdbagent.exe')
|
||||
STAGED_TRAY_RELPATH = os.path.join('siteagent', 'shopdb-endpoint-tray.exe')
|
||||
|
||||
# The standalone uninstaller lives in the repo tree (not staged per-instance),
|
||||
# so it ships in every bundle. Resolve it relative to this file:
|
||||
# plugins/endpoints/api/siteagent.py -> repo root is three levels up.
|
||||
UNINSTALLER_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))),
|
||||
'scripts', 'uninstall-shopdbagent.ps1')
|
||||
|
||||
# Public TLS certificate the agent must trust when this deployment terminates
|
||||
# TLS itself with a self-signed or otherwise untrusted certificate. We can only
|
||||
# auto-bundle when THIS app owns the cert file (the dev topology, where gunicorn
|
||||
@@ -143,6 +151,12 @@ FILES
|
||||
appsettings.json Non-secret runtime config (cadence, monitors,
|
||||
launch paths). No credentials.
|
||||
install.ps1 Provisions the credentials and installs the service.
|
||||
uninstall-shopdbagent.ps1
|
||||
Standalone uninstaller. Run elevated to reverse the
|
||||
install: stops and deletes the service, kills agent
|
||||
and tray processes, removes the tray autostart Run
|
||||
key, the Start Menu shortcut, and the credential
|
||||
hive, and deletes the install directory.
|
||||
server.crt Server TLS certificate (present only for self-signed
|
||||
or internal-CA deployments, either auto-detected or
|
||||
uploaded by the operator). install.ps1 imports it
|
||||
@@ -153,7 +167,13 @@ USAGE
|
||||
1. Copy this whole folder to the target endpoint.
|
||||
2. Right-click install.ps1 and Run with PowerShell as administrator. It
|
||||
writes the server URL and collector key to HKLM\\SOFTWARE\\GE\\ShopDB,
|
||||
copies the agent into Program Files, and starts the service.
|
||||
copies the agent into Program Files, and starts the service. It also
|
||||
registers the tray to autostart for every user at logon and adds a
|
||||
"ShopDB Agent" Start Menu shortcut for manual relaunch.
|
||||
|
||||
UNINSTALL
|
||||
Run uninstall-shopdbagent.ps1 from an elevated PowerShell to fully remove the
|
||||
agent, tray autostart, Start Menu shortcut, and credentials.
|
||||
|
||||
SECURITY
|
||||
install.ps1 carries a live collector key in its default -CollectorKey value.
|
||||
@@ -305,6 +325,36 @@ if (-not $NoTray) {{
|
||||
$traySource = Join-Path $sourceDir $TrayExeName
|
||||
if (Test-Path $traySource) {{
|
||||
Copy-Item -Path $traySource -Destination $InstallDir -Force
|
||||
$installedTray = Join-Path $InstallDir $TrayExeName
|
||||
|
||||
# Autostart the tray for every user at logon via the HKLM Run key (both
|
||||
# 64-bit and 32-bit views, matching the credential hive). This is the
|
||||
# reliable launch guarantee: a plain Start Menu shortcut does not
|
||||
# autostart, and the SYSTEM service cannot cleanly spawn into the
|
||||
# interactive session. The tray itself is single-instance, so a stray
|
||||
# second launch is harmless.
|
||||
Set-HklmValueBothViews -SubKey "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" -Name "ShopDbEndpointTray" -Value ('"' + $installedTray + '"')
|
||||
|
||||
# Start Menu shortcut so operators can relaunch the tray on demand. This
|
||||
# is discoverability only, not autostart. Written to the machine-wide
|
||||
# (All Users) Programs folder.
|
||||
try {{
|
||||
$commonPrograms = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonPrograms)
|
||||
$shortcutPath = Join-Path $commonPrograms "ShopDB Agent.lnk"
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $shell.CreateShortcut($shortcutPath)
|
||||
$shortcut.TargetPath = $installedTray
|
||||
$shortcut.WorkingDirectory = $InstallDir
|
||||
$shortcut.Description = "ShopDB Endpoint Agent tray"
|
||||
$shortcut.Save()
|
||||
Write-Host " created Start Menu shortcut: $shortcutPath"
|
||||
}}
|
||||
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)" }}
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -420,6 +470,9 @@ def package_siteagent():
|
||||
cadenceseconds, certfilename))
|
||||
archive.writestr('README.txt', _README)
|
||||
|
||||
if os.path.isfile(UNINSTALLER_PATH):
|
||||
archive.write(UNINSTALLER_PATH, 'uninstall-shopdbagent.ps1')
|
||||
|
||||
agent = _staged_path(STAGED_AGENT_RELPATH)
|
||||
if os.path.isfile(agent):
|
||||
archive.write(agent, 'shopdbagent.exe')
|
||||
|
||||
@@ -114,6 +114,14 @@ Start-Sleep -Seconds 1
|
||||
Write-Host "[3/6] Removing registry entries ..."
|
||||
Remove-HklmBothViews -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "ShopDbEndpointTray"
|
||||
Remove-HklmBothViews -SubKey "SOFTWARE\GE\ShopDB"
|
||||
|
||||
# Remove the All-Users Start Menu shortcut the site-agent installer created.
|
||||
$commonPrograms = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonPrograms)
|
||||
$shortcutPath = Join-Path $commonPrograms "ShopDB Agent.lnk"
|
||||
if (Test-Path $shortcutPath) {
|
||||
try { Remove-Item -Path $shortcutPath -Force -ErrorAction Stop; Write-Host " removed Start Menu shortcut $shortcutPath" }
|
||||
catch { Write-Warning " could not remove Start Menu shortcut: $($_.Exception.Message)" }
|
||||
}
|
||||
# Drop the parent GE key only if we emptied it (some sites nest siblings under
|
||||
# GE such as SFLD; never tree-delete a key we do not own).
|
||||
foreach ($view in @([Microsoft.Win32.RegistryView]::Registry64,
|
||||
|
||||
Reference in New Issue
Block a user