diff --git a/agent/ShopDbEndpointAgent.Tray/Program.cs b/agent/ShopDbEndpointAgent.Tray/Program.cs index 6e4e5840..4e8cdc0a 100644 --- a/agent/ShopDbEndpointAgent.Tray/Program.cs +++ b/agent/ShopDbEndpointAgent.Tray/Program.cs @@ -19,8 +19,20 @@ internal static class Program private const string SingleInstanceMutexName = @"Global\ShopDbEndpointAgentTray"; [STAThread] - private static void Main() + private static void Main(string[] args) { + // Headless icon export for the installer: `--export-icon ` writes + // the branded .ico and exits without starting the tray, so install.ps1 + // can point the Start Menu shortcut at a real brand icon. Kept in this + // exe so the icon has a single source (TrayIcon) and no binary is + // checked into the repo. + if (args.Length >= 2 && + string.Equals(args[0], "--export-icon", StringComparison.OrdinalIgnoreCase)) + { + TrayIcon.Export(args[1]); + return; + } + using var singleInstance = new Mutex(true, SingleInstanceMutexName, out bool owned); if (!owned) { diff --git a/agent/ShopDbEndpointAgent.Tray/TrayIcon.cs b/agent/ShopDbEndpointAgent.Tray/TrayIcon.cs index dbc0957b..b00e8bb9 100644 --- a/agent/ShopDbEndpointAgent.Tray/TrayIcon.cs +++ b/agent/ShopDbEndpointAgent.Tray/TrayIcon.cs @@ -62,6 +62,22 @@ internal static class TrayIcon } } + // Writes the branded icon to an .ico file so a Start Menu / desktop shortcut + // can show the brand instead of the generic exe icon. Reuses the same GDI+ + // drawing as the tray so there is still one source of brand truth and no + // checked-in binary. 64px suits shortcut/large-icon views; the OS downscales. + public static void Export(string path, int size = 64) + { + using var icon = Create(size); + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); + icon.Save(stream); + } + private static GraphicsPath RoundedRect(RectangleF rect, float radius) { float diameter = radius * 2; diff --git a/plugins/endpoints/api/siteagent.py b/plugins/endpoints/api/siteagent.py index 9fa82544..f0419a68 100644 --- a/plugins/endpoints/api/siteagent.py +++ b/plugins/endpoints/api/siteagent.py @@ -267,7 +267,7 @@ Write-Host "ServiceName : $ServiceName" # 0. Stop and remove any previous install so the file copy is a clean overwrite. $existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($existing) {{ - Write-Host "[0/4] Removing existing service $ServiceName ..." + Write-Host "[0/6] Removing existing service $ServiceName ..." if ($existing.Status -ne "Stopped") {{ try {{ Stop-Service -Name $ServiceName -Force -ErrorAction Stop }} catch {{ Write-Warning "could not stop service: $($_.Exception.Message)" }} @@ -283,7 +283,7 @@ foreach ($name in @("shopdbagent", $trayBase)) {{ }} # 1. Provision registry credentials (both 64-bit and 32-bit views). -Write-Host "[1/4] Provisioning registry HKLM:\\SOFTWARE\\GE\\ShopDB (Registry64 + Registry32) ..." +Write-Host "[1/6] Provisioning registry HKLM:\\SOFTWARE\\GE\\ShopDB (Registry64 + Registry32) ..." Set-HklmValueBothViews -SubKey "SOFTWARE\\GE\\ShopDB" -Name "BaseUrl" -Value $BaseUrl Set-HklmValueBothViews -SubKey "SOFTWARE\\GE\\ShopDB" -Name "CollectorKey" -Value $CollectorKey @@ -307,7 +307,7 @@ if (-not [string]::IsNullOrWhiteSpace($CertFileName)) {{ }} # 2. Copy agent files to the install directory. -Write-Host "[2/4] Copying agent files to $InstallDir ..." +Write-Host "[2/6] Copying agent files to $InstallDir ..." New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null Get-ChildItem -Path $sourceDir -File | ForEach-Object {{ try {{ Unblock-File -Path $_.FullName -ErrorAction Stop }} catch {{ }} @@ -335,6 +335,14 @@ if (-not $NoTray) {{ # second launch is harmless. Set-HklmValueBothViews -SubKey "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" -Name "ShopDbEndpointTray" -Value ('"' + $installedTray + '"') + # Export the branded icon from the tray exe so the shortcut shows the + # ShopDB brand instead of the generic exe icon. The tray draws its icon + # at runtime (no embedded Win32 icon), so we ask it to write an .ico into + # the install dir and point the shortcut at it. + $iconPath = Join-Path $InstallDir "shopdb-agent.ico" + try {{ & $installedTray --export-icon $iconPath | Out-Null }} + catch {{ Write-Warning "could not export shortcut icon: $($_.Exception.Message)" }} + # 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. @@ -346,6 +354,7 @@ if (-not $NoTray) {{ $shortcut.TargetPath = $installedTray $shortcut.WorkingDirectory = $InstallDir $shortcut.Description = "ShopDB Endpoint Agent tray" + if (Test-Path $iconPath) {{ $shortcut.IconLocation = "$iconPath,0" }} $shortcut.Save() Write-Host " created Start Menu shortcut: $shortcutPath" }} @@ -359,13 +368,75 @@ if (-not $NoTray) {{ }} # 3. Install and start the service. The agent supports its own service install. -Write-Host "[3/4] Installing and starting the service ..." +Write-Host "[3/6] Installing and starting the service ..." $installedAgent = Join-Path $InstallDir "shopdbagent.exe" $binPath = '"' + $installedAgent + '"' sc.exe create $ServiceName binPath= $binPath start= auto DisplayName= "ShopDB Endpoint Agent" | Out-Null sc.exe start $ServiceName | Out-Null -Write-Host "[4/4] Done." -ForegroundColor Green +# 4. Confirm the service (and tray, when installed) actually reached a running +# state before we destroy the installer. sc.exe start returns before the service +# is up, and a failed start would otherwise leave a half-installed endpoint with +# the secret-bearing install.ps1 already deleted. Poll with a short backoff and +# retry the start a few times; only proceed to cleanup once healthy. +Write-Host "[4/6] Verifying agent runtime ..." +$serviceOk = $false +for ($attempt = 1; $attempt -le 5; $attempt++) {{ + Start-Sleep -Seconds 3 + $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -eq "Running") {{ $serviceOk = $true; break }} + Write-Host " service not running yet (attempt $attempt/5); retrying start ..." + try {{ Start-Service -Name $ServiceName -ErrorAction Stop }} catch {{ }} +}} +if (-not $serviceOk) {{ + throw "Service $ServiceName did not reach Running after 5 attempts. Leaving install.ps1 in place for diagnosis; re-run once resolved." +}} +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 {{ }} + 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." }} +}} + +# 5. Preserve the install artifacts (minus the secret-bearing install.ps1) under +# ProgramData so the uninstaller and appsettings/cert are available later, then +# scrub install.ps1 from wherever it was run. install.ps1 carries the live +# collector key in its default parameter, so it must not linger on the endpoint. +Write-Host "[5/6] Archiving install artifacts and removing install.ps1 ..." +$dataDir = Join-Path $env:ProgramData "ShopDB\\EndpointAgent" +New-Item -ItemType Directory -Force -Path $dataDir | Out-Null +foreach ($name in @("uninstall-shopdbagent.ps1", "appsettings.json", "README.txt", $CertFileName)) {{ + if ([string]::IsNullOrWhiteSpace($name)) {{ continue }} + $src = Join-Path $sourceDir $name + if (Test-Path $src) {{ + try {{ Copy-Item -Path $src -Destination $dataDir -Force }} + catch {{ Write-Warning "could not archive $name : $($_.Exception.Message)" }} + }} +}} +$uninstaller = Join-Path $dataDir "uninstall-shopdbagent.ps1" + +$installSelf = Join-Path $sourceDir "install.ps1" +if (Test-Path $installSelf) {{ + try {{ Remove-Item -Path $installSelf -Force -ErrorAction Stop; Write-Host " removed install.ps1 (carried the collector key)." }} + catch {{ Write-Warning "could not remove install.ps1 - delete it manually, it contains the collector key: $($_.Exception.Message)" }} +}} + +Write-Host "[6/6] Done." -ForegroundColor Green +if (Test-Path $uninstaller) {{ + Write-Host "Uninstaller: $uninstaller" -ForegroundColor Cyan + Write-Host " To remove the agent, run that script from an elevated PowerShell." +}} +else {{ + Write-Warning "uninstaller was not bundled; fetch uninstall-shopdbagent.ps1 from the ShopDB repo scripts/ folder if needed." +}} """.format( baseurl=baseurl_literal, collectorkey=collectorkey_literal,