/
Francuz
/
bitrix_setup
Обзор
Документация
Войти
/
Francuz
/
bitrix_setup
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
setup/install.js
510 строк
21 KB
Francuz
fix: переработан автоустановщик на основе ручной установки
24 июл 2026, 14:46
24 июл 2026, 14:46
4d2448b
Код
Авторство
О чём код?
const { chromium } = require('playwright'); const WEB_URL = process.env.WEB_URL || 'http://web:80'; const ADMIN_LOGIN = process.env.BITRIX_ADMIN_LOGIN || 'admin'; const ADMIN_PASSWORD = process.env.BITRIX_ADMIN_PASSWORD || 'bitrix'; const ADMIN_EMAIL = process.env.BITRIX_ADMIN_EMAIL || 'admin@example.com'; const ADMIN_FIRST_NAME = process.env.BITRIX_ADMIN_FIRST_NAME || 'Иван'; const ADMIN_LAST_NAME = process.env.BITRIX_ADMIN_LAST_NAME || 'Иванов'; const BITRIX_SOLUTION = process.env.BITRIX_SOLUTION || 'none'; const fs = require('fs'); const LOG_FILE = '/logs/install.log'; function log(msg) { const line = `[${new Date().toISOString()}] ${msg}`; console.log(line); try { fs.appendFileSync(LOG_FILE, line + '\n'); } catch(e) {} } function delay(ms) { return new Promise(r => setTimeout(r, ms)); } async function removeContainer() { try { const http = require('http'); const fs = require('fs'); const containerId = fs.readFileSync('/etc/hostname', 'utf-8').trim(); log(`Удаление контейнера: ${containerId}`); await new Promise((resolve) => { const req = http.request({ socketPath: '/var/run/docker.sock', path: `/containers/${containerId}?force=true`, method: 'DELETE' }, (res) => { let data = ''; res.on('data', c => data += c); res.on('end', () => { if (res.statusCode === 204 || res.statusCode === 200) { log('Контейнер setup удалён'); } else { log(`Не удалось удалить контейнер: HTTP ${res.statusCode} ${data}`); } resolve(); }); }); req.on('error', (e) => { log(`Не удалось удалить контейнер: ${e.message}`); resolve(); }); req.end(); }); } catch (e) { log(`Не удалось удалить контейнер: ${e.message}`); } } async function getCurrentStep(page) { return await page.evaluate(() => { const el = document.querySelector('input[name="CurrentStepID"]'); return el ? el.value : ''; }).catch(() => ''); } async function prefillWizardFields(page, extraFields = {}) { return await page.evaluate((f) => { const results = []; for (const [name, value] of Object.entries(f)) { let el = document.querySelector(`[name="${name}"]`); if (!el) { el = document.createElement('input'); el.type = 'hidden'; el.name = name; const form = document.querySelector('form'); if (form) form.appendChild(el); } el.value = value; results.push(`OK: ${name}`); } return results; }, extraFields); } // Проверка загрузки CSS на странице async function verifyCssLoaded(page, timeoutMs = 10000) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const hasCss = await page.evaluate(() => { const links = document.querySelectorAll('link[rel="stylesheet"]'); return links.length > 0; }).catch(() => false); if (hasCss) { log(' CSS загружен'); return true; } await delay(500); } log(' WARNING: CSS не загружен за ' + timeoutMs + 'мс'); return false; } // Клик "Далее" + ожидание смены шага (AJAX-цепка через wizard JS) async function clickNextAndWait(page, timeoutSec = 600) { const prevStep = await getCurrentStep(page); log(` Шаг до клика: ${prevStep}`); try { await page.click('input[name="StepNext"]', { timeout: 15000 }); } catch (e) { log(' Кнопка "Далее" не найдена'); return false; } // Ждём смены CurrentStepID for (let i = 0; i < timeoutSec; i++) { await delay(1000); const newStep = await getCurrentStep(page); if (newStep !== prevStep && newStep !== '') { log(` Шаг: ${newStep}`); return true; } if (i > 0 && i % 60 === 0) log(` Ожидание... ${i}с (текущий: ${newStep || prevStep})`); } log(' Таймаут'); return false; } // Ожидание завершения AJAX-цепки модулей в iframe async function waitForAjaxChain(page, timeoutSec = 900) { let lastIframeStep = ''; let stableCount = 0; const STABLE_THRESHOLD = 10; // 10 секунд стабильности = завершено for (let i = 0; i < timeoutSec; i++) { await delay(1000); // Проверяем iframe — там идёт AJAX-цепка const iframeInfo = await page.evaluate(() => { try { const iframe = document.querySelector('iframe'); if (!iframe || !iframe.contentDocument) return null; const el = iframe.contentDocument.querySelector('input[name="CurrentStepID"]'); return el ? el.value : ''; } catch(e) { return null; } }).catch(() => null); // Если iframe исчез — цепка завершена if (iframeInfo === null) { log(` iframe исчез (завершено)`); return 'iframe_gone'; } // Если iframe пустой — возможно загружается if (iframeInfo === '') { if (i > 0 && i % 60 === 0) log(` Ожидание AJAX... ${i}с (iframe: пустой)`); continue; } // Отслеживаем стабилизацию if (iframeInfo === lastIframeStep) { stableCount++; if (stableCount >= STABLE_THRESHOLD) { log(` iframe стабилизировался: ${iframeInfo} (${STABLE_THRESHOLD}с без изменений)`); return iframeInfo; } } else { if (lastIframeStep) log(` iframe: ${lastIframeStep} → ${iframeInfo}`); lastIframeStep = iframeInfo; stableCount = 0; } if (i > 0 && i % 60 === 0) log(` Ожидание AJAX... ${i}с (iframe: ${iframeInfo}, стабильность: ${stableCount}/${STABLE_THRESHOLD})`); } log(' Таймаут AJAX-цепки'); return lastIframeStep || ''; } async function main() { log('Запуск автоматической установки Bitrix (нативный iframe)'); log(`URL: ${WEB_URL}`); // Ожидание веб-сервера log('Ожидание веб-сервера...'); const http = require('http'); let webBody = ''; let webReady = false; for (let i = 0; i < 300; i++) { try { webBody = await new Promise((resolve, reject) => { const url = new URL(WEB_URL); const req = http.request({ hostname: url.hostname, port: url.port || 80, path: '/', method: 'GET', timeout: 5000 }, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve(body)); }); req.on('error', reject); req.end(); }); webReady = true; log('Веб-сервер доступен'); break; } catch (e) { if (i % 30 === 0) log(` Попытка ${i}/300...`); await delay(3000); } } if (!webReady) { log('ОШИБКА: Веб-сервер не доступен'); process.exit(1); } // Проверка: Bitrix уже установлен? if (!webBody.includes('StepNext') && !webBody.includes('CurrentStepID')) { log('Bitrix уже установлен. Пропуск автоустановки.'); log(`Адрес: http://localhost:${process.env.WEB_PORT || 8080}/`); log(`Админка: http://localhost:${process.env.WEB_PORT || 8080}/bitrix/admin/`); log(`Логин: ${ADMIN_LOGIN}`); log(`Пароль: ${ADMIN_PASSWORD}`); await removeContainer(); process.exit(0); } log('Мастер установки обнаружен, запуск автоустановки...'); // Ожидание MySQL log('Ожидание MySQL...'); const net = require('net'); for (let i = 0; i < 120; i++) { try { const ok = await new Promise((resolve) => { const sock = net.connect(3306, process.env.MYSQL_HOST || 'db', () => { sock.destroy(); resolve(true); }); sock.on('error', () => resolve(false)); sock.setTimeout(2000, () => { sock.destroy(); resolve(false); }); }); if (ok) { log('MySQL доступен'); break; } } catch (e) {} if (i % 10 === 0) log(` Попытка MySQL ${i}/120...`); await delay(3000); } // Запуск браузера log('Запуск браузера...'); const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); page.setDefaultTimeout(120000); // 2 минуты таймаут по умолчанию try { log('Открытие мастера установки...'); await page.goto(`${WEB_URL}/`, { waitUntil: 'networkidle', timeout: 120000 }); await delay(5000); // Ждём полной загрузки страницы // Проверяем загрузку CSS await verifyCssLoaded(page, 15000); let step = await getCurrentStep(page); log(`Текущий шаг: ${step}`); // === ФАЗА 1: ОСНОВНОЙ МАСТЕР === // Шаг 1: Приветствие log('1/7 Приветствие'); await clickNextAndWait(page); // Шаг 2: Лицензионное соглашение log('2/7 Лицензионное соглашение'); await prefillWizardFields(page, { '__wiz_agree_license': 'Y' }); await clickNextAndWait(page); // Шаг 3: Ключ лицензии (пропуск) — через fetch (page.click блокирует этот шаг) log('3/7 Ключ лицензии (пропуск)'); await prefillWizardFields(page, { '__wiz_lic_key_variant': '', '__wiz_dbType': 'mysql' }); const licHtml = await page.evaluate(async () => { const form = document.querySelector('form'); const fd = new FormData(form); fd.set('StepNext', 'Далее'); fd.set('__wiz_lic_key_variant', ''); fd.set('__wiz_dbType', 'mysql'); const resp = await fetch('/', { method: 'POST', body: fd }); const html = await resp.text(); document.open(); document.write(html); document.close(); const el = document.querySelector('input[name="CurrentStepID"]'); return { step: el ? el.value : '', len: html.length }; }); await delay(2000); log(` → ${licHtml.step} (${licHtml.len} байт)`); // Шаг 4: Проверка окружения log('4/7 Проверка окружения'); await prefillWizardFields(page, { '__wiz_dbType': 'mysql' }); await clickNextAndWait(page); // Шаг 5: Настройка БД log('5/7 Настройка БД'); const DB_HOST = process.env.MYSQL_HOST || 'db'; const DB_NAME = process.env.MYSQL_DATABASE || 'bitrix'; const DB_USER = process.env.MYSQL_USER || 'bitrix'; const DB_PASS = process.env.MYSQL_PASSWORD || 'bitrix'; await prefillWizardFields(page, { '__wiz_host': DB_HOST, '__wiz_user': DB_USER, '__wiz_password': DB_PASS, '__wiz_database': DB_NAME, '__wiz_create_database': 'N', '__wiz_create_user': 'N', '__wiz_dbType': 'mysql', }); // Шаг 6-7: Установка модулей — кликаем "Далее" и ждём AJAX-цепку через iframe log('6-7/7 Установка модулей + обновление (AJAX-цепка, до 15 минут)...'); await page.click('input[name="StepNext"]', { timeout: 15000 }).catch(() => {}); log(' Клик по "Далее" отправлен, ожидаем AJAX-цепку...'); const ajaxResult = await waitForAjaxChain(page, 900); // 15 минут максимум log(` AJAX-цепка завершена: ${ajaxResult}`); // Диагностика: текущее состояние страницы const afterAjaxStep = await getCurrentStep(page); log(` Шаг после AJAX: ${afterAjaxStep}`); const pageUrl = page.url(); log(` URL страницы: ${pageUrl}`); // Загружаем содержимое iframe в parent если нужно const iframeHtml = await page.evaluate(() => { try { const iframe = document.querySelector('iframe'); if (!iframe || !iframe.contentDocument) return ''; return iframe.contentDocument.body ? iframe.contentDocument.body.innerHTML : ''; } catch(e) { return ''; } }).catch(() => ''); if (iframeHtml.length > 100) { await page.evaluate((html) => { document.open(); document.write(html); document.close(); }, iframeHtml); await delay(2000); log(' Содержимое iframe загружено в parent'); } // Создание администратора log('Создание администратора...'); step = await getCurrentStep(page); log(` Текущий шаг: ${step}`); await prefillWizardFields(page, { '__wiz_login': ADMIN_LOGIN, '__wiz_admin_password': ADMIN_PASSWORD, '__wiz_admin_password_confirm': ADMIN_PASSWORD, '__wiz_email': ADMIN_EMAIL, '__wiz_user_name': ADMIN_FIRST_NAME, '__wiz_user_surname': ADMIN_LAST_NAME, 'NextStepID': 'select_wizard', '__wiz_dbType': 'mysql', }); await clickNextAndWait(page); step = await getCurrentStep(page); log(` → ${step}`); // === ФАЗА 2: МАСТЕР РЕШЕНИЙ === if (BITRIX_SOLUTION === 'none') { log('Пропуск мастера решений — кликаем "Далее" на шаге select_wizard'); await page.click('input[name="StepNext"]', { timeout: 15000 }).catch(() => {}); await delay(5000); await waitForAjaxChain(page, 120); } else { log('=== МАСТЕР РЕШЕНИЙ ==='); step = await getCurrentStep(page); log(` Текущий шаг: ${step}`); const solutionMap = { corp_furniture: 'bitrix.sitecorporate:bitrix:corp_furniture', corp_services: 'bitrix.sitecorporate:bitrix:corp_services' }; const solName = solutionMap[BITRIX_SOLUTION] || solutionMap.corp_furniture; // Выбор решения log(`Выбор решения: ${solName}`); await prefillWizardFields(page, { '__wiz_selected_wizard': solName, 'NextStepID': 'select_template', 'StepNext': 'Далее' }); await clickNextAndWait(page); // Выбор шаблона log('Выбор шаблона...'); await prefillWizardFields(page, { 'NextStepID': 'select_theme', }); await clickNextAndWait(page); // Выбор темы log('Выбор темы...'); await prefillWizardFields(page, { '__wiz_furniture_themeID': 'pink', 'NextStepID': 'site_settings', }); await clickNextAndWait(page); // Настройки сайта log('Настройки сайта...'); await prefillWizardFields(page, { 'NextStepID': 'data_install', }); await clickNextAndWait(page); // Установка данных решения — НАТИВНО через iframe log('Установка данных решения (нативный iframe)...'); step = await getCurrentStep(page); log(` Текущий шаг: ${step}`); // Кликаем "Далее" — wizard запустит AJAX-цепку через iframe await page.click('input[name="StepNext"]', { timeout: 15000 }).catch(() => {}); log(' Клик по "Далее" отправлен, ожидаем AJAX-цепку данных решения...'); // Ждём завершения AJAX-цепки (module database/files × N модулей + __finish) const dataInstallResult = await waitForAjaxChain(page, 900); // 15 минут максимум log(` AJAX-цепка данных решений завершена: ${dataInstallResult}`); // Загружаем содержимое iframe в parent const dataIframeHtml = await page.evaluate(() => { try { const iframe = document.querySelector('iframe'); if (!iframe || !iframe.contentDocument) return ''; return iframe.contentDocument.body ? iframe.contentDocument.body.innerHTML : ''; } catch(e) { return ''; } }).catch(() => ''); if (dataIframeHtml.length > 100) { await page.evaluate((html) => { document.open(); document.write(html); document.close(); }, dataIframeHtml); await delay(2000); log(' Содержимое iframe загружено в parent'); } step = await getCurrentStep(page); log(` Шаг после данных: ${step}`); } // Завершение — кликаем "Готово" или "Далее" на финальном шаге log('Завершение...'); step = await getCurrentStep(page); log(` Текущий шаг: ${step}`); // Проверяем, есть ли кнопка "Далее" или "Готово" const hasButton = await page.evaluate(() => { const btn = document.querySelector('input[name="StepNext"]'); return btn ? btn.value : ''; }).catch(() => ''); if (hasButton) { log(` Кнопка: "${hasButton}"`); await page.click('input[name="StepNext"]', { timeout: 15000 }).catch(() => {}); await delay(10000); // Ждём завершения } // Проверяем, установился ли Bitrix (нет wizard form = установлено) const isInstalled = await page.evaluate(() => { return !document.querySelector('input[name="CurrentStepID"]'); }).catch(() => false); if (!isInstalled) { // Пробуем перейти на главную — возможно установка завершена log(' Проверка: переход на главную...'); await page.goto(`${WEB_URL}/`, { waitUntil: 'networkidle', timeout: 60000 }).catch(() => {}); await delay(5000); const finalCheck = await page.evaluate(() => { return !document.querySelector('input[name="CurrentStepID"]'); }).catch(() => false); if (finalCheck) { log(' Установка завершена успешно!'); } else { log(' Внимание: визард всё ещё активен'); const currentStep = await getCurrentStep(page); log(` Текущий шаг: ${currentStep}`); } } else { log(' Установка завершена успешно!'); } log('=== Установка Bitrix завершена! ==='); } catch (e) { log(`ОШИБКА: ${e.message}`); log(`Stack: ${e.stack}`); try { await page.screenshot({ path: '/logs/error-screenshot.png', fullPage: true }); log('Скриншот сохранён: /logs/error-screenshot.png'); } catch(se) { log(`Не удалось сделать скриншот: ${se.message}`); } } finally { await browser.close(); } log(`Адрес: http://localhost:${process.env.WEB_PORT || 8080}/`); log(`Админка: http://localhost:${process.env.WEB_PORT || 8080}/bitrix/admin/`); log(`Логин: ${ADMIN_LOGIN}`); log(`Пароль: ${ADMIN_PASSWORD}`); await removeContainer(); } main().catch(err => { log(`ОШИБКА: ${err.message}`); process.exit(1); });