Files
sfera/scripts/windows-agent/sfera-windows-agent.ps1
m de8b0eb795
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled
Support direct CF and CFE inputs in AI structure flow
2026-05-22 00:45:57 +03:00

1278 lines
54 KiB
PowerShell

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)
}
}