mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-13 09:24:55 +03:00
Auto-build: copilot (python) from 7fa279c
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
# web-publish v1.2 — Publish 1C infobase via Apache
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Публикация информационной базы 1С через Apache HTTP Server
|
||||
|
||||
.DESCRIPTION
|
||||
Генерирует default.vrd и настраивает httpd.conf для веб-доступа
|
||||
к информационной базе 1С. При необходимости скачивает portable Apache.
|
||||
Идемпотентный — повторный вызов обновляет конфигурацию.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы (для wsap24.dll)
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER AppName
|
||||
Имя публикации (по умолчанию из имени каталога базы)
|
||||
|
||||
.PARAMETER ApachePath
|
||||
Корень Apache (по умолчанию tools\apache24)
|
||||
|
||||
.PARAMETER Port
|
||||
Порт (по умолчанию 8081)
|
||||
|
||||
.PARAMETER Manual
|
||||
Не скачивать Apache — только проверить и дать инструкцию
|
||||
|
||||
.EXAMPLE
|
||||
.\web-publish.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\web-publish.ps1 -InfoBasePath "C:\Bases\MyDB" -AppName "mydb" -Port 9090
|
||||
|
||||
.EXAMPLE
|
||||
.\web-publish.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -Manual
|
||||
#>
|
||||
|
||||
[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]$AppName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ApachePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[int]$Port = 8081,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$Manual
|
||||
)
|
||||
|
||||
# --- Encoding ---
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = Split-Path $found.FullName -Parent
|
||||
} else {
|
||||
Write-Host "Error: платформа 1С не найдена. Укажите -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Leaf) {
|
||||
$V8Path = Split-Path $V8Path -Parent
|
||||
}
|
||||
|
||||
# Validate wsap24.dll
|
||||
$wsapDll = Join-Path $V8Path "wsap24.dll"
|
||||
if (-not (Test-Path $wsapDll)) {
|
||||
Write-Host "Error: wsap24.dll не найден в $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Resolve ApachePath ---
|
||||
if (-not $ApachePath) {
|
||||
$projectRoot = (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.FullName
|
||||
$ApachePath = Join-Path $projectRoot "tools\apache24"
|
||||
}
|
||||
# Ensure absolute path (agent may pass relative like "tools/apache24")
|
||||
if (-not [System.IO.Path]::IsPathRooted($ApachePath)) {
|
||||
$ApachePath = [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $ApachePath))
|
||||
}
|
||||
|
||||
# --- Check / Install Apache ---
|
||||
$httpdExe = Join-Path (Join-Path $ApachePath "bin") "httpd.exe"
|
||||
|
||||
if (-not (Test-Path $httpdExe)) {
|
||||
if ($Manual) {
|
||||
Write-Host "Apache не найден: $ApachePath" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Установите Apache вручную:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Скачайте Apache Lounge (x64) с https://www.apachelounge.com/download/"
|
||||
Write-Host " 2. Распакуйте содержимое Apache24\ в: $ApachePath"
|
||||
Write-Host " 3. Запустите скрипт повторно"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Apache не найден. Скачиваю..." -ForegroundColor Cyan
|
||||
$tmpZip = Join-Path $env:TEMP "apache24.zip"
|
||||
$tmpDir = Join-Path $env:TEMP "apache24_extract"
|
||||
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$wc = New-Object System.Net.WebClient
|
||||
|
||||
# Parse Apache Lounge download page for latest Win64 zip URL
|
||||
$downloadPage = "https://www.apachelounge.com/download/"
|
||||
Write-Host "Определяю актуальную версию с $downloadPage ..."
|
||||
$html = $wc.DownloadString($downloadPage)
|
||||
# Links are typically relative (/download/...), try that first
|
||||
$match = [regex]::Match($html, '(?i)href="(/download/[^"]*?httpd-[^"]*?Win64[^"]*?\.zip)"')
|
||||
if (-not $match.Success) {
|
||||
$match = [regex]::Match($html, '(?i)href="(https://[^"]*?httpd-[^"]*?Win64[^"]*?\.zip)"')
|
||||
}
|
||||
|
||||
if ($match.Success) {
|
||||
$zipUrl = $match.Groups[1].Value
|
||||
if ($zipUrl.StartsWith('/')) {
|
||||
$zipUrl = "https://www.apachelounge.com$zipUrl"
|
||||
}
|
||||
Write-Host "Найдено: $zipUrl" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Не удалось определить ссылку автоматически." -ForegroundColor Yellow
|
||||
Write-Host "Скачайте вручную: $downloadPage" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
$wc.DownloadFile($zipUrl, $tmpZip)
|
||||
} catch {
|
||||
Write-Host "Error: не удалось скачать Apache: $_" -ForegroundColor Red
|
||||
Write-Host "Скачайте вручную: https://www.apachelounge.com/download/" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Распаковка..."
|
||||
if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force }
|
||||
Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force
|
||||
|
||||
# Move Apache24 contents up to ApachePath
|
||||
$innerDir = Join-Path $tmpDir "Apache24"
|
||||
if (-not (Test-Path $innerDir)) {
|
||||
# Try to find Apache24 in nested folder
|
||||
$innerDir = Get-ChildItem $tmpDir -Directory -Recurse -Filter "Apache24" | Select-Object -First 1
|
||||
if ($innerDir) { $innerDir = $innerDir.FullName } else {
|
||||
Write-Host "Error: каталог Apache24 не найден в архиве" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ApachePath)) {
|
||||
New-Item -ItemType Directory -Path $ApachePath -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path (Join-Path $innerDir "*") -Destination $ApachePath -Recurse -Force
|
||||
|
||||
# Cleanup
|
||||
Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Patch ServerRoot in httpd.conf
|
||||
$confFile = Join-Path (Join-Path $ApachePath "conf") "httpd.conf"
|
||||
if (Test-Path $confFile) {
|
||||
$apachePathFwd = $ApachePath -replace '\\','/'
|
||||
$confContent = [System.IO.File]::ReadAllText($confFile)
|
||||
$confContent = $confContent -replace '(?m)^Define SRVROOT .*$', "Define SRVROOT `"$apachePathFwd`""
|
||||
[System.IO.File]::WriteAllText($confFile, $confContent)
|
||||
Write-Host "ServerRoot обновлён: $apachePathFwd" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "Apache установлен: $ApachePath" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# --- Derive AppName ---
|
||||
if (-not $AppName) {
|
||||
if ($InfoBasePath) {
|
||||
$AppName = (Split-Path $InfoBasePath -Leaf) -replace '[^\w]',''
|
||||
} else {
|
||||
$AppName = $InfoBaseRef -replace '[^\w]',''
|
||||
}
|
||||
$AppName = $AppName.ToLower()
|
||||
}
|
||||
$AppName = $AppName.ToLower()
|
||||
|
||||
if (-not $AppName) {
|
||||
Write-Host "Error: не удалось определить имя публикации. Укажите -AppName" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Публикация: $AppName" -ForegroundColor Cyan
|
||||
|
||||
# --- Create publish directory ---
|
||||
$publishDir = Join-Path (Join-Path $ApachePath "publish") $AppName
|
||||
if (-not (Test-Path $publishDir)) {
|
||||
New-Item -ItemType Directory -Path $publishDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Generate default.vrd ---
|
||||
$vrdPath = Join-Path $publishDir "default.vrd"
|
||||
|
||||
$ibParts = @()
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$ibParts += "Srvr="$InfoBaseServer""
|
||||
$ibParts += "Ref="$InfoBaseRef""
|
||||
} else {
|
||||
$ibParts += "File="$InfoBasePath""
|
||||
}
|
||||
if ($UserName) { $ibParts += "Usr="$UserName"" }
|
||||
if ($Password) { $ibParts += "Pwd="$Password"" }
|
||||
$ibString = ($ibParts -join ";") + ";"
|
||||
|
||||
$vrdContent = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<point xmlns="http://v8.1c.ru/8.2/virtual-resource-system"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
base="/$AppName"
|
||||
ib="$ibString"
|
||||
enableStandardOdata="true">
|
||||
<ws pointEnableCommon="true"/>
|
||||
<httpServices publishByDefault="true"/>
|
||||
</point>
|
||||
"@
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($vrdPath, $vrdContent, $utf8Bom)
|
||||
Write-Host "default.vrd: $vrdPath" -ForegroundColor Green
|
||||
|
||||
# --- Update httpd.conf ---
|
||||
$confFile = Join-Path (Join-Path $ApachePath "conf") "httpd.conf"
|
||||
if (-not (Test-Path $confFile)) {
|
||||
Write-Host "Error: httpd.conf не найден: $confFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$confContent = [System.IO.File]::ReadAllText($confFile)
|
||||
$apachePathFwd = $ApachePath -replace '\\','/'
|
||||
$wsapDllFwd = $wsapDll -replace '\\','/'
|
||||
$publishDirFwd = $publishDir -replace '\\','/'
|
||||
$vrdPathFwd = $vrdPath -replace '\\','/'
|
||||
|
||||
# --- Global block (Listen + LoadModule) ---
|
||||
$globalMarkerStart = "# --- 1C: global ---"
|
||||
$globalMarkerEnd = "# --- End: global ---"
|
||||
$globalBlock = @"
|
||||
$globalMarkerStart
|
||||
Listen $Port
|
||||
LoadModule _1cws_module "$wsapDllFwd"
|
||||
$globalMarkerEnd
|
||||
"@
|
||||
|
||||
if ($confContent -match [regex]::Escape($globalMarkerStart)) {
|
||||
# Replace existing global block
|
||||
$pattern = [regex]::Escape($globalMarkerStart) + '[\s\S]*?' + [regex]::Escape($globalMarkerEnd)
|
||||
$confContent = [regex]::Replace($confContent, $pattern, $globalBlock)
|
||||
} else {
|
||||
# Comment out default Listen to avoid port conflict
|
||||
$confContent = $confContent -replace '(?m)^(Listen\s+\d+)', '#$1 # commented by web-publish'
|
||||
# Append global block
|
||||
$confContent = $confContent.TrimEnd() + "`n`n" + $globalBlock + "`n"
|
||||
}
|
||||
|
||||
# --- Publication block ---
|
||||
$pubMarkerStart = "# --- 1C Publication: $AppName ---"
|
||||
$pubMarkerEnd = "# --- End: $AppName ---"
|
||||
$pubBlock = @"
|
||||
$pubMarkerStart
|
||||
Alias "/$AppName" "$publishDirFwd"
|
||||
<Directory "$publishDirFwd">
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
SetHandler 1c-application
|
||||
ManagedApplicationDescriptor "$vrdPathFwd"
|
||||
</Directory>
|
||||
$pubMarkerEnd
|
||||
"@
|
||||
|
||||
if ($confContent -match [regex]::Escape($pubMarkerStart)) {
|
||||
# Replace existing publication block
|
||||
$pattern = [regex]::Escape($pubMarkerStart) + '[\s\S]*?' + [regex]::Escape($pubMarkerEnd)
|
||||
$confContent = [regex]::Replace($confContent, $pattern, $pubBlock)
|
||||
} else {
|
||||
# Append publication block
|
||||
$confContent = $confContent.TrimEnd() + "`n`n" + $pubBlock + "`n"
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($confFile, $confContent)
|
||||
Write-Host "httpd.conf обновлён" -ForegroundColor Green
|
||||
|
||||
# --- Helper: filter httpd processes by our ApachePath ---
|
||||
function Get-OurHttpd {
|
||||
$httpdExeNorm = (Resolve-Path $httpdExe -ErrorAction SilentlyContinue).Path
|
||||
Get-Process httpd -ErrorAction SilentlyContinue | Where-Object {
|
||||
try { $_.Path -eq $httpdExeNorm } catch { $false }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check port availability ---
|
||||
$portCheck = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($portCheck) {
|
||||
$ourProc = Get-OurHttpd
|
||||
if ($ourProc) {
|
||||
# Our Apache holds the port — will restart
|
||||
} else {
|
||||
$holder = Get-Process -Id $portCheck.OwningProcess -ErrorAction SilentlyContinue
|
||||
$holderName = if ($holder) { "$($holder.ProcessName) (PID: $($holder.Id))" } else { "PID $($portCheck.OwningProcess)" }
|
||||
Write-Host "Error: порт $Port занят процессом $holderName" -ForegroundColor Red
|
||||
Write-Host "Укажите другой порт: -Port 9090" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Start Apache if not running ---
|
||||
$httpdProc = Get-OurHttpd
|
||||
if ($httpdProc) {
|
||||
Write-Host "Apache уже запущен (PID: $(($httpdProc | Select-Object -First 1).Id))" -ForegroundColor Yellow
|
||||
Write-Host "Перезапуск для применения конфигурации..."
|
||||
$httpdProc | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 1
|
||||
} else {
|
||||
# Check if a foreign httpd holds the port
|
||||
$foreignHttpd = Get-Process httpd -ErrorAction SilentlyContinue
|
||||
if ($foreignHttpd) {
|
||||
Write-Host "[WARN] Обнаружен сторонний Apache (PID: $(($foreignHttpd | Select-Object -First 1).Id))" -ForegroundColor Yellow
|
||||
Write-Host " Наш Apache: $httpdExe" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Запуск Apache..."
|
||||
Start-Process -FilePath $httpdExe -WorkingDirectory $ApachePath -WindowStyle Hidden
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$httpdCheck = Get-OurHttpd
|
||||
if ($httpdCheck) {
|
||||
Write-Host "Apache запущен (PID: $(($httpdCheck | Select-Object -First 1).Id))" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Apache не удалось запустить" -ForegroundColor Red
|
||||
# Run config test for diagnostics
|
||||
$testResult = & $httpdExe -t 2>&1
|
||||
if ($testResult) {
|
||||
Write-Host "--- httpd -t ---" -ForegroundColor Yellow
|
||||
$testResult | ForEach-Object { Write-Host " $_" }
|
||||
}
|
||||
$errorLog = Join-Path (Join-Path $ApachePath "logs") "error.log"
|
||||
if (Test-Path $errorLog) {
|
||||
Write-Host "--- error.log (последние 10 строк) ---" -ForegroundColor Yellow
|
||||
Get-Content $errorLog -Tail 10
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Result ---
|
||||
Write-Host ""
|
||||
Write-Host "=== Публикация готова ===" -ForegroundColor Green
|
||||
Write-Host "URL: http://localhost:$Port/$AppName" -ForegroundColor Cyan
|
||||
Write-Host "OData: http://localhost:$Port/$AppName/odata/standard.odata" -ForegroundColor Cyan
|
||||
Write-Host "HTTP-сервисы: http://localhost:$Port/$AppName/hs/<RootUrl>/..." -ForegroundColor Cyan
|
||||
Write-Host "Web-сервисы: http://localhost:$Port/$AppName/ws/<Имя>?wsdl" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,428 @@
|
||||
#!/usr/bin/env python3
|
||||
# web-publish v1.2 — Publish 1C infobase via Apache
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
"""
|
||||
Публикация информационной базы 1С через Apache HTTP Server.
|
||||
Генерирует default.vrd и настраивает httpd.conf для веб-доступа
|
||||
к информационной базе 1С. При необходимости скачивает portable Apache.
|
||||
Идемпотентный — повторный вызов обновляет конфигурацию.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
def get_our_httpd(httpd_exe_norm):
|
||||
"""Filter httpd processes by our ApachePath."""
|
||||
result = []
|
||||
if not httpd_exe_norm:
|
||||
return result
|
||||
for p in psutil.process_iter(['pid', 'name', 'exe']):
|
||||
try:
|
||||
if p.info['name'] and 'httpd' in p.info['name'].lower():
|
||||
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
|
||||
result.append(p)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def get_all_httpd():
|
||||
"""Get all httpd processes."""
|
||||
result = []
|
||||
for p in psutil.process_iter(['pid', 'name', 'exe']):
|
||||
try:
|
||||
if p.info['name'] and 'httpd' in p.info['name'].lower():
|
||||
result.append(p)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def check_port_in_use(port):
|
||||
"""Check if a port is in use and return the owning PID, or None."""
|
||||
for conn in psutil.net_connections(kind='tcp'):
|
||||
if conn.laddr and conn.laddr.port == port and conn.status == 'LISTEN':
|
||||
return conn.pid
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description='Publish 1C infobase via Apache', allow_abbrev=False)
|
||||
parser.add_argument('-V8Path', type=str, default='', help='Path to 1C platform bin directory (for wsap24.dll)')
|
||||
parser.add_argument('-InfoBasePath', type=str, default='', help='Path to file infobase')
|
||||
parser.add_argument('-InfoBaseServer', type=str, default='', help='1C server (for server infobase)')
|
||||
parser.add_argument('-InfoBaseRef', type=str, default='', help='Infobase name on server')
|
||||
parser.add_argument('-UserName', type=str, default='', help='1C user name')
|
||||
parser.add_argument('-Password', type=str, default='', help='1C password')
|
||||
parser.add_argument('-AppName', type=str, default='', help='Publication name (default: from infobase folder name)')
|
||||
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
|
||||
parser.add_argument('-Port', type=int, default=8081, help='Port (default: 8081)')
|
||||
parser.add_argument('-Manual', action='store_true', help='Do not download Apache — only check and give instructions')
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8_path = args.V8Path
|
||||
if not v8_path:
|
||||
candidates = glob.glob(r'C:\Program Files\1cv8\*\bin\1cv8.exe')
|
||||
candidates.sort(reverse=True)
|
||||
if candidates:
|
||||
v8_path = os.path.dirname(candidates[0])
|
||||
else:
|
||||
print('Error: платформа 1С не найдена. Укажите -V8Path', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isfile(v8_path):
|
||||
v8_path = os.path.dirname(v8_path)
|
||||
|
||||
# Validate wsap24.dll
|
||||
wsap_dll = os.path.join(v8_path, 'wsap24.dll')
|
||||
if not os.path.exists(wsap_dll):
|
||||
print(f'Error: wsap24.dll не найден в {v8_path}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Resolve ApachePath ---
|
||||
apache_path = args.ApachePath
|
||||
if not apache_path:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
|
||||
apache_path = os.path.join(project_root, 'tools', 'apache24')
|
||||
# Ensure absolute path (agent may pass relative like "tools/apache24")
|
||||
if not os.path.isabs(apache_path):
|
||||
apache_path = os.path.abspath(apache_path)
|
||||
|
||||
port = args.Port
|
||||
|
||||
# --- Check / Install Apache ---
|
||||
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
|
||||
|
||||
if not os.path.exists(httpd_exe):
|
||||
if args.Manual:
|
||||
print(f'Apache не найден: {apache_path}')
|
||||
print('')
|
||||
print('Установите Apache вручную:')
|
||||
print(' 1. Скачайте Apache Lounge (x64) с https://www.apachelounge.com/download/')
|
||||
print(f' 2. Распакуйте содержимое Apache24\\ в: {apache_path}')
|
||||
print(' 3. Запустите скрипт повторно')
|
||||
sys.exit(1)
|
||||
|
||||
print('Apache не найден. Скачиваю...')
|
||||
tmp_zip = os.path.join(tempfile.gettempdir(), 'apache24.zip')
|
||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'apache24_extract')
|
||||
|
||||
try:
|
||||
# Parse Apache Lounge download page for latest Win64 zip URL
|
||||
download_page = 'https://www.apachelounge.com/download/'
|
||||
print(f'Определяю актуальную версию с {download_page} ...')
|
||||
req = urllib.request.Request(download_page, headers={
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
html = resp.read().decode('utf-8', errors='replace')
|
||||
|
||||
# Links are typically relative (/download/...), try that first
|
||||
m = re.search(r'(?i)href="(/download/[^"]*?httpd-[^"]*?Win64[^"]*?\.zip)"', html)
|
||||
if not m:
|
||||
m = re.search(r'(?i)href="(https://[^"]*?httpd-[^"]*?Win64[^"]*?\.zip)"', html)
|
||||
|
||||
if m:
|
||||
zip_url = m.group(1)
|
||||
if zip_url.startswith('/'):
|
||||
zip_url = f'https://www.apachelounge.com{zip_url}'
|
||||
print(f'Найдено: {zip_url}')
|
||||
else:
|
||||
print('Не удалось определить ссылку автоматически.', file=sys.stderr)
|
||||
print(f'Скачайте вручную: {download_page}')
|
||||
sys.exit(1)
|
||||
|
||||
urllib.request.urlretrieve(zip_url, tmp_zip)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f'Error: не удалось скачать Apache: {e}', file=sys.stderr)
|
||||
print('Скачайте вручную: https://www.apachelounge.com/download/')
|
||||
sys.exit(1)
|
||||
|
||||
print('Распаковка...')
|
||||
if os.path.exists(tmp_dir):
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
with zipfile.ZipFile(tmp_zip, 'r') as zf:
|
||||
zf.extractall(tmp_dir)
|
||||
|
||||
# Move Apache24 contents up to ApachePath
|
||||
inner_dir = os.path.join(tmp_dir, 'Apache24')
|
||||
if not os.path.isdir(inner_dir):
|
||||
# Try to find Apache24 in nested folder
|
||||
found_inner = None
|
||||
for root, dirs, files in os.walk(tmp_dir):
|
||||
if 'Apache24' in dirs:
|
||||
found_inner = os.path.join(root, 'Apache24')
|
||||
break
|
||||
if found_inner:
|
||||
inner_dir = found_inner
|
||||
else:
|
||||
print('Error: каталог Apache24 не найден в архиве', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(apache_path, exist_ok=True)
|
||||
# Copy contents of inner_dir to apache_path
|
||||
for item in os.listdir(inner_dir):
|
||||
src = os.path.join(inner_dir, item)
|
||||
dst = os.path.join(apache_path, item)
|
||||
if os.path.isdir(src):
|
||||
if os.path.exists(dst):
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
os.remove(tmp_zip)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Patch ServerRoot in httpd.conf
|
||||
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
|
||||
if os.path.exists(conf_file):
|
||||
apache_path_fwd = apache_path.replace('\\', '/')
|
||||
with open(conf_file, 'r', encoding='utf-8-sig') as f:
|
||||
conf_content = f.read()
|
||||
conf_content = re.sub(
|
||||
r'(?m)^Define SRVROOT .*$',
|
||||
f'Define SRVROOT "{apache_path_fwd}"',
|
||||
conf_content,
|
||||
)
|
||||
with open(conf_file, 'w', encoding='utf-8') as f:
|
||||
f.write(conf_content)
|
||||
print(f'ServerRoot обновлён: {apache_path_fwd}')
|
||||
|
||||
print(f'Apache установлен: {apache_path}')
|
||||
|
||||
# --- Derive AppName ---
|
||||
app_name = args.AppName
|
||||
if not app_name:
|
||||
if args.InfoBasePath:
|
||||
app_name = re.sub(r'[^\w]', '', os.path.basename(args.InfoBasePath))
|
||||
else:
|
||||
app_name = re.sub(r'[^\w]', '', args.InfoBaseRef)
|
||||
app_name = app_name.lower()
|
||||
app_name = app_name.lower()
|
||||
|
||||
if not app_name:
|
||||
print('Error: не удалось определить имя публикации. Укажите -AppName', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f'Публикация: {app_name}')
|
||||
|
||||
# --- Create publish directory ---
|
||||
publish_dir = os.path.join(apache_path, 'publish', app_name)
|
||||
os.makedirs(publish_dir, exist_ok=True)
|
||||
|
||||
# --- Generate default.vrd ---
|
||||
vrd_path = os.path.join(publish_dir, 'default.vrd')
|
||||
|
||||
ib_parts = []
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
ib_parts.append(f'Srvr="{args.InfoBaseServer}"')
|
||||
ib_parts.append(f'Ref="{args.InfoBaseRef}"')
|
||||
else:
|
||||
ib_parts.append(f'File="{args.InfoBasePath}"')
|
||||
if args.UserName:
|
||||
ib_parts.append(f'Usr="{args.UserName}"')
|
||||
if args.Password:
|
||||
ib_parts.append(f'Pwd="{args.Password}"')
|
||||
ib_string = ';'.join(ib_parts) + ';'
|
||||
|
||||
vrd_content = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<point xmlns="http://v8.1c.ru/8.2/virtual-resource-system"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
base="/{app_name}"
|
||||
ib="{ib_string}"
|
||||
enableStandardOdata="true">
|
||||
<ws pointEnableCommon="true"/>
|
||||
<httpServices publishByDefault="true"/>
|
||||
</point>'''
|
||||
|
||||
with open(vrd_path, 'wb') as f:
|
||||
f.write(b'\xef\xbb\xbf')
|
||||
f.write(vrd_content.encode('utf-8'))
|
||||
print(f'default.vrd: {vrd_path}')
|
||||
|
||||
# --- Update httpd.conf ---
|
||||
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
|
||||
if not os.path.exists(conf_file):
|
||||
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(conf_file, 'r', encoding='utf-8-sig') as f:
|
||||
conf_content = f.read()
|
||||
|
||||
apache_path_fwd = apache_path.replace('\\', '/')
|
||||
wsap_dll_fwd = wsap_dll.replace('\\', '/')
|
||||
publish_dir_fwd = publish_dir.replace('\\', '/')
|
||||
vrd_path_fwd = vrd_path.replace('\\', '/')
|
||||
|
||||
# --- Global block (Listen + LoadModule) ---
|
||||
global_marker_start = '# --- 1C: global ---'
|
||||
global_marker_end = '# --- End: global ---'
|
||||
global_block = (
|
||||
f'{global_marker_start}\n'
|
||||
f'Listen {port}\n'
|
||||
f'LoadModule _1cws_module "{wsap_dll_fwd}"\n'
|
||||
f'{global_marker_end}'
|
||||
)
|
||||
|
||||
if re.search(re.escape(global_marker_start), conf_content):
|
||||
# Replace existing global block
|
||||
pattern = re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end)
|
||||
conf_content = re.sub(pattern, global_block, conf_content)
|
||||
else:
|
||||
# Comment out default Listen to avoid port conflict
|
||||
conf_content = re.sub(r'(?m)^(Listen\s+\d+)', r'#\1 # commented by web-publish', conf_content)
|
||||
# Append global block
|
||||
conf_content = conf_content.rstrip() + '\n\n' + global_block + '\n'
|
||||
|
||||
# --- Publication block ---
|
||||
pub_marker_start = f'# --- 1C Publication: {app_name} ---'
|
||||
pub_marker_end = f'# --- End: {app_name} ---'
|
||||
pub_block = (
|
||||
f'{pub_marker_start}\n'
|
||||
f'Alias "/{app_name}" "{publish_dir_fwd}"\n'
|
||||
f'<Directory "{publish_dir_fwd}">\n'
|
||||
f' AllowOverride All\n'
|
||||
f' Require all granted\n'
|
||||
f' SetHandler 1c-application\n'
|
||||
f' ManagedApplicationDescriptor "{vrd_path_fwd}"\n'
|
||||
f'</Directory>\n'
|
||||
f'{pub_marker_end}'
|
||||
)
|
||||
|
||||
if re.search(re.escape(pub_marker_start), conf_content):
|
||||
# Replace existing publication block
|
||||
pattern = re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end)
|
||||
conf_content = re.sub(pattern, pub_block, conf_content)
|
||||
else:
|
||||
# Append publication block
|
||||
conf_content = conf_content.rstrip() + '\n\n' + pub_block + '\n'
|
||||
|
||||
with open(conf_file, 'w', encoding='utf-8') as f:
|
||||
f.write(conf_content)
|
||||
print('httpd.conf обновлён')
|
||||
|
||||
# --- Normalize httpd_exe for process matching ---
|
||||
if os.path.exists(httpd_exe):
|
||||
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
|
||||
else:
|
||||
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
|
||||
|
||||
# --- Check port availability ---
|
||||
holder_pid = check_port_in_use(port)
|
||||
if holder_pid:
|
||||
our_proc = get_our_httpd(httpd_exe_norm)
|
||||
if not our_proc:
|
||||
# Port is held by someone else
|
||||
try:
|
||||
holder_proc = psutil.Process(holder_pid)
|
||||
holder_name = f'{holder_proc.name()} (PID: {holder_pid})'
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
holder_name = f'PID {holder_pid}'
|
||||
print(f'Error: порт {port} занят процессом {holder_name}', file=sys.stderr)
|
||||
print('Укажите другой порт: -Port 9090')
|
||||
sys.exit(1)
|
||||
|
||||
# --- Start Apache if not running ---
|
||||
httpd_proc = get_our_httpd(httpd_exe_norm)
|
||||
if httpd_proc:
|
||||
first_pid = httpd_proc[0].pid
|
||||
print(f'Apache уже запущен (PID: {first_pid})')
|
||||
print('Перезапуск для применения конфигурации...')
|
||||
for p in httpd_proc:
|
||||
try:
|
||||
p.kill()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
time.sleep(1)
|
||||
else:
|
||||
# Check if a foreign httpd holds the port
|
||||
foreign_httpd = get_all_httpd()
|
||||
if foreign_httpd:
|
||||
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign_httpd[0].pid})')
|
||||
print(f' Наш Apache: {httpd_exe}')
|
||||
|
||||
print('Запуск Apache...')
|
||||
subprocess.Popen(
|
||||
[httpd_exe],
|
||||
cwd=apache_path,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
httpd_check = get_our_httpd(httpd_exe_norm)
|
||||
if httpd_check:
|
||||
print(f'Apache запущен (PID: {httpd_check[0].pid})')
|
||||
else:
|
||||
print('Apache не удалось запустить', file=sys.stderr)
|
||||
# Run config test for diagnostics
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[httpd_exe, '-t'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
test_output = (result.stdout + result.stderr).strip()
|
||||
if test_output:
|
||||
print('--- httpd -t ---')
|
||||
for line in test_output.splitlines():
|
||||
print(f' {line}')
|
||||
except Exception:
|
||||
pass
|
||||
error_log = os.path.join(apache_path, 'logs', 'error.log')
|
||||
if os.path.exists(error_log):
|
||||
print('--- error.log (последние 10 строк) ---')
|
||||
try:
|
||||
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
|
||||
all_lines = f.readlines()
|
||||
for line in all_lines[-10:]:
|
||||
print(line.rstrip())
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(1)
|
||||
|
||||
# --- Result ---
|
||||
print('')
|
||||
print('=== Публикация готова ===')
|
||||
print(f'URL: http://localhost:{port}/{app_name}')
|
||||
print(f'OData: http://localhost:{port}/{app_name}/odata/standard.odata')
|
||||
print(f'HTTP-сервисы: http://localhost:{port}/{app_name}/hs/<RootUrl>/...')
|
||||
print(f'Web-сервисы: http://localhost:{port}/{app_name}/ws/<Имя>?wsdl')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user