A part marker has no machine number of its own - the device is what a backup belongs to, and C:\Enrollment\asset-id.txt is what names it. Reading that file inside the shared machine-number lookup looked like the tidy place for it and was wrong: marker bays run eDNC too, so Backup-NtlarsSettings.ps1 reaches the same lookup, and it would have started posting a machine's DNC settings under the marker's asset number where today it exits quietly. The marker script asks for the fallback explicitly instead, which is what -MachineNumberFallback is for. Also from review: Returning a byte array bare lets the pipeline enumerate it, so an EMPTY file came back as $null, ComputeHash threw, and with ErrorActionPreference=Continue the manifest entry silently kept the previous file's hash - a corrupted manifest stored as a good revision. Empty parameter files are plausible. Reading registry values directly under StrictMode throws when one is absent, so the "no Port Id" guard was unreachable and a partially configured marker bay reported itself as having no marker at all, permanently and quietly. Values are now read through a presence check with the fleet's own defaults. The outside-window quiet state embedded the hour, so the say-once module said it hourly. Command replies now have their checksum verified like data blocks do, since a corrupted listing entry would otherwise be trusted and a file requested under a name the controller does not have. A controller that genuinely holds nothing is no longer reported as a failed capture. A snapshot that could not be written to the share now counts as a missed backup like the rest, and the header no longer claims orphaned snapshots get tidied - they are kept on purpose, because a timed-out post may still have been recorded.
527 lines
22 KiB
PowerShell
527 lines
22 KiB
PowerShell
# ShopdbBackupClient - the client half of the ShopDB asset-backup contract.
|
|
#
|
|
# WHY THIS EXISTS
|
|
#
|
|
# ShopDB's backups plugin is a pluggable registry: a "kind" declares how a
|
|
# config is parsed, rendered and resolved to an asset, and in return gets
|
|
# revision history, content dedup, retention, diffs and an asset panel. The
|
|
# server half is a clean contract. The client half was not: exactly one script
|
|
# on this share posted backups (Backup-NtlarsSettings), and everything around
|
|
# the post - finding the collector key, reading the interval, throttling,
|
|
# logging - lived inside it, bespoke.
|
|
#
|
|
# The cost showed up the day someone looked. The interval came from a public
|
|
# settings key the plugin never declared public, so the read silently fell back
|
|
# to a hardcoded 24 hours and the setting did nothing for months. The log wrote
|
|
# three lines every five minutes whatever happened, reaching 3,234 lines of
|
|
# which 3,217 were the same "Throttled" line. Seven other Backup-*.ps1 scripts
|
|
# on this share capture device configs and post NONE of them, so nothing about
|
|
# a CMM, a PC-DMIS bay or an MTConnect box has any history in ShopDB.
|
|
#
|
|
# Wiring those seven the old way would have produced seven more copies of all
|
|
# of the above. So the shared parts live here, once, and a per-device script
|
|
# becomes: capture the config, call Send-ShopdbBackup.
|
|
#
|
|
# USAGE
|
|
#
|
|
# Import-Module "$PSScriptRoot\..\lib\ShopdbBackupClient.psm1" -Force
|
|
# $ctx = Initialize-ShopdbBackup -Kind 'gocmm'
|
|
# if (-not $ctx.Proceed) { exit 0 } # quiet exit, already logged
|
|
# $bytes = [IO.File]::ReadAllBytes($configPath)
|
|
# Send-ShopdbBackup -Context $ctx -Bytes $bytes -SourceFileName 'settings.xml'
|
|
#
|
|
# Initialize-ShopdbBackup does every check that can say "nothing to do today":
|
|
# machine number, collector key, base URL, throttle. Each of those is logged
|
|
# ONCE and then stays quiet while it holds, so a PC with no NTLARS does not
|
|
# write the same line 288 times a day.
|
|
#
|
|
# PER-KIND STATE, deliberately. Marker, state and log files are all named from
|
|
# the kind, so a bay running two backup kinds does not have them fighting over
|
|
# one marker file - which is what a single fixed name would have caused the
|
|
# first time a second kind shipped.
|
|
|
|
Set-StrictMode -Version Latest
|
|
|
|
$script:LOGDIR = 'C:\Logs\Shopfloor'
|
|
$script:SHOPDBREG = 'HKLM:\SOFTWARE\GE\ShopDB'
|
|
$script:KEYFILE = 'C:\Enrollment\shopdb-key.txt'
|
|
$script:PCCONFIG = 'C:\Enrollment\pc-config.txt'
|
|
# The DEVICE this PC drives, by asset number - a part marker, a CMM, a Keyence.
|
|
# Written at staging and read by the computers collector, which resolves it with
|
|
# _asset_by_number and refuses a name it cannot find rather than minting a
|
|
# phantom. Marker and measuring-tool bays have no machine number of their own
|
|
# ("keyence / genspect / part-marker have no per-bay id" in
|
|
# Report-AssetToShopDB.ps1), so for those this file IS the identity.
|
|
$script:ASSETIDFILE = 'C:\Enrollment\asset-id.txt'
|
|
# 0.12.0 shipped this name before the file was generalised; a bay staged then
|
|
# still has it.
|
|
$script:LEGACYASSETIDFILE = 'C:\Enrollment\measuringtool-id.txt'
|
|
|
|
# The action ShopDB reported for the most recent post, per kind: 'created',
|
|
# 'noop', or '' when nothing has been posted. Send-ShopdbBackup returns only
|
|
# accepted/not-accepted, and changing that would break its existing caller - but
|
|
# a share kind has to know the difference, because it decides whether the
|
|
# snapshot directory it just wrote is a new revision or a duplicate to remove.
|
|
$script:LASTACTION = @{}
|
|
|
|
|
|
function Get-ShopdbLogPath {
|
|
param([Parameter(Mandatory)][string]$Kind)
|
|
# Date-stamped at source. An append-only name can never age out of the
|
|
# GE-Enforce retention sweep, which drops by LastWriteTime - a file written
|
|
# every cycle is always "recent" and grows forever.
|
|
Join-Path $script:LOGDIR ('{0}-backup-{1}.log' -f $Kind, (Get-Date -Format 'yyyyMMdd'))
|
|
}
|
|
|
|
|
|
function Write-ShopdbBackupLog {
|
|
param([Parameter(Mandatory)][string]$Kind,
|
|
[Parameter(Mandatory)][string]$Message)
|
|
if (-not (Test-Path $script:LOGDIR)) {
|
|
New-Item -ItemType Directory -Path $script:LOGDIR -Force -EA SilentlyContinue | Out-Null
|
|
}
|
|
$line = '[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
|
Add-Content -Path (Get-ShopdbLogPath -Kind $Kind) -Value $line -EA SilentlyContinue
|
|
Write-Host $line
|
|
}
|
|
|
|
|
|
function Write-ShopdbQuietState {
|
|
<#
|
|
Log a no-change outcome once, then stay silent while it holds.
|
|
Returns nothing; the caller exits on a quiet outcome.
|
|
|
|
GE-Enforce runs every 5 minutes. An outcome that does not change is
|
|
worth saying once, not 288 times a day. Any real event clears the state
|
|
so the next quiet spell announces itself - otherwise a PC that stopped
|
|
working would look exactly like one still posting.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind,
|
|
[Parameter(Mandatory)][string]$State,
|
|
[Parameter(Mandatory)][string]$Message)
|
|
$statefile = Join-Path $script:LOGDIR ('{0}-backup.state' -f $Kind)
|
|
$previous = ''
|
|
if (Test-Path $statefile) {
|
|
try { $previous = (Get-Content -LiteralPath $statefile -First 1 -EA Stop).Trim() } catch { }
|
|
}
|
|
if ($previous -ne $State) {
|
|
Write-ShopdbBackupLog -Kind $Kind -Message $Message
|
|
Set-Content -Path $statefile -Value $State -EA SilentlyContinue
|
|
}
|
|
}
|
|
|
|
|
|
function Clear-ShopdbQuietState {
|
|
param([Parameter(Mandatory)][string]$Kind)
|
|
Remove-Item -LiteralPath (Join-Path $script:LOGDIR ('{0}-backup.state' -f $Kind)) `
|
|
-Force -EA SilentlyContinue
|
|
}
|
|
|
|
|
|
function Get-ShopdbRegValue {
|
|
<#
|
|
One value from HKLM:\SOFTWARE\GE\ShopDB.
|
|
|
|
The SAME contract the display kiosks use: Install-ShopdbKiosk writes
|
|
BaseUrl / ApiToken / CollectorKey there and ACLs it to SYSTEM and
|
|
Administrators. Machine bays read the same place rather than inventing
|
|
a second home for one secret, so one delivery mechanism serves the whole
|
|
fleet and the diagnostics collector already knows to redact it.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Name)
|
|
try {
|
|
$props = Get-ItemProperty -Path $script:SHOPDBREG -EA Stop
|
|
if ($props.PSObject.Properties.Name -contains $Name) {
|
|
$val = [string]$props.$Name
|
|
if ($val) { return $val.Trim() }
|
|
}
|
|
} catch { }
|
|
return ''
|
|
}
|
|
|
|
|
|
function Get-ShopdbCollectorKey {
|
|
# Registry first (how the fleet is provisioned), then the enrollment file
|
|
# for a bay staged before that existed.
|
|
$key = Get-ShopdbRegValue -Name 'CollectorKey'
|
|
if ($key) { return $key }
|
|
if (Test-Path $script:KEYFILE) {
|
|
foreach ($line in (Get-Content -LiteralPath $script:KEYFILE -EA SilentlyContinue)) {
|
|
if ($line -match '^\s*collector\s*=\s*(.+)$') { return $matches[1].Trim() }
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
|
|
function Get-ShopdbAssetId {
|
|
<#
|
|
The asset number of the device this PC drives, or ''.
|
|
|
|
Same file and same precedence the computers collector uses, so a bay
|
|
cannot report its marker under one identity and its backup under
|
|
another.
|
|
#>
|
|
foreach ($file in @($script:ASSETIDFILE, $script:LEGACYASSETIDFILE)) {
|
|
if (Test-Path $file) {
|
|
try {
|
|
$value = ([string](Get-Content -LiteralPath $file -First 1 -EA Stop)).Trim()
|
|
if ($value) { return $value }
|
|
} catch { }
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
|
|
function Get-ShopdbMachineNumber {
|
|
<#
|
|
The machine number this PC reports, from pc-config.txt.
|
|
|
|
A device-specific fallback can be supplied when the application itself
|
|
knows the number - NTLARS keeps one in its own registry key. Callers
|
|
without one just get '' and exit quietly.
|
|
#>
|
|
param([scriptblock]$Fallback)
|
|
if (Test-Path $script:PCCONFIG) {
|
|
foreach ($line in (Get-Content -LiteralPath $script:PCCONFIG -EA SilentlyContinue)) {
|
|
if ($line -match '^\s*machine(number|no)?\s*=\s*(.+)$') {
|
|
$val = $matches[2].Trim()
|
|
# 9999 is the imaging-time placeholder, not a real bay.
|
|
if ($val -and $val -ne '9999') { return $val }
|
|
}
|
|
}
|
|
}
|
|
if ($Fallback) {
|
|
try {
|
|
$val = & $Fallback
|
|
if ($val) { return ([string]$val).Trim() }
|
|
} catch { }
|
|
}
|
|
|
|
# NO asset-id.txt fallback here, deliberately. It belongs to the CALLER.
|
|
#
|
|
# A part-marker bay has no machine number of its own but does have the DNC
|
|
# registry tree, so Backup-NtlarsSettings.ps1 passes its key check and
|
|
# reaches this function. A shared fallback would make it post the machine's
|
|
# DNC settings under the MARKER's asset number - a kind that resolves by
|
|
# machine number, handed a device asset number - where today it exits
|
|
# quietly. That is a behaviour change to a script running in production.
|
|
#
|
|
# A kind that legitimately identifies itself by device passes
|
|
# -MachineNumberFallback { Get-ShopdbAssetId } instead. The per-kind
|
|
# mechanism already exists; this is what it is for.
|
|
return ''
|
|
}
|
|
|
|
|
|
function Get-ShopdbIntervalHours {
|
|
<#
|
|
Minimum hours between attempts, from the backups_intervalhours setting.
|
|
|
|
Read UNAUTHENTICATED from /api/settings/public, because this runs before
|
|
any credential is needed. That endpoint serves an allowlist, and the key
|
|
must be declared public by the plugin - it was not, for months, so this
|
|
read returned nothing and the fallback below silently governed the whole
|
|
fleet. The fallback stays (an unreachable server must not mean a hot
|
|
loop) but a miss is now LOGGED rather than swallowed, so the same
|
|
failure cannot hide again.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind,
|
|
[Parameter(Mandatory)][string]$BaseUrl,
|
|
[int]$Default = 24)
|
|
try {
|
|
$resp = Invoke-RestMethod -Uri "$BaseUrl/api/settings/public" -Method Get `
|
|
-TimeoutSec 10 -EA Stop
|
|
$val = $null
|
|
if ($resp -and $resp.PSObject.Properties.Name -contains 'data') {
|
|
if ($resp.data.PSObject.Properties.Name -contains 'backups_intervalhours') {
|
|
$val = $resp.data.backups_intervalhours
|
|
}
|
|
}
|
|
if ($val) { return [int]$val }
|
|
Write-ShopdbQuietState -Kind $Kind -State 'interval-not-public' -Message (
|
|
'backups_intervalhours is not readable from /api/settings/public; ' +
|
|
"using the built-in ${Default}h. The plugin must declare the key public.")
|
|
} catch {
|
|
Write-ShopdbQuietState -Kind $Kind -State 'interval-unreachable' -Message (
|
|
"Could not read settings from $BaseUrl ($($_.Exception.Message)); using ${Default}h.")
|
|
}
|
|
return $Default
|
|
}
|
|
|
|
|
|
function Initialize-ShopdbBackup {
|
|
<#
|
|
Every check that can say "nothing to do", in one call.
|
|
|
|
Returns a context object. .Proceed is $false when the run should stop -
|
|
the reason has already been logged, once. On $true the context carries
|
|
Kind, BaseUrl, CollectorKey, MachineNumber and IntervalHours, and the
|
|
marker has NOT yet been stamped (Send-ShopdbBackup does that).
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind,
|
|
[string]$BaseUrl,
|
|
[scriptblock]$MachineNumberFallback,
|
|
[switch]$Force)
|
|
|
|
$ctx = [pscustomobject]@{
|
|
Kind = $Kind
|
|
Proceed = $false
|
|
BaseUrl = ''
|
|
CollectorKey = ''
|
|
MachineNumber = ''
|
|
IntervalHours = 24
|
|
}
|
|
|
|
$machineNumber = Get-ShopdbMachineNumber -Fallback $MachineNumberFallback
|
|
if (-not $machineNumber) {
|
|
Write-ShopdbQuietState -Kind $Kind -State 'no-machine-number' -Message (
|
|
'No machine number in pc-config.txt and no device fallback. ' +
|
|
'A backup cannot be filed against an asset - skipping.')
|
|
return $ctx
|
|
}
|
|
|
|
$collectorKey = Get-ShopdbCollectorKey
|
|
if (-not $collectorKey) {
|
|
Write-ShopdbQuietState -Kind $Kind -State 'no-collector-key' -Message (
|
|
"No collector key in $script:SHOPDBREG\CollectorKey or $script:KEYFILE. " +
|
|
'The collector endpoint has no IP-allowlist path, unlike the GE-Enforce ' +
|
|
'manifest fetch, so it always needs a collector-scoped token. Skipping.')
|
|
return $ctx
|
|
}
|
|
|
|
if (-not $BaseUrl) { $BaseUrl = Get-ShopdbRegValue -Name 'BaseUrl' }
|
|
if (-not $BaseUrl) {
|
|
Write-ShopdbQuietState -Kind $Kind -State 'no-base-url' -Message (
|
|
"No ShopDB BaseUrl in $script:SHOPDBREG and none passed. Skipping.")
|
|
return $ctx
|
|
}
|
|
$BaseUrl = $BaseUrl.TrimEnd('/')
|
|
|
|
$intervalHours = Get-ShopdbIntervalHours -Kind $Kind -BaseUrl $BaseUrl
|
|
$markerfile = Join-Path $script:LOGDIR ('{0}-backup.marker' -f $Kind)
|
|
if (-not $Force -and (Test-Path $markerfile)) {
|
|
try {
|
|
$last = (Get-Item $markerfile).LastWriteTime
|
|
if (((Get-Date) - $last).TotalHours -lt $intervalHours) {
|
|
$due = $last.AddHours($intervalHours).ToString('yyyy-MM-dd HH:mm')
|
|
Write-ShopdbQuietState -Kind $Kind -State ("throttled-$due") -Message (
|
|
"Throttled: posted within the last ${intervalHours}h, next attempt after $due. " +
|
|
'Use -Force to override.')
|
|
return $ctx
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
$ctx.Proceed = $true
|
|
$ctx.BaseUrl = $BaseUrl
|
|
$ctx.CollectorKey = $collectorKey
|
|
$ctx.MachineNumber = $machineNumber
|
|
$ctx.IntervalHours = $intervalHours
|
|
|
|
Clear-ShopdbQuietState -Kind $Kind
|
|
Write-ShopdbBackupLog -Kind $Kind -Message (
|
|
"=== $Kind backup start === machine $machineNumber, $BaseUrl, interval ${intervalHours}h")
|
|
return $ctx
|
|
}
|
|
|
|
|
|
function Send-ShopdbBackup {
|
|
<#
|
|
POST one captured config to the ShopDB collector.
|
|
|
|
-Bytes for a kind ShopDB parses and stores (storagebackend 'shopdb').
|
|
-ContentHash with -SharePath for a kind whose bytes stay on the share
|
|
(storagebackend 'share'), where ShopDB keeps metadata and a pointer.
|
|
|
|
Returns $true when ShopDB accepted it, whether that produced a new
|
|
revision or a no-op; an unchanged config is a no-op by design and is
|
|
the expected outcome most cycles.
|
|
#>
|
|
param([Parameter(Mandatory)][pscustomobject]$Context,
|
|
[byte[]]$Bytes,
|
|
[string]$ContentHash,
|
|
[string]$SharePath,
|
|
[string]$SourceFileName,
|
|
# Optional description of a share kind's snapshot - what is in it, not
|
|
# the bytes. ShopDB stores it so an asset panel can list the contents
|
|
# and diff them against the previous snapshot; without it the server
|
|
# would have to mount the share, which it refuses to do. Advisory:
|
|
# dedup still keys on ContentHash.
|
|
[hashtable]$Manifest,
|
|
[int]$TimeoutSec = 30)
|
|
|
|
$kind = $Context.Kind
|
|
|
|
# Marker BEFORE the post, deliberately. If ShopDB is unreachable we do not
|
|
# want every cycle for the rest of the day retrying; the next window picks
|
|
# it up.
|
|
$markerfile = Join-Path $script:LOGDIR ('{0}-backup.marker' -f $kind)
|
|
Set-Content -Path $markerfile -Value (Get-Date -Format 'o') -EA SilentlyContinue
|
|
|
|
# sourcehostname is load-bearing now, not just informational: ShopDB
|
|
# resolves a part-marker PC's backup to ITS marker through this field, and
|
|
# dedup keys a revision chain on it. An empty value silently files the
|
|
# backup against the operation instead and merges two devices' histories,
|
|
# so fall back to the DNS name rather than posting a blank.
|
|
$sourcehost = $env:COMPUTERNAME
|
|
if (-not $sourcehost) {
|
|
try { $sourcehost = [System.Net.Dns]::GetHostName() } catch { $sourcehost = '' }
|
|
}
|
|
|
|
$payload = @{
|
|
machinenumber = $Context.MachineNumber
|
|
backupkind = $kind
|
|
sourcehostname = $sourcehost
|
|
collectedat = (Get-Date).ToUniversalTime().ToString('o')
|
|
}
|
|
if ($SourceFileName) { $payload['sourcefilename'] = $SourceFileName }
|
|
if ($Bytes) {
|
|
$payload['contentbase64'] = [Convert]::ToBase64String($Bytes)
|
|
$payload['bytesize'] = $Bytes.Length
|
|
}
|
|
if ($ContentHash) { $payload['contenthash'] = $ContentHash }
|
|
if ($SharePath) { $payload['sharepath'] = $SharePath }
|
|
if ($Manifest) { $payload['manifest'] = $Manifest }
|
|
|
|
$uri = '{0}/api/collector/backups' -f $Context.BaseUrl
|
|
$size = if ($Bytes) { '{0} bytes' -f $Bytes.Length } else { $SharePath }
|
|
Write-ShopdbBackupLog -Kind $kind -Message (
|
|
'Posting {0} for machine {1}' -f $size, $Context.MachineNumber)
|
|
|
|
try {
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
$resp = Invoke-RestMethod -Uri $uri -Method Post `
|
|
-Body ($payload | ConvertTo-Json -Compress -Depth 6) `
|
|
-ContentType 'application/json' `
|
|
-Headers @{ 'X-API-Key' = $Context.CollectorKey } `
|
|
-TimeoutSec $TimeoutSec -EA Stop
|
|
|
|
$script:LASTACTION[$kind] = "$($resp.data.action)"
|
|
switch ("$($resp.data.action)") {
|
|
'created' { Write-ShopdbBackupLog -Kind $kind -Message (
|
|
'New revision {0} recorded.' -f $resp.data.backuprevisionid) }
|
|
'noop' { Write-ShopdbBackupLog -Kind $kind -Message 'Settings unchanged - no new revision (expected most cycles).' }
|
|
default { Write-ShopdbBackupLog -Kind $kind -Message (
|
|
"ShopDB returned action '{0}'." -f $resp.data.action) }
|
|
}
|
|
foreach ($warning in @($resp.data.warnings)) {
|
|
if ($warning) { Write-ShopdbBackupLog -Kind $kind -Message " WARNING: $warning" }
|
|
}
|
|
return $true
|
|
} catch {
|
|
# A 400 here is usually meaningful rather than transient: an
|
|
# unconfigured device, or a machine number ShopDB does not know. Log the
|
|
# server's own message so the cause is visible at the bay.
|
|
$detail = $_.Exception.Message
|
|
try {
|
|
$stream = $_.Exception.Response.GetResponseStream()
|
|
$reader = New-Object IO.StreamReader($stream)
|
|
$body = $reader.ReadToEnd()
|
|
if ($body) { $detail = $body }
|
|
} catch { }
|
|
Write-ShopdbBackupLog -Kind $kind -Message "Post failed: $detail"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
|
|
function Add-ShopdbBackupSkip {
|
|
<#
|
|
Record that a cycle was skipped, and why.
|
|
|
|
A skipped cycle posts NOTHING to ShopDB, deliberately: lastseenat means
|
|
"this config was confirmed still current", so posting a no-op to keep a
|
|
marker green would be a lie that hides a backup which has not run for a
|
|
month. The staleness badge is what surfaces that.
|
|
|
|
What ShopDB cannot know is WHY, so the count and the reason are kept
|
|
locally and carried into the next successful snapshot's manifest. A
|
|
marker that captures once a week because its port is busy every shift
|
|
change then says so on its own asset page.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind,
|
|
[Parameter(Mandatory)][string]$Reason)
|
|
|
|
$file = Join-Path $script:LOGDIR ('{0}-backup.skips' -f $Kind)
|
|
$count = 0
|
|
$first = (Get-Date).ToUniversalTime().ToString('o')
|
|
if (Test-Path $file) {
|
|
try {
|
|
$existing = Get-Content -LiteralPath $file -Raw | ConvertFrom-Json
|
|
$count = [int]$existing.count
|
|
if ($existing.first) { $first = [string]$existing.first }
|
|
} catch { }
|
|
}
|
|
if (-not (Test-Path $script:LOGDIR)) {
|
|
New-Item -ItemType Directory -Path $script:LOGDIR -Force -EA SilentlyContinue | Out-Null
|
|
}
|
|
[pscustomobject]@{
|
|
count = $count + 1
|
|
first = $first
|
|
last = (Get-Date).ToUniversalTime().ToString('o')
|
|
reason = $Reason
|
|
} | ConvertTo-Json -Compress | Set-Content -Path $file -EA SilentlyContinue
|
|
}
|
|
|
|
|
|
function Get-ShopdbBackupSkips {
|
|
<#
|
|
Skipped cycles since the last successful capture: @{count; first; last;
|
|
reason}, or $null when none. Read into the manifest, then cleared by
|
|
Clear-ShopdbBackupSkips once a capture actually succeeds.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind)
|
|
$file = Join-Path $script:LOGDIR ('{0}-backup.skips' -f $Kind)
|
|
if (-not (Test-Path $file)) { return $null }
|
|
try {
|
|
$data = Get-Content -LiteralPath $file -Raw | ConvertFrom-Json
|
|
return @{
|
|
count = [int]$data.count
|
|
first = [string]$data.first
|
|
last = [string]$data.last
|
|
reason = [string]$data.reason
|
|
}
|
|
} catch { return $null }
|
|
}
|
|
|
|
|
|
function Clear-ShopdbBackupSkips {
|
|
param([Parameter(Mandatory)][string]$Kind)
|
|
Remove-Item -LiteralPath (Join-Path $script:LOGDIR ('{0}-backup.skips' -f $Kind)) `
|
|
-Force -EA SilentlyContinue
|
|
}
|
|
|
|
|
|
function Get-ShopdbLastBackupAction {
|
|
<#
|
|
What ShopDB did with the most recent post for this kind: 'created',
|
|
'noop', or '' if nothing was posted or the post failed.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind)
|
|
if ($script:LASTACTION.ContainsKey($Kind)) { return $script:LASTACTION[$Kind] }
|
|
return ''
|
|
}
|
|
|
|
|
|
function Test-ShopdbBackupNoop {
|
|
<#
|
|
True when the last post for this kind changed nothing. A share kind uses
|
|
this to delete the snapshot it just wrote: a no-op proves byte-identical
|
|
content already exists in the previous snapshot, so removing the copy
|
|
loses nothing and stops one identical directory accumulating per cycle.
|
|
#>
|
|
param([Parameter(Mandatory)][string]$Kind)
|
|
return ((Get-ShopdbLastBackupAction -Kind $Kind) -eq 'noop')
|
|
}
|
|
|
|
|
|
Export-ModuleMember -Function Initialize-ShopdbBackup, Send-ShopdbBackup,
|
|
Write-ShopdbBackupLog, Write-ShopdbQuietState, Clear-ShopdbQuietState,
|
|
Get-ShopdbRegValue, Get-ShopdbCollectorKey, Get-ShopdbMachineNumber,
|
|
Get-ShopdbIntervalHours, Get-ShopdbLogPath,
|
|
Get-ShopdbLastBackupAction, Test-ShopdbBackupNoop,
|
|
Add-ShopdbBackupSkip, Get-ShopdbBackupSkips, Clear-ShopdbBackupSkips,
|
|
Get-ShopdbAssetId
|