[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$AnalyticsHost = '', [string]$AnalyticsUser = 'igor', [string]$RemoteRoot = '/opt/activitywatch/clickhouse-1c/landing', [string]$RemoteKeyPath = 'C:\ProgramData\AWatch-rus\ssh\awops_ed25519', [string]$RegistryWorkbookPath = '' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $LogDir = 'C:\ProgramData\AWatch-rus\logs' $LogPath = Join-Path $LogDir 'file1c-telemetry.log' $ScpExe = Join-Path $env:WINDIR 'System32\OpenSSH\scp.exe' $ExporterStatePath = Join-Path (Split-Path -Parent $ConfigPath) 'file1c-telemetry-state.json' New-Item -ItemType Directory -Path $LogDir -Force | Out-Null function Write-RunLog { param( [Parameter(Mandatory = $true)] [string]$Message ) $line = '{0} {1}' -f ([DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')), $Message Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8 } function Save-DeploymentConfig { param( [Parameter(Mandatory = $true)] [psobject]$Config, [Parameter(Mandatory = $true)] [string]$Path ) $json = $Config | ConvertTo-Json -Depth 12 Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 } function Get-LastSuccessfulAnalyticsHostFromRunLog { param( [Parameter(Mandatory = $true)] [string]$Path ) if (-not (Test-Path -LiteralPath $Path)) { return '' } $matchedHost = '' foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) { if ([string]$line -match 'upload complete analyticsHost=([^\s]+)') { $matchedHost = [string]$Matches[1] } } return $matchedHost } trap { Write-RunLog ("ERROR: " + ($_ | Out-String).Trim()) exit 1 } function New-TemporarySshKeyCopy { param( [Parameter(Mandatory = $true)] [string]$SourceKeyPath ) $tempDir = Join-Path $env:TEMP 'aw-rus-1c-ssh' New-Item -ItemType Directory -Path $tempDir -Force | Out-Null $tempKeyPath = Join-Path $tempDir 'awops_ed25519' $candidates = New-Object System.Collections.Generic.List[string] foreach ($candidate in @( $SourceKeyPath, (Join-Path $env:USERPROFILE '.ssh\awops_ed25519'), (Join-Path ([System.Environment]::GetFolderPath('UserProfile')) '.ssh\awops_ed25519') )) { if (-not [string]::IsNullOrWhiteSpace($candidate) -and -not $candidates.Contains($candidate)) { $candidates.Add($candidate) } } foreach ($candidate in $candidates) { $pathError = $null $exists = Test-Path -LiteralPath $candidate -ErrorAction SilentlyContinue -ErrorVariable pathError if ($pathError) { Write-RunLog ("skip unusable ssh key path={0} reason={1}" -f $candidate, $pathError[0].Exception.Message) continue } if (-not $exists) { continue } & cmd.exe /d /c "copy /Y `"$candidate`" `"$tempKeyPath`" >nul" if ($LASTEXITCODE -ne 0) { Write-RunLog ("skip unusable ssh key path={0} reason=copy command failed rc={1}" -f $candidate, $LASTEXITCODE) continue } & icacls.exe $tempKeyPath /inheritance:r | Out-Null & icacls.exe $tempKeyPath /grant:r "$($env:USERNAME):(F)" | Out-Null & icacls.exe $tempKeyPath /remove:g 'Users' 'Authenticated Users' 'Everyone' 'BUILTIN\Users' 'BUILTIN\Administrators' 'NT AUTHORITY\SYSTEM' 2>$null | Out-Null return $tempKeyPath } throw "No usable SSH private key found. checked={0}" -f ($candidates -join ', ') } function Get-1CFileInfobases { $results = New-Object System.Collections.Generic.List[object] $launcherFiles = New-Object System.Collections.Generic.List[string] Get-ChildItem -Path 'C:\Users' -Directory -ErrorAction SilentlyContinue | ForEach-Object { $launcherFile = Join-Path $_.FullName 'AppData\Roaming\1C\1CEStart\ibases.v8i' try { if (Test-Path -LiteralPath $launcherFile -ErrorAction Stop) { $launcherFiles.Add($launcherFile) } } catch { Write-RunLog ("skip inaccessible launcher file={0} reason={1}" -f $launcherFile, $_.Exception.Message) } } foreach ($file in $launcherFiles) { $userName = 'unknown' if ([string]$file -match '^[A-Za-z]:\\Users\\([^\\]+)\\') { $userName = [string]$Matches[1] } $currentName = $null $currentId = $null $fileLines = $null try { $fileLines = Get-Content -LiteralPath $file -Encoding UTF8 -ErrorAction Stop } catch { Write-RunLog ("skip unreadable launcher file={0} reason={1}" -f $file, $_.Exception.Message) continue } foreach ($lineRaw in $fileLines) { $line = [string]$lineRaw if ($line -match '^\[(.+)\]$') { $currentName = $Matches[1] $currentId = $null continue } if ($line -match '^ID=(.+)$') { $currentId = $Matches[1].Trim() continue } if ($line -match '^Connect=File="(.+)";$' -and $currentName) { $results.Add([pscustomobject]@{ userName = $userName infobase = $currentName baseId = $currentId path = $Matches[1] launcherFile = $file }) } } } return $results | Group-Object infobase, path | ForEach-Object { $_.Group | Select-Object -First 1 } } function Get-HostSample { $cpu = 0.0 $ramPct = 0.0 try { Add-Type -AssemblyName Microsoft.VisualBasic -ErrorAction Stop $computerInfo = New-Object Microsoft.VisualBasic.Devices.ComputerInfo $totalMemory = [double]$computerInfo.TotalPhysicalMemory $availableMemory = [double]$computerInfo.AvailablePhysicalMemory if ($totalMemory -gt 0) { $ramPct = (($totalMemory - $availableMemory) / $totalMemory) * 100 } } catch { Write-RunLog ("warning: host memory sample fallback reason={0}" -f $_.Exception.Message) } $disk = Get-PSDrive -Name E -ErrorAction SilentlyContinue $rdp = (quser 2>$null | Select-Object -Skip 1 | Measure-Object).Count return [ordered]@{ ts = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') host = $env:COMPUTERNAME cpu_pct = [math]::Round($cpu, 2) ram_pct = [math]::Round($ramPct, 2) disk_free_gb = if ($disk) { [math]::Round($disk.Free / 1GB, 2) } else { 0 } disk_latency_ms = 0 smb_errors = 0 rdp_sessions = $rdp backup_ok = 1 } } function Write-JsonLines { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$Rows ) $directory = Split-Path -Parent $Path if ($directory) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } $normalizedRows = @() if ($null -ne $Rows) { $normalizedRows = @($Rows) } $normalizedRows | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Set-Content -LiteralPath $Path -Encoding UTF8 } function Invoke-SshUploadWithRetry { param( [Parameter(Mandatory = $true)] [string]$KeyPath, [Parameter(Mandatory = $true)] [string]$SourcePath, [Parameter(Mandatory = $true)] [string]$Destination, [int]$Attempts = 3, [int]$DelaySeconds = 5 ) for ($attempt = 1; $attempt -le $Attempts; $attempt++) { Write-RunLog "scp attempt=$attempt source=$SourcePath destination=$Destination" & $ScpExe -q -i $KeyPath -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL $SourcePath $Destination if ($LASTEXITCODE -eq 0) { Write-RunLog "scp success source=$SourcePath" return } if ($attempt -ge $Attempts) { throw "scp upload failed after $Attempts attempts for $SourcePath with rc=$LASTEXITCODE" } Write-RunLog "scp retry source=$SourcePath rc=$LASTEXITCODE delay=${DelaySeconds}s" Start-Sleep -Seconds $DelaySeconds } } function Get-ExporterState { param( [Parameter(Mandatory = $true)] [string]$Path ) if (-not (Test-Path -LiteralPath $Path)) { return @{} } $raw = Get-Content -Raw -LiteralPath $Path -Encoding UTF8 if ([string]::IsNullOrWhiteSpace($raw)) { return @{} } $payload = $raw | ConvertFrom-Json $state = @{} foreach ($prop in $payload.PSObject.Properties) { $state[[string]$prop.Name] = $prop.Value } return $state } function Save-ExporterState { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [hashtable]$State ) $directory = Split-Path -Parent $Path if ($directory) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } try { $State | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $Path -Encoding UTF8 } catch { Write-RunLog ("warning: failed to persist exporter state path={0} reason={1}" -f $Path, $_.Exception.Message) } } function Get-CompanyActivityScore { param( [double]$DbDeltaMb, [double]$ReglogDeltaMb, [int]$ActiveLocks, [bool]$HasTempDb, [bool]$SchedulerTouched, [string]$Status, [bool]$IsBootstrap ) $score = 0.0 $score += [math]::Abs($DbDeltaMb) $score += [math]::Abs($ReglogDeltaMb) if ($ActiveLocks -gt 0) { $score += ($ActiveLocks * 5) } if ($HasTempDb) { $score += 10 } if ($SchedulerTouched) { $score += 3 } if ($Status -eq 'busy') { $score += 5 } if ($IsBootstrap -and $score -le 0) { $score = 1 } return [math]::Round($score, 2) } Write-RunLog 'file1c exporter start' if (-not (Test-Path -LiteralPath $ScpExe)) { throw "scp client not found: $ScpExe" } $config = Get-Content -Raw -LiteralPath $ConfigPath | ConvertFrom-Json if ($config.PSObject.Properties.Name -contains 'analytics' -and $config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'remoteKeyPath' -and -not [string]::IsNullOrWhiteSpace([string]$config.analytics.file1cAutomation.remoteKeyPath)) { $RemoteKeyPath = [string]$config.analytics.file1cAutomation.remoteKeyPath } if ([string]::IsNullOrWhiteSpace($AnalyticsHost)) { if ($config.PSObject.Properties.Name -contains 'analytics' -and $config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetHost') { $AnalyticsHost = [string]$config.analytics.file1cAutomation.targetHost } } if ([string]::IsNullOrWhiteSpace($AnalyticsHost)) { $AnalyticsHost = Get-LastSuccessfulAnalyticsHostFromRunLog -Path $LogPath if (-not [string]::IsNullOrWhiteSpace($AnalyticsHost)) { Write-RunLog "recovered analyticsHost=$AnalyticsHost from previous successful uploader log" try { if ($config.PSObject.Properties.Name -notcontains 'analytics' -or $null -eq $config.analytics) { if ($config.PSObject.Properties.Name -contains 'analytics') { $config.analytics = [pscustomobject]@{} } else { $config | Add-Member -NotePropertyName 'analytics' -NotePropertyValue ([pscustomobject]@{}) } } if ($config.analytics.PSObject.Properties.Name -notcontains 'file1cAutomation' -or $null -eq $config.analytics.file1cAutomation) { if ($config.analytics.PSObject.Properties.Name -contains 'file1cAutomation') { $config.analytics.file1cAutomation = [pscustomobject]@{} } else { $config.analytics | Add-Member -NotePropertyName 'file1cAutomation' -NotePropertyValue ([pscustomobject]@{}) } } if ($config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetHost') { $config.analytics.file1cAutomation.targetHost = $AnalyticsHost } else { $config.analytics.file1cAutomation | Add-Member -NotePropertyName 'targetHost' -NotePropertyValue $AnalyticsHost } Save-DeploymentConfig -Config $config -Path $ConfigPath } catch { Write-RunLog ("warning: failed to persist recovered analyticsHost into deployment-config: " + $_.Exception.Message) } } } if ([string]::IsNullOrWhiteSpace($AnalyticsHost)) { throw "AnalyticsHost is empty, deployment-config has no analytics.file1cAutomation.targetHost, and no previous successful uploader log was found" } if ($config.PSObject.Properties.Name -contains 'analytics' -and $config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetUser' -and -not [string]::IsNullOrWhiteSpace([string]$config.analytics.file1cAutomation.targetUser)) { $AnalyticsUser = [string]$config.analytics.file1cAutomation.targetUser } if ($config.PSObject.Properties.Name -contains 'analytics' -and $config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'remoteRoot' -and -not [string]::IsNullOrWhiteSpace([string]$config.analytics.file1cAutomation.remoteRoot)) { $RemoteRoot = [string]$config.analytics.file1cAutomation.remoteRoot } if ([string]::IsNullOrWhiteSpace($RegistryWorkbookPath) -and $config.PSObject.Properties.Name -contains 'analytics' -and $config.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $config.analytics.file1cAutomation.PSObject.Properties.Name -contains 'registryWorkbookPath' -and -not [string]::IsNullOrWhiteSpace([string]$config.analytics.file1cAutomation.registryWorkbookPath)) { $RegistryWorkbookPath = [string]$config.analytics.file1cAutomation.registryWorkbookPath } if ([string]::IsNullOrWhiteSpace($RegistryWorkbookPath)) { $RegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx' } $infobases = @(Get-1CFileInfobases) $nowUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $exporterState = Get-ExporterState -Path $ExporterStatePath $nextExporterState = @{} $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $documents = New-Object System.Collections.Generic.List[object] $companies = New-Object System.Collections.Generic.List[object] $reglog = New-Object System.Collections.Generic.List[object] $audit = New-Object System.Collections.Generic.List[object] foreach ($base in $infobases) { $dbFile = Join-Path $base.path '1Cv8.1CD' $dbItem = Get-Item -LiteralPath $dbFile -ErrorAction SilentlyContinue $logDir = Join-Path $base.path '1Cv8Log' $logItems = @(Get-ChildItem -LiteralPath $logDir -File -ErrorAction SilentlyContinue) $mainLog = $logItems | Where-Object { $_.Extension -ieq '.lgp' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 $activeLocks = @(Get-ChildItem -LiteralPath $base.path -File -Filter '1Cv8*.1CL*' -ErrorAction SilentlyContinue) $tempDb = Get-Item -LiteralPath (Join-Path $base.path '1Cv8tmp.1CD') -ErrorAction SilentlyContinue $schedulerDir = Get-Item -LiteralPath (Join-Path $base.path '1Cv8JobScheduler') -ErrorAction SilentlyContinue $owner = if ($base.userName) { [string]$base.userName } else { 'unknown' } $organization = [string](Split-Path -Leaf (Split-Path -Parent $base.path)) $status = if ($activeLocks.Count -gt 0 -or $tempDb) { 'busy' } else { 'online' } $docId = if ($base.baseId) { [string]$base.baseId } else { ([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$base.path)).TrimEnd('=').Replace('/','_').Replace('+','-')) } $stateKey = [string]$docId $previous = if ($exporterState.ContainsKey($stateKey)) { $exporterState[$stateKey] } else { $null } $dbSizeBytes = if ($dbItem) { [int64]$dbItem.Length } else { 0 } $mainLogBytes = if ($mainLog) { [int64]$mainLog.Length } else { 0 } $schedulerWriteUtc = if ($schedulerDir) { [datetime]$schedulerDir.LastWriteTimeUtc } else { $null } $dbDeltaMb = 0.0 $reglogDeltaMb = 0.0 $schedulerTouched = $false $isBootstrap = ($null -eq $previous) if (-not $isBootstrap) { $prevDbSize = if ($previous.PSObject.Properties.Name -contains 'dbSizeBytes') { [double]$previous.dbSizeBytes } else { 0 } $prevMainLogSize = if ($previous.PSObject.Properties.Name -contains 'mainLogBytes') { [double]$previous.mainLogBytes } else { 0 } $dbDeltaMb = [math]::Round(($dbSizeBytes - $prevDbSize) / 1MB, 2) $reglogDeltaMb = [math]::Round(($mainLogBytes - $prevMainLogSize) / 1MB, 2) if ($schedulerWriteUtc -and $previous.PSObject.Properties.Name -contains 'schedulerWriteUtc' -and -not [string]::IsNullOrWhiteSpace([string]$previous.schedulerWriteUtc)) { $prevSchedulerWriteUtc = [datetime]::Parse([string]$previous.schedulerWriteUtc) $schedulerTouched = ($schedulerWriteUtc -gt $prevSchedulerWriteUtc) } elseif ($schedulerWriteUtc) { $schedulerTouched = $true } } $activityScore = Get-CompanyActivityScore ` -DbDeltaMb $dbDeltaMb ` -ReglogDeltaMb $reglogDeltaMb ` -ActiveLocks $activeLocks.Count ` -HasTempDb ([bool]$tempDb) ` -SchedulerTouched $schedulerTouched ` -Status $status ` -IsBootstrap $isBootstrap $documents.Add([ordered]@{ ts = $nowUtc infobase = [string]$base.infobase organization = $organization department = 'FileBase' doc_type = 'InfobaseSnapshot' doc_id = $docId doc_number = '' author = $owner counterparty = '' operation_type = 'inventory' amount = 0 status = $status posted = 1 }) $companies.Add([ordered]@{ ts = $nowUtc infobase = [string]$base.infobase company_name = [string]$base.infobase organization = $organization owner_user = $owner base_id = $docId base_path = [string]$base.path status = $status db_size_bytes = $dbSizeBytes reglog_size_bytes = $mainLogBytes active_locks = $activeLocks.Count temp_db_present = if ($tempDb) { 1 } else { 0 } scheduler_touched = if ($schedulerTouched) { 1 } else { 0 } activity_score = $activityScore }) if ($activityScore -gt 0) { $documents.Add([ordered]@{ ts = $nowUtc infobase = [string]$base.infobase organization = $organization department = 'FileBaseActivity' doc_type = 'CompanyActivitySnapshot' doc_id = "$docId-$stamp" doc_number = $stamp author = $owner counterparty = [string]$base.infobase operation_type = 'activity_snapshot' amount = $activityScore status = $status posted = 1 }) } $audit.Add([ordered]@{ ts = $nowUtc infobase = [string]$base.infobase user = $owner object_type = 'infobase' object_id = $docId action = 'inventory_snapshot' before_hash = '' after_hash = '' risk_tag = if ($status -eq 'busy') { 'busy' } else { '' } }) if ($mainLog) { $reglog.Add([ordered]@{ ts = ([datetime]$mainLog.LastWriteTimeUtc).ToString('yyyy-MM-ddTHH:mm:ssZ') infobase = [string]$base.infobase user = $owner host = $env:COMPUTERNAME app = '1cv8-file' event_name = 'RegLogInventory' level = if ($mainLog.Length -gt 536870912) { 'warn' } else { 'info' } duration_ms = 0 message = "Registration log file $($mainLog.Name) size=$([math]::Round($mainLog.Length / 1MB, 2))MB path=$($mainLog.FullName)" }) } if ($activeLocks.Count -gt 0 -or $tempDb) { $reglog.Add([ordered]@{ ts = $nowUtc infobase = [string]$base.infobase user = $owner host = $env:COMPUTERNAME app = '1cv8-file' event_name = 'FileBaseBusy' level = 'warn' duration_ms = 0 message = "Detected active file-base markers: locks=$($activeLocks.Count) tempDb=$([bool]$tempDb)" }) } if ($schedulerDir) { $reglog.Add([ordered]@{ ts = ([datetime]$schedulerDir.LastWriteTimeUtc).ToString('yyyy-MM-ddTHH:mm:ssZ') infobase = [string]$base.infobase user = $owner host = $env:COMPUTERNAME app = '1cv8-file' event_name = 'JobSchedulerActivity' level = 'info' duration_ms = 0 message = "1Cv8JobScheduler touched at $([datetime]$schedulerDir.LastWriteTimeUtc)" }) } $reglog.Add([ordered]@{ ts = $nowUtc infobase = [string]$base.infobase user = $owner host = $env:COMPUTERNAME app = '1cv8-file' event_name = 'CompanyActivitySnapshot' level = if ($activityScore -gt 20) { 'warn' } else { 'info' } duration_ms = 0 message = "activityScore=$activityScore dbDeltaMb=$dbDeltaMb reglogDeltaMb=$reglogDeltaMb locks=$($activeLocks.Count) tempDb=$([bool]$tempDb) schedulerTouched=$schedulerTouched" }) $nextExporterState[$stateKey] = [ordered]@{ infobase = [string]$base.infobase path = [string]$base.path dbSizeBytes = $dbSizeBytes mainLogBytes = $mainLogBytes schedulerWriteUtc = if ($schedulerWriteUtc) { $schedulerWriteUtc.ToString('o') } else { '' } updatedAtUtc = $nowUtc } } $hostRows = @((Get-HostSample)) $outRoot = Join-Path $env:TEMP "aw-rus-1c-outbox-$stamp" New-Item -ItemType Directory -Path $outRoot -Force | Out-Null $files = @{ documents = Join-Path $outRoot "documents-$stamp.jsonl" companies = Join-Path $outRoot "companies-$stamp.jsonl" reglog = Join-Path $outRoot "reglog-$stamp.jsonl" audit = Join-Path $outRoot "audit-$stamp.jsonl" host = Join-Path $outRoot "host-$stamp.jsonl" registry = Join-Path $outRoot "company-registry-$stamp.xlsx" } $documentRows = @($documents | ForEach-Object { $_ }) $companyRows = @($companies | ForEach-Object { $_ }) $reglogRows = @($reglog | ForEach-Object { $_ }) $auditRows = @($audit | ForEach-Object { $_ }) $hostRowsNormalized = @($hostRows | ForEach-Object { $_ }) Write-JsonLines -Path ([string]$files['documents']) -Rows $documentRows Write-JsonLines -Path ([string]$files['companies']) -Rows $companyRows Write-JsonLines -Path ([string]$files['reglog']) -Rows $reglogRows Write-JsonLines -Path ([string]$files['audit']) -Rows $auditRows Write-JsonLines -Path ([string]$files['host']) -Rows $hostRowsNormalized $registryUploaded = $false if (Test-Path -LiteralPath $RegistryWorkbookPath) { Copy-Item -LiteralPath $RegistryWorkbookPath -Destination ([string]$files['registry']) -Force } $effectiveKeyPath = New-TemporarySshKeyCopy -SourceKeyPath $RemoteKeyPath try { Write-RunLog "prepared datasets documents=$($documentRows.Count) companies=$($companyRows.Count) reglog=$($reglogRows.Count) audit=$($auditRows.Count) host=$($hostRowsNormalized.Count)" foreach ($dataset in 'documents', 'companies', 'reglog', 'audit', 'host') { Invoke-SshUploadWithRetry -KeyPath $effectiveKeyPath -SourcePath ([string]$files[$dataset]) -Destination "$AnalyticsUser@$AnalyticsHost`:$RemoteRoot/$dataset/" } if (Test-Path -LiteralPath ([string]$files['registry'])) { Invoke-SshUploadWithRetry -KeyPath $effectiveKeyPath -SourcePath ([string]$files['registry']) -Destination "$AnalyticsUser@$AnalyticsHost`:$RemoteRoot/registry/" $registryUploaded = $true } Save-ExporterState -Path $ExporterStatePath -State $nextExporterState Write-RunLog "upload complete analyticsHost=$AnalyticsHost remoteRoot=$RemoteRoot" } finally { Remove-Item -LiteralPath $effectiveKeyPath -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $outRoot -Recurse -Force -ErrorAction SilentlyContinue } Write-RunLog 'file1c exporter done' [ordered]@{ analyticsHost = $AnalyticsHost analyticsUser = $AnalyticsUser remoteRoot = $RemoteRoot infobases = @($infobases | ForEach-Object { $_.infobase }) datasets = [ordered]@{ documents = $documents.Count companies = $companies.Count reglog = $reglog.Count audit = $audit.Count host = $hostRows.Count registry = if ($registryUploaded) { 1 } else { 0 } } } | ConvertTo-Json -Depth 8