param( [string]$ServerUrl = "", [string]$AgentId = "", [string]$ConfigPath = "C:\ProgramData\SFERA\WindowsAgent\agent-config.json", [string[]]$NetworkRoot = @(), [int]$PollSeconds = 5 ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 $AgentVersion = "0.2.31" $startedAt = (Get-Date).ToUniversalTime().ToString("o") $JsonContentType = "application/json; charset=utf-8" function Write-AgentLog { param([string]$Message) $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$stamp] $Message" } function Get-AgentApiBase { param([object]$Config) if ($Config.PSObject.Properties.Name -contains "api_url" -and ![string]::IsNullOrWhiteSpace($Config.api_url)) { return ([string]$Config.api_url).TrimEnd("/") } try { $uri = [System.Uri]([string]$Config.server_url) if ($uri.Port -eq 49230) { $builder = New-Object System.UriBuilder($uri) $builder.Port = 49280 return $builder.Uri.AbsoluteUri.TrimEnd("/") } } catch { # Fall back to the web proxy below. } return "$(([string]$Config.server_url).TrimEnd("/"))/api/sfera" } function Agent-ApiUrl { param([string]$Path) return "$script:AgentApiBase$Path" } function Invoke-AgentRestMethod { param( [string]$Method, [string]$Uri, [string]$ContentType = $null, [string]$Body = $null ) $arguments = @{ Method = $Method Uri = $Uri TimeoutSec = 20 } if ($ContentType) { $arguments.ContentType = $ContentType } if ($Body) { $arguments.Body = $Body } return Invoke-RestMethod @arguments } function Invoke-AgentWebRequest { param( [string]$Method = "Get", [string]$Uri, [string]$OutFile = $null, [string]$ContentType = $null, [string]$InFile = $null, [int]$TimeoutSec = 20 ) $arguments = @{ Method = $Method Uri = $Uri TimeoutSec = $TimeoutSec } if ($OutFile) { $arguments.OutFile = $OutFile } if ($ContentType) { $arguments.ContentType = $ContentType } if ($InFile) { $arguments.InFile = $InFile } return Invoke-WebRequest -UseBasicParsing @arguments } function Get-AgentPowerShellPath { $windowsPowerShell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe" if (Test-Path -LiteralPath $windowsPowerShell -PathType Leaf) { return $windowsPowerShell } return Join-Path $PSHOME "powershell.exe" } $script:TrayIcon = $null $script:TrayServer = $null $script:TrayGreenIcon = $null $script:TrayRedIcon = $null function New-AgentStatusIcon { param([string]$ColorName) $bitmap = New-Object System.Drawing.Bitmap 16, 16 $graphics = [System.Drawing.Graphics]::FromImage($bitmap) $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias $graphics.Clear([System.Drawing.Color]::Transparent) $brush = if ($ColorName -eq "Green") { New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(37, 160, 80)) } else { New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(210, 62, 62)) } $border = New-Object System.Drawing.Pen ([System.Drawing.Color]::White), 2 $graphics.FillEllipse($brush, 2, 2, 12, 12) $graphics.DrawEllipse($border, 2, 2, 12, 12) $handle = $bitmap.GetHicon() $icon = [System.Drawing.Icon]::FromHandle($handle).Clone() $graphics.Dispose() $brush.Dispose() $border.Dispose() $bitmap.Dispose() return $icon } function Initialize-TrayIcon { param([string]$Server, [string]$AgentId) try { Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $script:TrayServer = $Server $script:TrayGreenIcon = New-AgentStatusIcon -ColorName "Green" $script:TrayRedIcon = New-AgentStatusIcon -ColorName "Red" $menu = New-Object System.Windows.Forms.ContextMenuStrip $openItem = New-Object System.Windows.Forms.ToolStripMenuItem("Open SFERA") $openItem.add_Click({ try { Start-Process $script:TrayServer } catch {} }) $exitItem = New-Object System.Windows.Forms.ToolStripMenuItem("Exit agent") $exitItem.add_Click({ if ($script:TrayIcon) { $script:TrayIcon.Visible = $false $script:TrayIcon.Dispose() } exit 0 }) [void]$menu.Items.Add($openItem) [void]$menu.Items.Add($exitItem) $script:TrayIcon = New-Object System.Windows.Forms.NotifyIcon $script:TrayIcon.Icon = $script:TrayRedIcon $script:TrayIcon.ContextMenuStrip = $menu $script:TrayIcon.Visible = $true $script:TrayIcon.Text = "SFERA Agent: connecting" Write-AgentLog "Tray icon initialized for $AgentId." } catch { Write-AgentLog "Tray icon unavailable: $($_.Exception.Message)" } } function Update-TrayIcon { param([bool]$Online, [string]$Message) if ($null -eq $script:TrayIcon) { return } $text = if ($Online) { "SFERA Agent: online" } else { "SFERA Agent: offline" } if (![string]::IsNullOrWhiteSpace($Message)) { $text = "$text - $Message" } if ($text.Length -gt 63) { $text = $text.Substring(0, 60) + "..." } $script:TrayIcon.Icon = if ($Online) { $script:TrayGreenIcon } else { $script:TrayRedIcon } $script:TrayIcon.Text = $text [System.Windows.Forms.Application]::DoEvents() } function Pump-TrayIcon { if ($null -ne $script:TrayIcon) { [System.Windows.Forms.Application]::DoEvents() } } function Read-AgentConfig { if (Test-Path -LiteralPath $ConfigPath -PathType Leaf) { return Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json } return $null } function Write-AgentConfig { param([object]$Config) $dir = Split-Path -Path $ConfigPath -Parent New-Item -ItemType Directory -Force -Path $dir | Out-Null $Config | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $ConfigPath -Encoding UTF8 } function Initialize-AgentConfig { $config = Read-AgentConfig if ($null -eq $config) { if ([string]::IsNullOrWhiteSpace($ServerUrl)) { throw "ServerUrl is required for first start or install from SFERA web." } if ([string]::IsNullOrWhiteSpace($AgentId)) { $AgentId = "sfera-$env:COMPUTERNAME" } $config = [ordered]@{ server_url = $ServerUrl.TrimEnd("/") api_url = "" agent_id = $AgentId poll_seconds = $PollSeconds network_roots = @($NetworkRoot) } Write-AgentConfig -Config $config return $config } if (![string]::IsNullOrWhiteSpace($ServerUrl)) { $config.server_url = $ServerUrl.TrimEnd("/") } if (![string]::IsNullOrWhiteSpace($AgentId)) { $config.agent_id = $AgentId } if ($NetworkRoot.Count -gt 0) { $config.network_roots = @($NetworkRoot) } if ($PollSeconds -gt 0) { $config.poll_seconds = $PollSeconds } Write-AgentConfig -Config $config return $config } function Find-1CPlatformBins { $roots = @( "${env:ProgramFiles}\1cv8", "${env:ProgramFiles(x86)}\1cv8" ) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) } $bins = foreach ($root in $roots) { Get-ChildItem -LiteralPath $root -Recurse -Filter "1cv8.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName } return @($bins | Sort-Object -Descending | Select-Object -First 20) } function Get-AgentNetworkRoots { param([object]$Config) $roots = @($Config.network_roots) | Where-Object { ![string]::IsNullOrWhiteSpace($_) } $mappedRoots = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | Where-Object { ![string]::IsNullOrWhiteSpace($_.DisplayRoot) } | Select-Object -ExpandProperty DisplayRoot return @($roots + $mappedRoots | Sort-Object -Unique) } function Sync-AgentConfigFromServer { param([object]$Config) try { $agentQuery = [System.Uri]::EscapeDataString($Config.agent_id) $remote = Invoke-AgentRestMethod -Method Get -Uri (Agent-ApiUrl "/agent/config?agent_id=$agentQuery") $roots = @($Config.network_roots) foreach ($project in @($remote.projects)) { foreach ($root in @($project.network_roots)) { if (![string]::IsNullOrWhiteSpace($root) -and $roots -notcontains $root) { $roots += $root } } } $Config.network_roots = @($roots) if ($remote.poll_seconds -gt 0) { $Config.poll_seconds = [int]$remote.poll_seconds } Write-AgentConfig -Config $Config } catch { Write-AgentLog "Config sync skipped: $($_.Exception.Message)" } } function Test-AgentUpdate { param() try { $manifest = Invoke-AgentRestMethod -Method Get -Uri (Agent-ApiUrl "/agent/windows/manifest") $localHash = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant() $remoteVersion = [string]$manifest.version $remoteHash = [string]$manifest.script_hash $needsUpdate = $false if (![string]::IsNullOrWhiteSpace($remoteVersion)) { $needsUpdate = $remoteVersion -ne $AgentVersion } elseif (![string]::IsNullOrWhiteSpace($remoteHash)) { $needsUpdate = !$localHash.StartsWith($remoteHash) } if ($needsUpdate) { $tmp = "$PSCommandPath.new" Invoke-AgentWebRequest -Uri $manifest.script_url -OutFile $tmp Move-Item -LiteralPath $tmp -Destination $PSCommandPath -Force Write-AgentLog "Agent updated from server. Restarting agent process." $powerShellPath = Get-AgentPowerShellPath $arguments = @("-STA", "-NoProfile", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass", "-File", "`"$PSCommandPath`"", "-ConfigPath", "`"$ConfigPath`"") Start-Process -FilePath $powerShellPath -ArgumentList $arguments -WindowStyle Hidden exit 0 } } catch { Write-AgentLog "Update check skipped: $($_.Exception.Message)" } } function Send-Heartbeat { param([object]$Config, [string[]]$PlatformBins) $networkRoots = Get-AgentNetworkRoots -Config $Config $body = @{ agent_id = $Config.agent_id host = $env:COMPUTERNAME user = "$env:USERDOMAIN\$env:USERNAME" version = $AgentVersion started_at = $startedAt network_roots = @($networkRoots) platform_bins = @($PlatformBins) } Invoke-AgentRestMethod ` -Method Post ` -Uri (Agent-ApiUrl "/agent/heartbeat") ` -ContentType $JsonContentType ` -Body ($body | ConvertTo-Json -Depth 8) | Out-Null } function Send-JobLogs { param([string]$JobId, [string[]]$Logs, [string]$Status = $null) $body = @{ logs = $Logs } if ($Status) { $body.status = $Status } Invoke-AgentRestMethod -Method Post -Uri (Agent-ApiUrl "/agent/jobs/$JobId/logs") -ContentType $JsonContentType -Body ($body | ConvertTo-Json -Depth 6) | Out-Null } function Complete-Job { param([string]$JobId, [string]$Status, [string[]]$Logs, [string]$ErrorMessage = $null) $body = @{ status = $Status; logs = $Logs } if ($ErrorMessage) { $body.error = $ErrorMessage } Invoke-AgentRestMethod -Method Post -Uri (Agent-ApiUrl "/agent/jobs/$JobId/result") -ContentType $JsonContentType -Body ($body | ConvertTo-Json -Depth 8) | Out-Null } function Upload-Zip { param([string]$JobId, [string]$ZipPath) if (!(Test-Path -LiteralPath $ZipPath -PathType Leaf)) { throw "Agent archive was not created: $ZipPath" } $zipSize = (Get-Item -LiteralPath $ZipPath).Length $zipSizeMb = [Math]::Round($zipSize / 1MB, 1) Send-JobLogs -JobId $JobId -Logs @("Uploading archive to SFERA server. Size: $zipSizeMb MB.") $name = [System.Uri]::EscapeDataString([System.IO.Path]::GetFileName($ZipPath)) $uploadUri = Agent-ApiUrl "/agent/jobs/$JobId/upload?filename=$name" $request = [System.Net.HttpWebRequest]::Create($uploadUri) $request.Method = "POST" $request.ContentType = "application/octet-stream" $request.ContentLength = $zipSize $request.Timeout = 7200000 $request.ReadWriteTimeout = 7200000 $request.AllowWriteStreamBuffering = $false $buffer = New-Object byte[] (1024 * 1024) $sent = 0L $nextLogAt = 256MB $fileStream = [System.IO.File]::Open($ZipPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read) $requestStream = $request.GetRequestStream() try { while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $requestStream.Write($buffer, 0, $read) $sent += $read if ($sent -ge $nextLogAt -or $sent -eq $zipSize) { $sentMb = [Math]::Round($sent / 1MB, 1) $percent = if ($zipSize -gt 0) { [Math]::Round(($sent * 100.0) / $zipSize, 1) } else { 100 } Send-JobLogs -JobId $JobId -Logs @("Uploaded $sentMb / $zipSizeMb MB ($percent%).") $nextLogAt = $sent + 256MB } } } finally { $requestStream.Dispose() $fileStream.Dispose() } $response = $request.GetResponse() try { if ([int]$response.StatusCode -lt 200 -or [int]$response.StatusCode -ge 300) { throw "Archive upload failed with HTTP status $([int]$response.StatusCode) $($response.StatusDescription)." } } finally { $response.Dispose() } Send-JobLogs -JobId $JobId -Logs @("Archive upload completed.") } function Get-AgentLongPath { param([string]$Path) if ($Path.StartsWith("\\?\")) { return $Path } if ($Path.StartsWith("\\")) { return "\\?\UNC\" + $Path.Substring(2) } return "\\?\" + $Path } function New-AgentZipArchive { param([string]$SourceDir, [string]$ZipPath, [string]$JobId) Add-Type -AssemblyName System.IO.Compression Add-Type -AssemblyName System.IO.Compression.FileSystem $resolvedSource = (Resolve-Path -LiteralPath $SourceDir).Path.TrimEnd("\") $longSource = Get-AgentLongPath -Path $resolvedSource $longZip = Get-AgentLongPath -Path $ZipPath $zipStream = [System.IO.File]::Open($longZip, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) $archive = [System.IO.Compression.ZipArchive]::new($zipStream, [System.IO.Compression.ZipArchiveMode]::Create, $false, [System.Text.Encoding]::UTF8) $added = 0 $skipped = 0 try { $files = [System.IO.Directory]::EnumerateFiles($longSource, "*", [System.IO.SearchOption]::AllDirectories) foreach ($file in $files) { try { $normalFile = if ($file.StartsWith("\\?\UNC\")) { "\\" + $file.Substring(8) } elseif ($file.StartsWith("\\?\")) { $file.Substring(4) } else { $file } $relative = $normalFile.Substring($resolvedSource.Length).TrimStart("\").Replace("\", "/") if ([string]::IsNullOrWhiteSpace($relative)) { continue } $entry = $archive.CreateEntry($relative, [System.IO.Compression.CompressionLevel]::Optimal) $entryStream = $entry.Open() $fileStream = [System.IO.File]::Open($file, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) try { $fileStream.CopyTo($entryStream) } finally { $fileStream.Dispose() $entryStream.Dispose() } $added += 1 if ($added % 5000 -eq 0) { Send-JobLogs -JobId $JobId -Logs @("Packed $added files, skipped $skipped files.") } } catch { $skipped += 1 if ($skipped -le 10 -or $skipped % 100 -eq 0) { Send-JobLogs -JobId $JobId -Logs @("Skipped file while packing: $($_.Exception.Message)") } } } } finally { $archive.Dispose() $zipStream.Dispose() } if ($added -eq 0) { throw "Agent archive is empty. Source directory contains no readable files: $SourceDir" } return @{ added = $added; skipped = $skipped } } function Get-JobMetadataValue { param([object]$Job, [string]$Key) if ($null -eq $Job -or $null -eq $Job.metadata) { return "" } if ($Job.metadata.PSObject.Properties.Name -contains $Key) { return [string]$Job.metadata.$Key } return "" } function Get-JobMetadataBoolean { param([object]$Job, [string]$Key, [bool]$DefaultValue = $false) if ($null -eq $Job -or $null -eq $Job.metadata) { return $DefaultValue } if (!($Job.metadata.PSObject.Properties.Name -contains $Key)) { return $DefaultValue } $value = $Job.metadata.$Key if ($null -eq $value) { return $DefaultValue } if ($value -is [bool]) { return [bool]$value } $text = ([string]$value).Trim().ToLowerInvariant() if (@("true", "1", "yes") -contains $text) { return $true } if (@("false", "0", "no") -contains $text) { return $false } return $DefaultValue } function Get-JobMetadataList { param([object]$Job, [string]$Key) if ($null -eq $Job -or $null -eq $Job.metadata) { return @() } if (!($Job.metadata.PSObject.Properties.Name -contains $Key)) { return @() } $value = $Job.metadata.$Key if ($null -eq $value) { return @() } if ($value -is [System.Array]) { return @($value | ForEach-Object { [string]$_ } | Where-Object { ![string]::IsNullOrWhiteSpace($_) }) } return @(([string]$value).Split(",;`n`r", [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } function Get-DesignerBinPath { param([object]$Job, [string[]]$PlatformBins) if (![string]::IsNullOrWhiteSpace($Job.bin_path)) { return [string]$Job.bin_path } $configured = Get-JobMetadataValue -Job $Job -Key "one_c_bin" if (![string]::IsNullOrWhiteSpace($configured)) { return $configured } if ($PlatformBins.Count -gt 0) { return [string]$PlatformBins[0] } throw "1C Designer executable was not found. Set path to 1cv8.exe in project agent settings." } function Get-AgentExportInfobaseArg { param([object]$Job) $connection = if (![string]::IsNullOrWhiteSpace($Job.infobase)) { [string]$Job.infobase } else { Get-JobMetadataValue -Job $Job -Key "one_c_connection" } if (![string]::IsNullOrWhiteSpace($connection)) { if ($connection -match '^[^;=]+\\[^;=]+$') { return @("/S", $connection) } if ($connection -match '(?i)^\s*(Srvr|File|ws|Usr|Pwd)\s*=') { return @("/IBConnectionString", $connection) } if ($connection -match '^\s*https?://') { return @("/WS", $connection) } return @("/IBConnectionString", $connection) } $server = Get-JobMetadataValue -Job $Job -Key "one_c_server" $infobase = Get-JobMetadataValue -Job $Job -Key "one_c_infobase" if ([string]::IsNullOrWhiteSpace($server) -or [string]::IsNullOrWhiteSpace($infobase)) { throw "1C server and infobase are required for CF/CFE export." } return @("/S", "$server/$infobase") } function Convert-AgentProcessArgument { param([string]$Value) if ($Value -match '[\s"]') { return '"' + ($Value -replace '"', '\"') + '"' } return $Value } function Invoke-DesignerCommand { param([string]$DesignerPath, [string[]]$Arguments, [string]$LogPath, [string]$JobId, [string]$ActionTitle, [int]$TimeoutSeconds = 900) if (!(Test-Path -LiteralPath $DesignerPath -PathType Leaf)) { throw "1C Designer executable not found: $DesignerPath" } $fullArguments = @("DESIGNER", "/DisableStartupDialogs", "/DisableStartupMessages") + $Arguments + @("/Out", $LogPath, "-NoTruncate") Send-JobLogs -JobId $JobId -Logs @("$ActionTitle started.") $argumentLine = ($fullArguments | ForEach-Object { Convert-AgentProcessArgument -Value ([string]$_) }) -join " " $safeArguments = New-Object System.Collections.Generic.List[string] $maskNext = $false foreach ($argument in $fullArguments) { $value = [string]$argument if ($maskNext) { $safeArguments.Add("***") $maskNext = $false continue } $safeArguments.Add((Convert-AgentProcessArgument -Value $value)) if ($value -eq "/P") { $maskNext = $true } } $safeArgumentLine = $safeArguments -join " " Send-JobLogs -JobId $JobId -Logs @("$ActionTitle command: `"$DesignerPath`" $safeArgumentLine") $process = Start-Process -FilePath $DesignerPath -ArgumentList $argumentLine -PassThru -WindowStyle Hidden $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() while (!$process.HasExited) { if ($stopwatch.Elapsed.TotalSeconds -gt $TimeoutSeconds) { try { $process.Kill() } catch {} $tail = "" if (Test-Path -LiteralPath $LogPath -PathType Leaf) { $logTextOnTimeout = Get-Content -LiteralPath $LogPath -Raw -ErrorAction SilentlyContinue if (![string]::IsNullOrWhiteSpace($logTextOnTimeout)) { $tail = if ($logTextOnTimeout.Length -gt 1200) { $logTextOnTimeout.Substring($logTextOnTimeout.Length - 1200) } else { $logTextOnTimeout } } } throw "$ActionTitle timed out after $TimeoutSeconds seconds. Designer likely waited for an interactive prompt. $tail" } Start-Sleep -Seconds 2 try { $process.Refresh() } catch {} } $logText = "" if (Test-Path -LiteralPath $LogPath -PathType Leaf) { $logText = Get-Content -LiteralPath $LogPath -Raw -ErrorAction SilentlyContinue } if ($process.ExitCode -ne 0) { $tail = if ($logText.Length -gt 1000) { $logText.Substring($logText.Length - 1000) } else { $logText } throw "$ActionTitle failed with exit code $($process.ExitCode). $tail" } if (![string]::IsNullOrWhiteSpace($logText)) { $tail = if ($logText.Length -gt 700) { $logText.Substring($logText.Length - 700) } else { $logText } Send-JobLogs -JobId $JobId -Logs @("$ActionTitle log: $tail") } Send-JobLogs -JobId $JobId -Logs @("$ActionTitle finished.") } function Invoke-1CCommand { param([string]$PlatformPath, [string[]]$Arguments, [string]$LogPath, [string]$JobId, [string]$ActionTitle, [int]$TimeoutSeconds = 180) if (!(Test-Path -LiteralPath $PlatformPath -PathType Leaf)) { throw "1C executable not found: $PlatformPath" } $fullArguments = $Arguments + @("/Out", $LogPath, "-NoTruncate") Send-JobLogs -JobId $JobId -Logs @("$ActionTitle started.") $argumentLine = ($fullArguments | ForEach-Object { Convert-AgentProcessArgument -Value ([string]$_) }) -join " " Send-JobLogs -JobId $JobId -Logs @("$ActionTitle command: `"$PlatformPath`" $argumentLine") $process = Start-Process -FilePath $PlatformPath -ArgumentList $argumentLine -PassThru -WindowStyle Hidden $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() while (!$process.HasExited) { if ($stopwatch.Elapsed.TotalSeconds -gt $TimeoutSeconds) { try { $process.Kill() } catch {} throw "$ActionTitle timed out after $TimeoutSeconds seconds." } Start-Sleep -Seconds 2 try { $process.Refresh() } catch {} } $logText = "" if (Test-Path -LiteralPath $LogPath -PathType Leaf) { $logText = Get-Content -LiteralPath $LogPath -Raw -ErrorAction SilentlyContinue } if ($process.ExitCode -ne 0) { $tail = if ($logText.Length -gt 1000) { $logText.Substring($logText.Length - 1000) } else { $logText } throw "$ActionTitle failed with exit code $($process.ExitCode). $tail" } if (![string]::IsNullOrWhiteSpace($logText)) { $tail = if ($logText.Length -gt 700) { $logText.Substring($logText.Length - 700) } else { $logText } Send-JobLogs -JobId $JobId -Logs @("$ActionTitle log: $tail") } Send-JobLogs -JobId $JobId -Logs @("$ActionTitle finished.") } function Get-InfobaseExtensionNames { param([string]$DesignerPath, [string[]]$BaseArgs, [string]$ExportRoot, [string]$JobId) $logPath = Join-Path $ExportRoot "designer-dumpdbcfglist.log" Invoke-DesignerCommand -DesignerPath $DesignerPath -Arguments (@($BaseArgs) + @("/DumpDBCfgList", "-AllExtensions")) -LogPath $logPath -JobId $JobId -ActionTitle "1C DumpDBCfgList" if (!(Test-Path -LiteralPath $logPath -PathType Leaf)) { return @() } $lines = Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue $names = foreach ($line in $lines) { $value = ([string]$line).Trim() if ([string]::IsNullOrWhiteSpace($value)) { continue } if ($value -match '^(INFO|ERROR|WARNING)[:\s]') { continue } if ($value -match '^[0-9a-fA-F]{32,}$') { continue } $first = ($value -split '\s+')[0].Trim() if (![string]::IsNullOrWhiteSpace($first)) { $first } } return @($names | Sort-Object -Unique) } function Export-CfOrCfeFromInfobase { param([object]$Job, [string[]]$PlatformBins) $workRoot = Join-Path $env:TEMP "sfera-agent" $exportRoot = Join-Path $workRoot "$($Job.job_id)-export" if (Test-Path -LiteralPath $exportRoot) { Remove-Item -LiteralPath $exportRoot -Recurse -Force } New-Item -ItemType Directory -Force -Path $exportRoot | Out-Null $designerPath = Get-DesignerBinPath -Job $Job -PlatformBins $PlatformBins $baseArgs = Get-AgentExportInfobaseArg -Job $Job $user = Get-JobMetadataValue -Job $Job -Key "one_c_user" $password = Get-JobMetadataValue -Job $Job -Key "one_c_password" if (![string]::IsNullOrWhiteSpace($user)) { $baseArgs += @("/N", $user) } if (![string]::IsNullOrWhiteSpace($password)) { $baseArgs += @("/P", $password) } $isSingleExtensionExport = [string]$Job.source -eq "CFE_FILE" $extensionName = Get-JobMetadataValue -Job $Job -Key "one_c_extension" if ($isSingleExtensionExport -and [string]::IsNullOrWhiteSpace($extensionName)) { throw "Extension name is required for CFE export." } $fileName = if ($isSingleExtensionExport) { "$extensionName.cfe" } else { "$($Job.project_id).cf" } $artifactsRoot = Join-Path $exportRoot "artifacts" New-Item -ItemType Directory -Force -Path $artifactsRoot | Out-Null $dumpFile = Join-Path $exportRoot $fileName $logPath = Join-Path $exportRoot "designer-dumpcfg.log" $dumpArgs = @($baseArgs) + @("/DumpCfg", $dumpFile) if ($isSingleExtensionExport) { $dumpArgs += @("-Extension", $extensionName) } Invoke-DesignerCommand -DesignerPath $designerPath -Arguments $dumpArgs -LogPath $logPath -JobId $Job.job_id -ActionTitle "1C DumpCfg" if (!(Test-Path -LiteralPath $dumpFile -PathType Leaf)) { throw "1C DumpCfg completed but output file was not created: $dumpFile" } $dumpSizeMb = [Math]::Round((Get-Item -LiteralPath $dumpFile).Length / 1MB, 1) Send-JobLogs -JobId $Job.job_id -Logs @("Exported $fileName from infobase. Size: $dumpSizeMb MB.") Copy-Item -LiteralPath $dumpFile -Destination (Join-Path $artifactsRoot $fileName) -Force $metadataRoot = if ($isSingleExtensionExport) { Join-Path $exportRoot "extension" } else { Join-Path $exportRoot "configuration" } New-Item -ItemType Directory -Force -Path $metadataRoot | Out-Null $metadataLogPath = Join-Path $exportRoot "designer-dumpconfigtofiles.log" $metadataArgs = @($baseArgs) + @("/DumpConfigToFiles", $metadataRoot, "-Format", "Hierarchical") if ($isSingleExtensionExport) { $metadataArgs += @("-Extension", $extensionName) } try { Invoke-DesignerCommand -DesignerPath $designerPath -Arguments $metadataArgs -LogPath $metadataLogPath -JobId $Job.job_id -ActionTitle "1C DumpConfigToFiles" Copy-Item -LiteralPath $dumpFile -Destination (Join-Path $metadataRoot $fileName) -Force Send-JobLogs -JobId $Job.job_id -Logs @("Metadata files exported for server-side parsing.") } catch { Send-JobLogs -JobId $Job.job_id -Logs @("Metadata files export failed for main configuration, binary file is still included. $($_.Exception.Message)") } if (!$isSingleExtensionExport -and (Get-JobMetadataBoolean -Job $Job -Key "include_extensions" -DefaultValue $true)) { $extensionNames = Get-JobMetadataList -Job $Job -Key "one_c_extensions" if ($extensionNames.Count -eq 0) { $extensionNames = Get-InfobaseExtensionNames -DesignerPath $designerPath -BaseArgs $baseArgs -ExportRoot $exportRoot -JobId $Job.job_id } Send-JobLogs -JobId $Job.job_id -Logs @("Extensions selected for export: $($extensionNames.Count).") foreach ($name in $extensionNames) { $safeName = ($name -replace '[\\/:*?"<>|]', "_") $extensionFile = Join-Path $exportRoot "$safeName.cfe" $extensionLog = Join-Path $exportRoot "designer-dumpcfg-$safeName.log" Invoke-DesignerCommand -DesignerPath $designerPath -Arguments (@($baseArgs) + @("/DumpCfg", $extensionFile, "-Extension", $name)) -LogPath $extensionLog -JobId $Job.job_id -ActionTitle "1C DumpCfg extension $name" Copy-Item -LiteralPath $extensionFile -Destination (Join-Path $artifactsRoot "$safeName.cfe") -Force $extensionMetadataRoot = Join-Path (Join-Path $exportRoot "extensions") $safeName New-Item -ItemType Directory -Force -Path $extensionMetadataRoot | Out-Null try { Invoke-DesignerCommand -DesignerPath $designerPath -Arguments (@($baseArgs) + @("/DumpConfigToFiles", $extensionMetadataRoot, "-Format", "Hierarchical", "-Extension", $name)) -LogPath (Join-Path $exportRoot "designer-dumpconfigtofiles-$safeName.log") -JobId $Job.job_id -ActionTitle "1C DumpConfigToFiles extension $name" Copy-Item -LiteralPath $extensionFile -Destination (Join-Path $extensionMetadataRoot "$safeName.cfe") -Force } catch { Send-JobLogs -JobId $Job.job_id -Logs @("Metadata files export failed for extension $name, CFE is still included. $($_.Exception.Message)") } } } return $exportRoot } function Convert-LocalCfOrCfeToMetadataExport { param([object]$Job, [string[]]$PlatformBins) $payloadPath = [string]$Job.local_path if ([string]::IsNullOrWhiteSpace($payloadPath)) { throw "local_path is required for direct CF/CFE conversion." } if (!(Test-Path -LiteralPath $payloadPath)) { throw "Local CF/CFE path not found on agent machine: $payloadPath" } $designerPath = Get-DesignerBinPath -Job $Job -PlatformBins $PlatformBins $workRoot = Join-Path $env:TEMP "sfera-agent" $exportRoot = Join-Path $workRoot "$($Job.job_id)-local-binary" if (Test-Path -LiteralPath $exportRoot) { Remove-Item -LiteralPath $exportRoot -Recurse -Force } New-Item -ItemType Directory -Force -Path $exportRoot | Out-Null $builderInfobase = Join-Path $exportRoot "builder-infobase" $createLog = Join-Path $exportRoot "create-builder-infobase.log" Invoke-1CCommand ` -PlatformPath $designerPath ` -Arguments @("CREATEINFOBASE", "File=$builderInfobase;") ` -LogPath $createLog ` -JobId $Job.job_id ` -ActionTitle "1C CREATEINFOBASE for local CF/CFE conversion" ` -TimeoutSeconds 180 $builderArgs = @("/F", $builderInfobase) $sourceKind = [string]$Job.source $fileName = [System.IO.Path]::GetFileName($payloadPath) $artifactsRoot = Join-Path $exportRoot "artifacts" New-Item -ItemType Directory -Force -Path $artifactsRoot | Out-Null Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $artifactsRoot $fileName) -Force if ($sourceKind -eq "CF_FILE") { $loadLog = Join-Path $exportRoot "designer-loadcfg-local-cf.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/LoadCfg", $payloadPath)) ` -LogPath $loadLog ` -JobId $Job.job_id ` -ActionTitle "1C LoadCfg local CF" ` -TimeoutSeconds 180 $metadataRoot = Join-Path $exportRoot "configuration" New-Item -ItemType Directory -Force -Path $metadataRoot | Out-Null $metadataLog = Join-Path $exportRoot "designer-dumpconfigtofiles-local-cf.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/DumpConfigToFiles", $metadataRoot, "-Format", "Hierarchical")) ` -LogPath $metadataLog ` -JobId $Job.job_id ` -ActionTitle "1C DumpConfigToFiles from local CF" ` -TimeoutSeconds 180 Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $metadataRoot $fileName) -Force Send-JobLogs -JobId $Job.job_id -Logs @("Local .cf converted to metadata export for server-side parsing.") return $exportRoot } if ($sourceKind -eq "CFE_FILE") { $extensionName = Get-JobMetadataValue -Job $Job -Key "one_c_extension" if ([string]::IsNullOrWhiteSpace($extensionName)) { $extensionName = [System.IO.Path]::GetFileNameWithoutExtension($payloadPath) } if ([string]::IsNullOrWhiteSpace($extensionName)) { throw "Extension name is required for local CFE conversion." } $loadLog = Join-Path $exportRoot "designer-loadcfg-local-cfe.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/LoadCfg", $payloadPath, "-Extension", $extensionName, "/UpdateDBCfg")) ` -LogPath $loadLog ` -JobId $Job.job_id ` -ActionTitle "1C LoadCfg local CFE" ` -TimeoutSeconds 180 $metadataRoot = Join-Path $exportRoot "extension" New-Item -ItemType Directory -Force -Path $metadataRoot | Out-Null $metadataLog = Join-Path $exportRoot "designer-dumpconfigtofiles-local-cfe.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/DumpConfigToFiles", $metadataRoot, "-Format", "Hierarchical", "-Extension", $extensionName)) ` -LogPath $metadataLog ` -JobId $Job.job_id ` -ActionTitle "1C DumpConfigToFiles from local CFE" ` -TimeoutSeconds 180 Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $metadataRoot $fileName) -Force Send-JobLogs -JobId $Job.job_id -Logs @("Local .cfe converted to metadata export for server-side parsing.") return $exportRoot } throw "Unsupported source for local CF/CFE conversion: $sourceKind" } function Install-SferaExtensionJob { param([object]$Job, [string[]]$PlatformBins) $workRoot = Join-Path $env:TEMP "sfera-agent" $installRoot = Join-Path $workRoot "$($Job.job_id)-sfera-extension" if (Test-Path -LiteralPath $installRoot) { Remove-Item -LiteralPath $installRoot -Recurse -Force } New-Item -ItemType Directory -Force -Path $installRoot | Out-Null $designerPath = Get-DesignerBinPath -Job $Job -PlatformBins $PlatformBins $baseArgs = Get-AgentExportInfobaseArg -Job $Job $user = Get-JobMetadataValue -Job $Job -Key "one_c_user" $password = Get-JobMetadataValue -Job $Job -Key "one_c_password" if (![string]::IsNullOrWhiteSpace($user)) { $baseArgs += @("/N", $user) } if (![string]::IsNullOrWhiteSpace($password)) { $baseArgs += @("/P", $password) } $extensionName = Get-JobMetadataValue -Job $Job -Key "extension_name" if ([string]::IsNullOrWhiteSpace($extensionName)) { $extensionName = "SFERA" } $forceReinstall = Get-JobMetadataBoolean -Job $Job -Key "force_reinstall" -DefaultValue $true if ($forceReinstall) { $deleteLog = Join-Path $installRoot "designer-delete-sfera-extension.log" $deletedExistingExtension = $false try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/DeleteCfg", "-Extension", $extensionName)) ` -LogPath $deleteLog ` -JobId $Job.job_id ` -ActionTitle "1C DeleteCfg stale SFERA extension" ` -TimeoutSeconds 180 $deletedExistingExtension = $true } catch { Send-JobLogs -JobId $Job.job_id -Logs @("DeleteCfg skipped or extension was absent. $($_.Exception.Message)") } if ($deletedExistingExtension) { $deleteUpdateLog = Join-Path $installRoot "designer-updatedbcfg-delete-sfera-extension.log" try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/UpdateDBCfg", "-Extension", $extensionName)) ` -LogPath $deleteUpdateLog ` -JobId $Job.job_id ` -ActionTitle "1C UpdateDBCfg after stale SFERA extension delete" ` -TimeoutSeconds 180 } catch { Send-JobLogs -JobId $Job.job_id -Logs @("UpdateDBCfg after DeleteCfg skipped. $($_.Exception.Message)") } } $rollbackMainLog = Join-Path $installRoot "designer-rollback-main-config.log" try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/RollbackCfg")) ` -LogPath $rollbackMainLog ` -JobId $Job.job_id ` -ActionTitle "1C RollbackCfg main configuration changes" ` -TimeoutSeconds 180 } catch { Send-JobLogs -JobId $Job.job_id -Logs @("RollbackCfg main configuration skipped. $($_.Exception.Message)") } $rollbackLog = Join-Path $installRoot "designer-rollback-stale-main-config.log" try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/RollbackCfg", "-Extension", $extensionName)) ` -LogPath $rollbackLog ` -JobId $Job.job_id ` -ActionTitle "1C RollbackCfg stale main configuration changes" ` -TimeoutSeconds 180 } catch { Send-JobLogs -JobId $Job.job_id -Logs @("RollbackCfg skipped. $($_.Exception.Message)") } } $cfePath = Get-JobMetadataValue -Job $Job -Key "extension_cfe_path" $builtCfeFromXml = $false if ([string]::IsNullOrWhiteSpace($cfePath)) { $packageUrl = Get-JobMetadataValue -Job $Job -Key "extension_package_url" $sourceRoot = "" if (![string]::IsNullOrWhiteSpace($packageUrl)) { $packagePath = Join-Path $installRoot "sfera-extension-source.zip" Send-JobLogs -JobId $Job.job_id -Logs @("Downloading SFERA extension source package from server.") Invoke-AgentWebRequest -Method Get -Uri $packageUrl -OutFile $packagePath -TimeoutSec 120 | Out-Null Send-JobLogs -JobId $Job.job_id -Logs @("Downloaded SFERA extension source package to $packagePath.") $sourceRoot = Join-Path $installRoot "source" New-Item -ItemType Directory -Force -Path $sourceRoot | Out-Null Expand-Archive -LiteralPath $packagePath -DestinationPath $sourceRoot -Force } if ([string]::IsNullOrWhiteSpace($sourceRoot)) { throw "SFERA extension package URL is empty and compiled .cfe path is not configured." } $xmlRootCandidates = @( (Join-Path $sourceRoot "xml"), (Join-Path $sourceRoot "configuration"), (Join-Path $sourceRoot "src"), $sourceRoot ) $xmlRoot = $xmlRootCandidates | Where-Object { Test-Path -LiteralPath $_ -PathType Container } | Select-Object -First 1 if ([string]::IsNullOrWhiteSpace($xmlRoot)) { throw "SFERA extension source package does not contain a loadable folder." } $builtRoot = Join-Path $env:ProgramData "SFERA\WindowsAgent\extensions" New-Item -ItemType Directory -Force -Path $builtRoot | Out-Null $cfePath = Join-Path $builtRoot "$extensionName.cfe" $builderInfobase = Join-Path $installRoot "builder-infobase" if (Test-Path -LiteralPath $builderInfobase) { Remove-Item -LiteralPath $builderInfobase -Recurse -Force } New-Item -ItemType Directory -Force -Path (Split-Path -Path $builderInfobase -Parent) | Out-Null $builderArgs = @("/F", $builderInfobase) $builderReady = $false try { Send-JobLogs -JobId $Job.job_id -Logs @("Building SFERA extension .cfe in isolated temporary file infobase: $builderInfobase") $createBuilderLog = Join-Path $installRoot "create-builder-infobase.log" Invoke-1CCommand ` -PlatformPath $designerPath ` -Arguments @("CREATEINFOBASE", "File=$builderInfobase;") ` -LogPath $createBuilderLog ` -JobId $Job.job_id ` -ActionTitle "1C CREATEINFOBASE for SFERA extension builder" ` -TimeoutSeconds 180 $loadXmlLog = Join-Path $installRoot "designer-load-sfera-extension-from-files.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/LoadConfigFromFiles", $xmlRoot, "-Extension", $extensionName, "-Format", "Hierarchical", "-updateConfigDumpInfo")) ` -LogPath $loadXmlLog ` -JobId $Job.job_id ` -ActionTitle "1C LoadConfigFromFiles SFERA extension" ` -TimeoutSeconds 180 $builderReady = $true } catch { Send-JobLogs -JobId $Job.job_id -Logs @("Isolated file infobase build is unavailable. Falling back to selected infobase builder. $($_.Exception.Message)") $builderArgs = @($baseArgs) $loadXmlLog = Join-Path $installRoot "designer-load-sfera-extension-from-files-target-builder.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/LoadConfigFromFiles", $xmlRoot, "-Extension", $extensionName, "-Format", "Hierarchical", "-updateConfigDumpInfo")) ` -LogPath $loadXmlLog ` -JobId $Job.job_id ` -ActionTitle "1C LoadConfigFromFiles SFERA extension in selected infobase builder" ` -TimeoutSeconds 180 $builtCfeFromXml = $true $builderReady = $true } if (!$builderReady) { throw "SFERA extension builder was not prepared." } $dumpLog = Join-Path $installRoot "designer-dump-sfera-extension-cfe.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($builderArgs) + @("/DumpCfg", $cfePath, "-Extension", $extensionName)) ` -LogPath $dumpLog ` -JobId $Job.job_id ` -ActionTitle "1C DumpCfg SFERA extension to .cfe" ` -TimeoutSeconds 180 Send-JobLogs -JobId $Job.job_id -Logs @("Built SFERA extension .cfe without EDT: $cfePath") if (($builtCfeFromXml) -or (($builderArgs -join "|") -eq (@($baseArgs) -join "|"))) { Send-JobLogs -JobId $Job.job_id -Logs @("Selected infobase was used only as a temporary .cfe builder. The staged XML extension will be removed before installing the built .cfe.") } } else { if (!(Test-Path -LiteralPath $cfePath -PathType Leaf)) { throw "SFERA extension .cfe not found on agent machine: $cfePath" } } if ($builtCfeFromXml) { $deleteStagedLog = Join-Path $installRoot "designer-delete-staged-sfera-extension.log" try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/DeleteCfg", "-Extension", $extensionName)) ` -LogPath $deleteStagedLog ` -JobId $Job.job_id ` -ActionTitle "1C DeleteCfg staged SFERA extension before .cfe install" ` -TimeoutSeconds 180 } catch { Send-JobLogs -JobId $Job.job_id -Logs @("DeleteCfg staged extension skipped. $($_.Exception.Message)") } $deleteStagedUpdateLog = Join-Path $installRoot "designer-updatedbcfg-delete-staged-sfera-extension.log" try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/UpdateDBCfg", "-Extension", $extensionName)) ` -LogPath $deleteStagedUpdateLog ` -JobId $Job.job_id ` -ActionTitle "1C UpdateDBCfg after staged SFERA extension delete" ` -TimeoutSeconds 180 } catch { Send-JobLogs -JobId $Job.job_id -Logs @("UpdateDBCfg after staged DeleteCfg skipped. $($_.Exception.Message)") } $rollbackStagedLog = Join-Path $installRoot "designer-rollback-staged-main-config.log" try { Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/RollbackCfg", "-Extension", $extensionName)) ` -LogPath $rollbackStagedLog ` -JobId $Job.job_id ` -ActionTitle "1C RollbackCfg after staged extension build" ` -TimeoutSeconds 180 } catch { Send-JobLogs -JobId $Job.job_id -Logs @("RollbackCfg after staged build skipped. $($_.Exception.Message)") } } $loadLog = Join-Path $installRoot "designer-load-sfera-extension.log" Invoke-DesignerCommand ` -DesignerPath $designerPath ` -Arguments (@($baseArgs) + @("/LoadCfg", $cfePath, "-Extension", $extensionName, "/UpdateDBCfg")) ` -LogPath $loadLog ` -JobId $Job.job_id ` -ActionTitle "1C LoadCfg and UpdateDBCfg SFERA extension" ` -TimeoutSeconds 180 Test-SferaExtensionInstall -Job $Job -DesignerPath $designerPath -BaseArgs $baseArgs -ExtensionName $extensionName -InstallRoot $installRoot Send-JobLogs -JobId $Job.job_id -Logs @("SFERA extension install/update finished for extension '$extensionName'.") } function Test-SferaExtensionInstall { param([object]$Job, [string]$DesignerPath, [object[]]$BaseArgs, [string]$ExtensionName, [string]$InstallRoot) $checkModulesLog = Join-Path $InstallRoot "designer-checkmodules-sfera-extension.log" Invoke-DesignerCommand ` -DesignerPath $DesignerPath ` -Arguments (@($BaseArgs) + @("/CheckModules", "-Server", "-ExternalConnection", "-Extension", $ExtensionName)) ` -LogPath $checkModulesLog ` -JobId $Job.job_id ` -ActionTitle "1C CheckModules SFERA extension" ` -TimeoutSeconds 300 $checkApplyLog = Join-Path $InstallRoot "designer-checkapply-sfera-extension.log" Invoke-DesignerCommand ` -DesignerPath $DesignerPath ` -Arguments (@($BaseArgs) + @("/CheckCanApplyConfigurationExtensions", "-Extension", $ExtensionName)) ` -LogPath $checkApplyLog ` -JobId $Job.job_id ` -ActionTitle "1C CheckCanApplyConfigurationExtensions SFERA extension" ` -TimeoutSeconds 300 } function Ensure-SferaHttpServicesPublished { param([object]$Job, [string]$ExtensionName) $vrdPath = Get-JobMetadataValue -Job $Job -Key "published_vrd_path" if ([string]::IsNullOrWhiteSpace($vrdPath)) { $infobase = Get-JobMetadataValue -Job $Job -Key "one_c_infobase" if (![string]::IsNullOrWhiteSpace($infobase)) { $candidate = Join-Path (Join-Path $env:SystemDrive "inetpub\wwwroot") (Join-Path $infobase "default.vrd") if (Test-Path -LiteralPath $candidate -PathType Leaf) { $vrdPath = $candidate } } } if ([string]::IsNullOrWhiteSpace($vrdPath)) { Send-JobLogs -JobId $Job.job_id -Logs @("default.vrd path is not configured. If /hs/sfera/health returns 404, set published_vrd_path or run the agent on the web server and republish extension HTTP services.") return } if (!(Test-Path -LiteralPath $vrdPath -PathType Leaf)) { Send-JobLogs -JobId $Job.job_id -Logs @("default.vrd not found: $vrdPath. Extension was installed, but HTTP services from extensions may remain unpublished.") return } [xml]$xml = Get-Content -LiteralPath $vrdPath -Raw $root = $xml.DocumentElement if ($null -eq $root) { Send-JobLogs -JobId $Job.job_id -Logs @("default.vrd is empty or invalid XML: $vrdPath") return } $namespace = $root.NamespaceURI $httpServices = $null foreach ($child in $root.ChildNodes) { if ($child.LocalName -eq "httpServices") { $httpServices = $child break } } if ($null -eq $httpServices) { $httpServices = if ([string]::IsNullOrWhiteSpace($namespace)) { $xml.CreateElement("httpServices") } else { $xml.CreateElement("httpServices", $namespace) } [void]$root.AppendChild($httpServices) } $httpServices.SetAttribute("publishByDefault", "true") $httpServices.SetAttribute("publishExtensionsByDefault", "true") $serviceRoot = Get-JobMetadataValue -Job $Job -Key "published_extension_service_root" if ([string]::IsNullOrWhiteSpace($serviceRoot)) { $serviceRoot = "sfera" } $serviceName = Get-JobMetadataValue -Job $Job -Key "published_extension_service_name" if ([string]::IsNullOrWhiteSpace($serviceName)) { $serviceName = "BridgeHTTP" } $existingServices = @() foreach ($child in $httpServices.ChildNodes) { if ($child.LocalName -eq "service") { $nameMatches = $child.GetAttribute("name") -eq $serviceName $rootMatches = $child.GetAttribute("rootUrl") -eq $serviceRoot if ($nameMatches -or $rootMatches) { $existingServices += $child } } } foreach ($service in $existingServices) { [void]$httpServices.RemoveChild($service) } $serviceNode = if ([string]::IsNullOrWhiteSpace($namespace)) { $xml.CreateElement("service") } else { $xml.CreateElement("service", $namespace) } $serviceNode.SetAttribute("name", $serviceName) $serviceNode.SetAttribute("rootUrl", $serviceRoot) $serviceNode.SetAttribute("enable", "true") $serviceNode.SetAttribute("reuseSessions", "autouse") $serviceNode.SetAttribute("sessionMaxAge", "20") [void]$httpServices.AppendChild($serviceNode) $backupPath = "$vrdPath.sfera-backup-$(Get-Date -Format 'yyyyMMddHHmmss')" Copy-Item -LiteralPath $vrdPath -Destination $backupPath -Force $xml.Save($vrdPath) Send-JobLogs -JobId $Job.job_id -Logs @("Updated default.vrd for extension HTTP services: $vrdPath. Service: $serviceName/$serviceRoot. Backup: $backupPath") try { $iisreset = Join-Path $env:SystemRoot "System32\iisreset.exe" if (Test-Path -LiteralPath $iisreset -PathType Leaf) { Send-JobLogs -JobId $Job.job_id -Logs @("Restarting IIS to reload default.vrd publication settings.") $process = Start-Process -FilePath $iisreset -ArgumentList @("/restart") -NoNewWindow -PassThru -Wait Send-JobLogs -JobId $Job.job_id -Logs @("IIS restart finished with exit code $($process.ExitCode).") } else { Send-JobLogs -JobId $Job.job_id -Logs @("iisreset.exe not found. Restart IIS or recycle the publication manually if /hs/sfera/health is still unavailable.") } } catch { Send-JobLogs -JobId $Job.job_id -Logs @("IIS restart skipped or failed: $($_.Exception.Message)") } } function Complete-Browse { param([string]$RequestId, [string]$Status, [object[]]$Entries = @(), [string]$ParentPath = $null, [string]$ErrorMessage = $null) $body = @{ status = $Status; entries = $Entries } if ($ParentPath) { $body.parent_path = $ParentPath } if ($ErrorMessage) { $body.error = $ErrorMessage } Invoke-AgentRestMethod -Method Post -Uri (Agent-ApiUrl "/agent/browse/$RequestId/result") -ContentType $JsonContentType -Body ($body | ConvertTo-Json -Depth 8) | Out-Null } function Handle-BrowseRequest { param([string]$Server, [object]$Browse, [object]$Config) $path = $Browse.path if ([string]::IsNullOrWhiteSpace($path)) { $driveEntries = Get-PSDrive -PSProvider FileSystem | ForEach-Object { $name = "$($_.Name):\" if (![string]::IsNullOrWhiteSpace($_.DisplayRoot)) { $name = "$name ($($_.DisplayRoot))" } @{ name = $name; path = "$($_.Name):\"; is_directory = $true } } $networkEntries = Get-AgentNetworkRoots -Config $Config | ForEach-Object { @{ name = $_; path = $_; is_directory = $true } } Complete-Browse -RequestId $Browse.request_id -Status "SUCCEEDED" -Entries @($driveEntries + $networkEntries) return } if (!(Test-Path -LiteralPath $path -PathType Container)) { throw "Folder not found on agent machine: $path" } $resolved = (Resolve-Path -LiteralPath $path).Path $parent = Split-Path -Path $resolved -Parent $entries = Get-ChildItem -LiteralPath $resolved -Directory -Force | Sort-Object Name | Select-Object -First 200 | ForEach-Object { @{ name = $_.Name; path = $_.FullName; is_directory = $true } } Complete-Browse -RequestId $Browse.request_id -Status "SUCCEEDED" -Entries @($entries) -ParentPath $parent } $config = Initialize-AgentConfig $server = ([string]$config.server_url).TrimEnd("/") $script:AgentApiBase = Get-AgentApiBase -Config $config $platformBins = Find-1CPlatformBins Write-AgentLog "SFERA Windows Agent started. Server=$server AgentId=$($config.agent_id)" Write-AgentLog "API=$script:AgentApiBase" Write-AgentLog "Config=$ConfigPath" Initialize-TrayIcon -Server $server -AgentId $config.agent_id Update-TrayIcon -Online $false -Message "connecting" $lastUpdateCheck = [DateTime]::MinValue $lastConfigSync = [DateTime]::MinValue $lastHeartbeatLog = [DateTime]::MinValue while ($true) { $job = $null try { $config = Read-AgentConfig $server = ([string]$config.server_url).TrimEnd("/") $script:AgentApiBase = Get-AgentApiBase -Config $config if (((Get-Date) - $lastUpdateCheck).TotalMinutes -ge 15) { Test-AgentUpdate $lastUpdateCheck = Get-Date } if (((Get-Date) - $lastConfigSync).TotalSeconds -ge 60) { Sync-AgentConfigFromServer -Config $config $platformBins = Find-1CPlatformBins $lastConfigSync = Get-Date } Send-Heartbeat -Config $config -PlatformBins $platformBins Update-TrayIcon -Online $true -Message $config.agent_id if (((Get-Date) - $lastHeartbeatLog).TotalSeconds -ge 60) { Write-AgentLog "Heartbeat sent. AgentId=$($config.agent_id)" $lastHeartbeatLog = Get-Date } $agentQuery = [System.Uri]::EscapeDataString($config.agent_id) $browse = Invoke-AgentRestMethod -Method Get -Uri (Agent-ApiUrl "/agent/browse/next?agent_id=$agentQuery") if ($null -ne $browse -and ![string]::IsNullOrWhiteSpace($browse.request_id)) { try { Write-AgentLog "Browse request $($browse.request_id): $($browse.path)" Handle-BrowseRequest -Server $server -Browse $browse -Config $config } catch { Complete-Browse -RequestId $browse.request_id -Status "FAILED" -ErrorMessage $_.Exception.Message } continue } $versionQuery = [System.Uri]::EscapeDataString($AgentVersion) $job = Invoke-AgentRestMethod -Method Get -Uri (Agent-ApiUrl "/agent/jobs/next?agent_id=$agentQuery&version=$versionQuery") if ($null -eq $job -or [string]::IsNullOrWhiteSpace($job.job_id)) { Pump-TrayIcon Start-Sleep -Seconds ([int]$config.poll_seconds) continue } Write-AgentLog "Claimed job $($job.job_id): $($job.source) $($job.mode)" Send-JobLogs -JobId $job.job_id -Logs @("Windows agent claimed job on $env:COMPUTERNAME.") $workRoot = Join-Path $env:TEMP "sfera-agent" New-Item -ItemType Directory -Force -Path $workRoot | Out-Null $agentAction = Get-JobMetadataValue -Job $job -Key "agent_action" if ($agentAction -eq "INSTALL_SFERA_EXTENSION") { Install-SferaExtensionJob -Job $job -PlatformBins $platformBins Complete-Job -JobId $job.job_id -Status "SUCCEEDED" -Logs @("SFERA extension install/update completed.") Write-AgentLog "Completed service job $($job.job_id)" continue } $payloadPath = $job.local_path if (($job.source -eq "CF_FILE") -or ($job.source -eq "CFE_FILE")) { if (![string]::IsNullOrWhiteSpace($payloadPath)) { $payloadPath = Convert-LocalCfOrCfeToMetadataExport -Job $job -PlatformBins $platformBins } else { $payloadPath = Export-CfOrCfeFromInfobase -Job $job -PlatformBins $platformBins } } if ([string]::IsNullOrWhiteSpace($payloadPath)) { throw "Job does not contain local_path or enough 1C infobase settings for agent export." } if (!(Test-Path -LiteralPath $payloadPath)) { throw "Local path not found on agent machine: $payloadPath" } $zipSource = $payloadPath if (Test-Path -LiteralPath $payloadPath -PathType Leaf) { $singleFileRoot = Join-Path $workRoot "$($job.job_id)-single-file" if (Test-Path -LiteralPath $singleFileRoot) { Remove-Item -LiteralPath $singleFileRoot -Recurse -Force } New-Item -ItemType Directory -Force -Path $singleFileRoot | Out-Null Copy-Item -LiteralPath $payloadPath -Destination (Join-Path $singleFileRoot ([System.IO.Path]::GetFileName($payloadPath))) -Force $zipSource = $singleFileRoot } $zipPath = Join-Path $workRoot "$($job.job_id).zip" if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force } $packStats = New-AgentZipArchive -SourceDir $zipSource -ZipPath $zipPath -JobId $job.job_id if (!(Test-Path -LiteralPath $zipPath -PathType Leaf)) { throw "Agent failed to create archive: $zipPath" } Send-JobLogs -JobId $job.job_id -Logs @("Packed payload $payloadPath to $zipPath. Files: $($packStats.added), skipped: $($packStats.skipped).") Upload-Zip -JobId $job.job_id -ZipPath $zipPath Complete-Job -JobId $job.job_id -Status "SUCCEEDED" -Logs @("Uploaded payload and requested server import.") Write-AgentLog "Completed job $($job.job_id)" } catch { Write-AgentLog "ERROR: $($_.Exception.Message)" Update-TrayIcon -Online $false -Message $_.Exception.Message if ($job -and $job.job_id) { Complete-Job -JobId $job.job_id -Status "FAILED" -Logs @("Agent failed.") -ErrorMessage $_.Exception.Message } Pump-TrayIcon Start-Sleep -Seconds ([int]$config.poll_seconds) } }