# db-update v1.11 — Update 1C database configuration # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# .SYNOPSIS Обновление конфигурации базы данных 1С .DESCRIPTION Применяет изменения основной конфигурации к конфигурации базы данных. Поддерживает динамическое обновление, обновление расширений. .PARAMETER V8Path Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER InfoBasePath Путь к файловой информационной базе .PARAMETER InfoBaseServer Сервер 1С (для серверной базы) .PARAMETER InfoBaseRef Имя базы на сервере .PARAMETER UserName Имя пользователя 1С .PARAMETER Password Пароль пользователя .PARAMETER Extension Имя расширения для обновления .PARAMETER AllExtensions Обновить все расширения .PARAMETER Dynamic Динамическое обновление: "+" включить, "-" отключить .PARAMETER Server Обновление на стороне сервера .PARAMETER WarningsAsErrors Предупреждения считать ошибками .PARAMETER AdditionalV8Arguments Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) .PARAMETER AdditionalIbcmdArguments Дополнительные аргументы запуска ibcmd (форма --ключ=значение) .EXAMPLE .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" .EXAMPLE .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" #> [CmdletBinding()] param( [Parameter(Mandatory=$false)] [string]$V8Path, [Parameter(Mandatory=$false)] [string]$InfoBasePath, [Parameter(Mandatory=$false)] [string]$InfoBaseServer, [Parameter(Mandatory=$false)] [string]$InfoBaseRef, [Parameter(Mandatory=$false)] [string]$UserName, [Parameter(Mandatory=$false)] [string]$Password, [Parameter(Mandatory=$false)] [string]$Extension, [Parameter(Mandatory=$false)] [switch]$AllExtensions, [Parameter(Mandatory=$false)] [ValidateSet("+", "-")] [string]$Dynamic, [Parameter(Mandatory=$false)] [switch]$Server, [Parameter(Mandatory=$false)] [switch]$WarningsAsErrors, [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), [Parameter(Mandatory=$false)] [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 function Protect-Secrets { # Redact literal secret values from a display string (String.Replace is literal, not regex). param([string]$Text, [string[]]$Secrets) foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } } return $Text } function Get-ExitAnnotation { # Annotate an abnormal process exit code so a crash isn't reported as a bare number. # A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or # half-updated — surface that instead of a plain code. (Windows exception codes only; # POSIX signals are handled in the .py port.) param([int]$Code) $win = @{ -1073741819 = "0xC0000005 (access violation)" -1073741515 = "0xC0000135 (missing DLL)" -1073740791 = "0xC0000409 (stack overrun)" } if ($win.ContainsKey($Code)) { return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying" } return "" } # --- Additional platform arguments --- $script:V8OwnedKeys = @( 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' ) $script:IbcmdOwnedKeys = @( '--db-path', '--data', '--out', '--file', '--load', '--restore', '--import', '--export', '--apply', '--force', '--create-database', '--user', '--password' ) $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') function Test-ArgKeyMatch { # A token matches a key when it equals the key, or starts with it and the next # character is not a letter — catches glued /N"user" and --password=x, while # keeping /ClearCache distinct from /C. param([string]$Token, [string]$Key) if ($Token.Length -lt $Key.Length) { return $false } if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false } if ($Token.Length -eq $Key.Length) { return $true } return -not [char]::IsLetter($Token[$Key.Length]) } function Get-ProjectExtraArgs { # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path. param([string]$Name) $dir = (Get-Location).Path while ($dir) { $pf = Join-Path $dir ".v8-project.json" if (Test-Path $pf) { try { $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) } } catch {} return @() } $parent = Split-Path $dir -Parent if (-not $parent -or $parent -eq $dir) { break } $dir = $parent } return @() } function Assert-ExtraArgs { # The platform accepts only one batch operation, and a duplicate connection or # output key fails with an opaque 1C error — reject what the skill owns itself. param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints) $paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' } $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys } foreach ($tok in $ExtraArgs) { if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') { Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red exit 1 } foreach ($k in $owned) { if (Test-ArgKeyMatch $tok $k) { $hint = '' if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" } Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red exit 1 } } } } function Resolve-ExtraArgs { # Pick the argument list for the selected engine and validate it. An explicitly passed # parameter for the other engine is an error; the same keys coming from .v8-project.json # simply do not apply — a project may describe both engines. param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red exit 1 } if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red exit 1 } if ($Engine -eq 'ibcmd') { $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) } else { $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) } if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there # would nest the array — the tokens would then be glued into one argument. return $extra } function Format-ArgsForDisplay { # Redact values of secret-prone keys in glued, =-joined and separate forms. # Matching here is a plain prefix (no letter rule): over-masking costs nothing, # a leaked password does. param([string[]]$ArgList, [string]$Engine) $keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys } $res = @() $maskNext = $false foreach ($tok in $ArgList) { if ($maskNext) { $res += '***'; $maskNext = $false; continue } $hit = $null foreach ($k in $keys) { if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break } } if (-not $hit) { $res += $tok; continue } if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true } elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') } else { $res += ($hit + '***') } } return ,$res } # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path while ($dir) { $pf = Join-Path $dir ".v8-project.json" if (Test-Path $pf) { try { $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json if ($j.v8path) { return [string]$j.v8path } } catch {} return $null } $parent = Split-Path $dir -Parent if (-not $parent -or $parent -eq $dir) { break } $dir = $parent } return $null } if (-not $V8Path) { $V8Path = Find-ProjectV8Path } if (-not $V8Path) { $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | Select-Object -First 1 if ($found) { $V8Path = $found.FullName Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow } else { Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red exit 1 } } if (Test-Path $V8Path -PathType Container) { $V8Path = Join-Path $V8Path "1cv8.exe" } if (-not (Test-Path $V8Path)) { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red exit 1 } # --- Detect engine (ibcmd vs 1cv8) by exe name --- function Invoke-IbcmdProcess { # Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. param([string]$Exe, [string[]]$IbArgs) $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Exe $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $psi.RedirectStandardInput = $true $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true try { $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866) $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866) } catch {} $p = [System.Diagnostics.Process]::Start($psi) $p.StandardInput.Close() $out = $p.StandardOutput.ReadToEnd() $err = $p.StandardError.ReadToEnd() $p.WaitForExit() if ($err) { $out += $err } return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } # --- Resolve additional arguments for the selected engine --- $argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } $extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red exit 1 } } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red exit 1 } # --- Temp dir --- $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null try { if ($engine -eq "ibcmd") { # --- ibcmd branch (file infobase only) --- if ($AllExtensions) { Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red exit 1 } $arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force") if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" } elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" } if ($Extension) { $arguments += "--extension=$Extension" } if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" $arguments += $extraArgs Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode if ($exitCode -eq 0) { Write-Host "Database configuration updated successfully" -ForegroundColor Green } else { Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red } if ($output) { Write-Host ($output | Out-String) } exit $exitCode } # --- 1cv8 branch --- # --- Build arguments --- $arguments = @("DESIGNER") if ($InfoBaseServer -and $InfoBaseRef) { $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" } else { $arguments += "/F", "`"$InfoBasePath`"" } if ($UserName) { $arguments += "/N`"$UserName`"" } if ($Password) { $arguments += "/P`"$Password`"" } $arguments += "/UpdateDBCfg" # --- Options --- if ($Dynamic) { $arguments += "-Dynamic$Dynamic" } if ($Server) { $arguments += "-Server" } if ($WarningsAsErrors) { $arguments += "-WarningsAsErrors" } # --- Extensions --- if ($Extension) { $arguments += "-Extension", "`"$Extension`"" } elseif ($AllExtensions) { $arguments += "-AllExtensions" } # --- Output --- $outFile = Join-Path $tempDir "update_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" $arguments += $extraArgs # --- Execute --- Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode # --- Result --- if ($exitCode -eq 0) { Write-Host "Database configuration updated successfully" -ForegroundColor Green } else { Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red } if (Test-Path $outFile) { $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if ($logContent) { Write-Host "--- Log ---" Write-Host $logContent Write-Host "--- End ---" } } exit $exitCode } finally { if (Test-Path $tempDir) { Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue } }