Private/ScanBridgeDiscoverySetup.ps1
|
# ScanBridge "Observe a scanner" step: a guided run of ScanBridge's read-only # Invoke-NSPScanBridgeDeliveryDiscovery against a dedicated test folder, then an optional # plan-first MinimumAgeSeconds change on one route and an optional manual override copy of the # saved profile. Discovery is owned by ScanBridge; this step only gathers its inputs. $script:NSPScanBridgeDiscoveryClasses=@('DirectExclusive','DirectReadable','TempRename','Hybrid','Unknown') function Test-NSPScanBridgeDiscoveryFolder { # Where the test folder sits against the route folders. Overlapping a Spool or Archive is refused; # overlapping a Source is returned so the caller can check the task. Pure. param([Parameter(Mandatory)][string]$Folder,[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Routes) $full={ param($p) [IO.Path]::GetFullPath([string]$p).TrimEnd('\') } $a=(& $full $Folder)+'\' $result=[pscustomobject]@{Problem=$null;SourceRoute=$null} foreach($route in $Routes) { foreach($kind in 'Source','Spool','Archive') { if(Test-NSPPlaceholderValue $route[$kind]) { continue } $b=(& $full $route[$kind])+'\' if(-not ($a.StartsWith($b,[StringComparison]::OrdinalIgnoreCase) -or $b.StartsWith($a,[StringComparison]::OrdinalIgnoreCase))) { continue } if($kind -ne 'Source') { $result.Problem="The test folder overlaps route '$($route.Name)' $kind; pick a dedicated folder."; return $result } if(-not $result.SourceRoute) { $result.SourceRoute=[string]$route.Name } } } $result } function Get-NSPScanBridgeDiscoveryAgePlan { # What applying a suggested MinimumAgeSeconds would do. Lowering needs explicit confirmation and # is never offered for an ambiguous (Unknown or Hybrid) classification. Pure. param([Parameter(Mandatory)][int]$Current,[Parameter(Mandatory)][int]$Suggested,[Parameter(Mandatory)][string]$Classification) if($Suggested -eq $Current) { return 'Same' } if($Suggested -gt $Current) { return 'Raise' } if($Classification -in @('Unknown','Hybrid')) { return 'KeepAmbiguous' } 'Lower' } function Save-NSPScanBridgeDiscoveryOverride { # Writes a copy of the profile with Override set; the original profile is kept unchanged. param([Parameter(Mandatory)][string]$ProfilePath,[Parameter(Mandatory)][string]$Classification,[Parameter(Mandatory)][string]$Reason,[DateTime]$NowUtc=[DateTime]::UtcNow) $deliveryProfile=[IO.File]::ReadAllText($ProfilePath) | ConvertFrom-Json $override=[pscustomobject][ordered]@{Classification=$Classification;Reason=$Reason;Utc=$NowUtc.ToString('o')} if($deliveryProfile.PSObject.Properties['Override']) { $deliveryProfile.Override=$override } else { $deliveryProfile | Add-Member -NotePropertyName Override -NotePropertyValue $override } $folder=Split-Path -Parent $ProfilePath $path=Join-Path $folder ('{0}_override_{1}.json' -f [IO.Path]::GetFileNameWithoutExtension($ProfilePath),$NowUtc.ToString('yyyyMMddTHHmmssZ')) [IO.File]::WriteAllText($path,($deliveryProfile | ConvertTo-Json -Depth 8),(New-Object Text.UTF8Encoding $false)) $path } function Invoke-NSPScanBridgeDiscoverySetup { <# Observe a scanner: test folder and device label (remembered), refusal while the folder is a live route Source with the task not disabled, capture window and file-name choice, then ScanBridge's discovery. Afterwards offers the suggested readiness age for one route (plan first, backup kept) and a manual override copy of the profile. DRY RUN still observes, since discovery is read-only, but creates no folder and writes no settings or override. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$ConfigurationPath) $dryRun=[bool](Get-NSPDryRun) $scanModule=Get-Module NSP.ScanBridge | Sort-Object Version -Descending | Select-Object -First 1 $discover=if($scanModule){$scanModule.ExportedCommands['Invoke-NSPScanBridgeDeliveryDiscovery']}else{$null} if(-not $discover -or -not $discover.Parameters.ContainsKey('Path')) { throw 'This ScanBridge version does not export Invoke-NSPScanBridgeDeliveryDiscovery. Provision the build with delivery discovery first.' } $config=Import-PowerShellDataFile -LiteralPath $ConfigurationPath -ErrorAction Stop $routes=@($config.Routes | Where-Object { $_ } | ForEach-Object { ConvertTo-NSPScanBridgeOrderedRoute -Route $_ }) $answers=Get-NSPScanBridgeAnswerTable # 1. Dedicated test folder and device label. $folderArgs=@{Prompt='Dedicated test folder the scanner saves to (preferably empty)'} if($answers.DiscoveryFolder) { $folderArgs.Default=[string]$answers.DiscoveryFolder } if(-not $dryRun) { $folderArgs.Create=$true } $folder=Read-NSPFolderPath @folderArgs # 2. Spool/Archive refused; a live Source refused unless the task is disabled (the worker would claim test scans mid-capture). $place=Test-NSPScanBridgeDiscoveryFolder -Folder $folder -Routes $routes if($place.Problem) { Write-NSPConsoleLine $place.Problem -Role Error; return } $existing=@(Get-ChildItem -LiteralPath $folder -File -Force -ErrorAction Stop).Count if($existing) { Write-NSPConsoleLine "$existing file(s) already in the folder are ignored; an empty folder keeps the capture clear." -Role Muted } if($place.SourceRoute) { $task=$null; $known=$true try { $task=Get-NSPScanBridgeTaskState } catch { $known=$false } if(-not $known) { Write-NSPConsoleLine "The test folder is route '$($place.SourceRoute)' Source and the task state cannot be read. Pick a dedicated folder." -Role Error; return } if($task -and $task.State -ne 'Disabled') { Write-NSPConsoleLine "The test folder is route '$($place.SourceRoute)' Source and the ScanBridge task is not disabled. Disable the task first, or pick a dedicated folder." -Role Error; return } Write-NSPConsoleLine "The test folder is route '$($place.SourceRoute)' Source. Test scans left there are uploaded when the task runs again; remove them after the capture." -Role Warning if(-not (Read-NSPConfirm -Prompt 'Observe this route Source anyway?')) { return } } Write-NSPConsoleLine 'The device label is saved in the profile: keep it generic (for example Front copier), no client or person names.' -Role Muted do { $device=(Read-NSPNonEmpty -Prompt 'Device label' -CurrentValue $(if($answers.DiscoveryDevice){[string]$answers.DiscoveryDevice}else{'Scanner'})).Trim() $valid=$device -match '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$' if(-not $valid) { Write-NSPConsoleLine 'Use letters, digits, spaces, dot, underscore or hyphen (up to 64).' -Role Warning } } until($valid) Save-NSPScanBridgeAnswer @{DiscoveryFolder=$folder;DiscoveryDevice=$device} # 3. Capture window and file names. $seconds=Read-NSPMenu -Prompt 'How long to watch' -CancelLabel 'Back' -Choices @( [pscustomobject]@{Label='3 minutes (recommended)';Value=180;Help='Time to send two or three test scans, including a multi-page one.'} [pscustomobject]@{Label='2 minutes';Value=120;Help='One or two short scans.'} [pscustomobject]@{Label='5 minutes';Value=300;Help='Several scans, or a slow device.'} [pscustomobject]@{Label='10 minutes';Value=600;Help='Large batches; Ctrl+C still stops early and saves results.'} ) if($null -eq $seconds) { return } $names=Read-NSPConfirm -Prompt 'Record file names (only needed to compare with simulator logs)?' # 4. Discovery itself: ScanBridge prints the classification, evidence and suggested age. $result=& $discover -Path $folder -DeviceName $device -DurationSeconds ([double]$seconds) -IncludeFileNames:$names if(-not $result) { return } foreach($note in @($result.Recommendation.Notes)) { Write-NSPConsoleLine " $note" -Role Muted } if($result.Confidence -ne 'High') { Write-NSPConsoleLine 'Repeat the capture with more test scans to raise confidence before changing settings.' -Role Muted } # 5. Suggested readiness age for one route, plan first; never lowered without explicit confirmation. $suggested=[int]$result.Recommendation.MinimumAgeSeconds if($routes.Count -and $suggested -gt 0 -and (Read-NSPConfirm -Prompt "Review the suggested readiness age ($suggested s) for a route?")) { $pick=Read-NSPMenu -Prompt 'Route this scanner saves to' -CancelLabel 'Skip' -Choices @(for($i=0;$i -lt $routes.Count;$i++) { $age=if($routes[$i].MinimumAgeSeconds){[int]$routes[$i].MinimumAgeSeconds}else{20} [pscustomobject]@{Label=('{0} (now {1} s)' -f $routes[$i].Name,$age);Value=$i;Help='Seconds a PDF must be unchanged before upload.'} }) if($null -ne $pick) { $route=$routes[$pick] $current=if($route.MinimumAgeSeconds){[int]$route.MinimumAgeSeconds}else{20} $plan=Get-NSPScanBridgeDiscoveryAgePlan -Current $current -Suggested $suggested -Classification ([string]$result.Classification) $write=$false switch($plan) { 'Same' { Write-NSPConsoleLine "Route '$($route.Name)' already uses $current s." -Role Success } 'KeepAmbiguous' { Write-NSPConsoleLine "The classification is $($result.Classification), so the current $current s is kept; ambiguous behavior never lowers the age." -Role Warning } 'Raise' { $write=$true } 'Lower' { Write-NSPConsoleLine "Lowering from $current s to $suggested s uploads sooner but leaves less margin for a paused writer." -Role Warning $write=Read-NSPConfirm -Prompt "Lower route '$($route.Name)' to $suggested s?" } } if($write) { $route['MinimumAgeSeconds']=$suggested if(Write-NSPScanBridgeSettingsEntry -ConfigurationPath $ConfigurationPath -Key Routes -Value $routes -DryRun $dryRun) { Write-NSPConsoleLine 'A Sysmon-triggered task waits the largest route age + 30 s; re-plan the task if this route is now the slowest.' -Role Muted } } } } # 6. Manual override: a separate copy; the measured profile stays as captured. if(-not $result.ProfilePath -or -not (Read-NSPConfirm -Prompt 'Record a manual classification override for this profile?')) { return } $class=Read-NSPMenu -Prompt 'Classification to record' -CancelLabel 'Back' -Choices @(foreach($c in $script:NSPScanBridgeDiscoveryClasses) { [pscustomobject]@{Label=$(if($c -eq $result.Classification){"$c (measured)"}else{$c});Value=$c;Help=$(switch($c){ 'DirectExclusive' {'Writes the final .pdf name and holds it exclusively.'} 'DirectReadable' {'Writes the final .pdf name and lets others read while writing.'} 'TempRename' {'Writes a temporary name, then renames it to .pdf.'} 'Hybrid' {'Mixed or alternating behavior.'} 'Unknown' {'Not enough evidence.'} })} }) if($null -eq $class) { return } $reason=(Read-NSPNonEmpty -Prompt 'Reason (generic; no client or person names)').Trim() if($dryRun) { Write-NSPConsoleLine 'DRY RUN: override not saved.' -Role Warning; return } $path=Save-NSPScanBridgeDiscoveryOverride -ProfilePath $result.ProfilePath -Classification $class -Reason $reason Write-NSPConsoleLine "Override saved as a copy: $path (the measured profile is unchanged)." -Role Success } |